DEV Community

Amos Isaila
Amos Isaila

Posted on • Originally published at codigotipado.com on

Angular 20.2.0: What’s new

Angular 20.2.0 introduces cleaner templates, smarter tooling, and improved debugging as some of its new features.

Angular 20.2.0

1. 🔥 @angular/core

a) Support TypeScript 5.9

b) Angular Components Now Use Real Tag Names in Tests — PR

Until now, there was a subtle but important difference between how your components behaved in tests versus production:

In Production: angular creates components using tag names inferred from their selectors:

@Component({selector: 'user-profile', template: '...'})
// Creates actual <user-profile> element
Enter fullscreen mode Exit fullscreen mode

In Tests (Previously): TestBed always wrapped components in generic

elements:
const fixture = TestBed.createComponent(UserProfileComponent);
// Always created <div> regardless of selector

This mismatch could hide CSS styling issues, accessibility problems, or any logic that depended on the actual element tag name.

Angular now offers the inferTagName option to align test behavior with production:

// Option 1: Per-component basis
const fixture = TestBed.createComponent(UserProfileComponent, {
  inferTagName: true
});
// Now creates <user-profile> element in tests

// Option 2: Configure globally for all tests
TestBed.configureTestingModule({
  inferTagName: true,
  // ... other config
});
@Component({selector: 'my-button'}) 
// → Creates <my-button>
@Component({selector: 'custom-input[type="text"]'}) 
// → Creates <custom-input>
@Component({selector: '[data-widget]'}) 
// → Falls back to <div> (no tag name in selector)
@Component({template: '...'}) 
// → Creates <ng-component> (no selector)

c) Property-to-Attribute Mapping — PR

Previously, developers faced a choice between verbose syntax and potential SSR problems when working with ARIA attributes:

// Verbose but SSR-safe
<button [attr.aria-label]="buttonText">

// Clean but potential SSR issues  
<button [ariaLabel]="buttonText">

The problem: ARIA DOM properties don’t always correctly reflect as HTML attributes during server-side rendering, potentially breaking accessibility for users with assistive technologies.

Angular now supports clean property binding syntax that automatically renders as proper HTML attributes:

// All of these now work identically and render correctly on SSR
<button [aria-label]="buttonText"> // New simplified syntax
<button [ariaLabel]="buttonText"> // Existing camelCase
<button [attr.aria-label]="buttonText"> // Explicit attribute binding

The enhancement includes intelligent mapping for all standard ARIA properties:

@Component({
  template: `
    <div [ariaLabel]="label"
         [ariaExpanded]="isExpanded" 
         [ariaDisabled]="isDisabled"
         [aria-hidden]="isHidden">
      <!-- All render as proper aria-* attributes -->
    </div>
  `
})

Automatic conversions include:

  • ariaLabel → aria-label
  • ariaExpanded → aria-expanded
  • ariaHasPopup → aria-haspopup
  • Plus 30+ other ARIA properties

The system intelligently prioritizes component inputs over attribute binding:

@Component({
  selector: 'custom-button'
})
class CustomButton {
  @Input() ariaLabel!: string; // This takes precedence
}

// Binds to component input, not HTML attribute
<custom-button [ariaLabel]="text">

This feature particularly shines in SSR scenarios where proper attribute rendering is crucial for accessibility:

// Server renders: <button aria-label="Save Document">
// Client hydrates seamlessly with identical markup
<button [aria-label]="saveLabel">

d) Promote zoneless to stable

As of Angular v20.2, Zoneless (provideZonelessChangeDetection) Angular is now stable and includes improvements in error handling and server-side rendering.

e) Control Flow Enhancement: as Aliases in @else if Blocks - PR

<!-- ✅ Enhanced: as works in @else if blocks -->
@if (user$ | async; as user) {
  <h1>Welcome, {{user.name}}</h1>
  <p>Role: {{user.role}}</p>
} @else if (userRole$ | async; as role) {
  <!-- 🎉 Now we can use 'as' here too! -->
  <p>Loading user data for {{role}}...</p>
  <p>Please wait while we fetch your {{role}} profile...</p>
} @else {
  <p>Please log in</p>
}

2. 🔥 @angular/common/http

a) Add referrer & integrity support for fetch requests in httpResource — PR

Currently, Angular’s httpResource does not expose the referrer and integrity options from the underlying Fetch API.

Exposing these options would provide developers with finer control over the request’s referrer and subresource integrity validation , which are important for ensuring security , privacy , and trust in critical resource fetching scenarios.

httpResource(() => ({
  url: '${CDN_DOMAIN}/assets/data.json',
  method: 'GET',
  referrer: 'no-referrer',
  integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GhEXAMPLEKEY='
}));

b) New redirected Property - PR

Angular’s HttpClient now provides complete visibility into HTTP redirects with a new redirected property that aligns with the native Fetch API.

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  constructor(private http: HttpClient) {}

  getUserProfile(userId: string) {
    return this.http.get(`/api/users/${userId}`, { 
      observe: 'response' 
    }).pipe(
      tap(response => {
        if (response.redirected) {
          console.log('User profile request was redirected');
          console.log('Original URL:', `/api/users/${userId}`);
          console.log('Final URL:', response.url);

          // Track redirect for analytics
          this.trackRedirect('user-profile', response.url);
        }
      }),
      map(response => response.body)
    );
  }

  private trackRedirect(endpoint: string, finalUrl: string | null) {
    // Send redirect data to analytics service
    analytics.track('http_redirect', {
      endpoint,
      finalUrl,
      timestamp: new Date().toISOString()
    });
  }
}

3. 🔥 @angular/language-service

a) Angular Language Service Now Detects Deprecated APIs in Templates — PR

export class LegacyComponent {
  /**
   * @deprecated Use newProp instead
   */
  @Input() oldProp: string;
}

<!-- Your IDE will now show a suggestion diagnostic here -->
<legacy-component [oldProp]="someValue"></legacy-component>
                   ~~~~~~~'oldProp' is deprecated. Use newProp instead

b) Auto-Import for Angular Attributes: No More Manual Directive Imports — PR

When you use directive attributes in your templates, the IDE will now automatically suggest importing the directive for you.

<!-- Type this: -->
<div appHighlight="yellow">
     ↑ 
💡 Quick Fix: Import HighlightDirective from '@app/highlight'

// Automatically added to your component:
import { HighlightDirective } from '@app/highlight';
@Component({
  imports: [HighlightDirective], // ← Added automatically!
  template: `<div appHighlight="yellow">Content</div>`
})
export class MyComponent { }

4. 🔥 @angular/service-worker

a) Take Control of Service Worker Updates: Angular’s New updateViaCache Configuration Option — PR

The new updateViaCache option allows you to specify exactly when the browser should check its HTTP cache when updating service workers or any scripts imported via importScripts(). This translates to:

Improved Performance Control : choose between 'imports', 'all', or 'none' to optimize your update strategy based on your application's specific needs.

Better Development Experience : gain more predictable behavior during development cycles, especially when testing service worker updates.

Production Optimization : fine-tune caching strategies for production deployments where update timing is critical.

export const appConfig: ApplicationConfig = {
  providers: [
    provideServiceWorker('ngsw-worker.js', {
      enabled: !isDevMode(),
      updateViaCache: 'imports', // New cache control option
      registrationStrategy: 'registerWhenStable:30000',
    }),
  ],
};

b) New proactive storage monitoring system that prevents cache failures before they happen — PR

The system monitors storage usage and alerts when capacity reaches 95% of the available quota:

private async detectStorageFull() {
  try {
    const estimate = await navigator.storage.estimate();
    const { quota, usage } = estimate;

    // Handle cases where quota or usage might be undefined
    if (typeof quota !== 'number' || typeof usage !== 'number') {
      return;
    }

    // Consider storage "full" if usage is >= 95% of quota
    // This provides a safety buffer before actual storage exhaustion
    const usagePercentage = (usage / quota) * 100;
    const isStorageFull = usagePercentage >= 95;

    if (isStorageFull) {
      this.debugHandler.log(
        'Storage is full or nearly full',
        `DataGroup(${this.config.name}@${this.config.version}).detectStorageFull()`,
      );
    }
  } catch {
    // Error estimating storage, possibly by unsupported browser.
  }
}

For PWA Applications

// Example of how this helps PWAs maintain offline functionality
if (storageNearFull) {
  // Implement cleanup strategies
  // Prioritize critical resources
  // Notify user about storage constraints
}

For Content-Heavy Apps

// Applications with large caching needs benefit from early warnings
if (storageApproachingLimit) {
  // Implement cache eviction policies
  // Compress cached data
  // Switch to selective caching strategies
}

Developers now get clear indicators when storage issues occur:

Storage is full or nearly full -DataGroup(api@v1.2.3).detectStorageFull()

c) Real-Time Version Failure Notifications — PR

Previously, when a Service Worker version encountered critical failures, applications would experience degraded functionality without clear visibility into the root cause. The system introduces a new VersionFailedEvent that provides comprehensive failure information:

/**
 * An event emitted when a specific version of the app has encountered a critical failure
 * that prevents it from functioning correctly.
 */
export interface VersionFailedEvent {
  type: 'VERSION_FAILED';
  version: {hash: string; appData?: object};
  error: string;
}

Automatic Client Notification

When a version fails, the Service Worker automatically notifies all affected clients. Applications can listen for version failures using the existing SwUpdate service:

@Component({
  selector: 'app-root',
  template: `
    <div class="app">
      @if (versionError()) {
        <div class="error-banner">
          <h3>Application Update Issue</h3>
          <p>{{versionError()}}</p>
          <button (click)="handleVersionFailure()">
            Refresh Application
          </button>
        </div>
      }

      <router-outlet />
    </div>
  `,
  styles: [`
    .error-banner {
      background: #fee;
      border: 1px solid #fcc;
      border-radius: 4px;
      padding: 16px;
      margin: 8px;
      color: #c66;
    }

    .error-banner button {
      background: #c66;
      color: white;
      border: none;
      padding: 8px 16px;
      border-radius: 4px;
      cursor: pointer;
      margin-top: 8px;
    }
  `]
})
export class AppComponent implements OnInit {
  versionError = signal<string | null>(null);

  constructor(private swUpdate: SwUpdate) {}

  ngOnInit() {
    // Listen for all version events
    this.swUpdate.versionUpdates.subscribe(event => {
      switch (event.type) {
        case 'VERSION_FAILED':
          this.handleVersionFailure(event);
          break;
        case 'VERSION_READY':
          this.handleVersionReady(event);
          break;
        case 'VERSION_DETECTED':
          this.handleVersionDetected(event);
          break;
      }
    });
  }

  private handleVersionFailure(event: VersionFailedEvent) {
    console.error('Service Worker version failed:', event);

    // Set user-friendly error message
    this.versionError.set(
      `Application version ${event.version.hash.substring(0, 8)} has encountered an error. ` +
      `Please refresh to restore full functionality.`
    );

    // Optional: Report to error monitoring service
    this.reportVersionFailure(event);
  }

  private handleVersionReady(event: VersionReadyEvent) {
    // Clear any previous errors when new version is ready
    this.versionError.set(null);
    console.log('New version ready:', event.latestVersion.hash);
  }

  private handleVersionDetected(event: VersionDetectedEvent) {
    console.log('New version detected:', event.latestVersion.hash);
  }

  handleVersionFailure() {
    // Force page reload to get latest version
    window.location.reload();
  }

  private reportVersionFailure(event: VersionFailedEvent) {
    // Example: Send to monitoring service
    // errorService.report({
    // type: 'service-worker-version-failure',
    // version: event.version.hash,
    // error: event.error,
    // userAgent: navigator.userAgent,
    // timestamp: new Date().toISOString()
    // });
  }
}

Testing the Feature

The new functionality includes comprehensive test coverage:

it('processes version failed events with cache corruption error', (done) => {
  update.versionUpdates.subscribe((event) => {
    expect(event.type).toEqual('VERSION_FAILED');
    expect((event as VersionFailedEvent).version).toEqual({
      hash: 'B',
      appData: {name: 'test-app'},
    });
    expect((event as VersionFailedEvent).error).toContain('Cache corruption detected');
    done();
  });

  mock.sendMessage({
    type: 'VERSION_FAILED',
    version: {
      hash: 'B',
      appData: {name: 'test-app'},
    },
    error: 'Cache corruption detected during resource fetch',
  });
});

d) Better Service Worker Debugging: Angular Now Catches Message Errors — PR

When Service Workers receive corrupted or badly formatted messages, they would previously fail silently. Instead of silent failures, you now get clear logs when messages fail:

[SW] Message error occurred - data could not be deserialized
[SW] Driver.onMessageError(origin: https://myapp.com)

e) Modern Service Workers: Angular Adds ES Module Support — PR

Angular Service Workers now support ES modules with a new type configuration option, bringing modern JavaScript features like import and export to your Service Worker scripts.

// Enable ES modules in Service Workers
export const appConfig: ApplicationConfig = {
  providers: [
    provideServiceWorker('ngsw-worker.js', {
      enabled: !isDevMode(),
      type: 'module', // Enable ES module features
      scope: '/app',
      updateViaCache: 'imports'
    }),
  ],
};

f) Service Worker Error Handling: unhandled promise rejections — PR

Unhandled promise rejections happen when a Promise fails but there’s no .catch() block or error handling to deal with it:

// Before: Silent failure scenario
class DataSyncService {
  syncUserPreferences() {
    // This could fail silently if the API is down
    fetch('/api/sync-preferences', {
      method: 'POST',
      body: JSON.stringify(this.preferences)
    })
    .then(response => response.json())
    .then(result => {
      // Update local cache
      this.updateLocalCache(result);
    });
    // No .catch() - failures would be silent!
  }
}

// After: With the new logging, you'd see in DevTools:
// "Unhandled promise rejection occurred: NetworkError: Failed to fetch"

5. 🔥 @angular/compiler-cli

a) Smart Template Diagnostics: Catch Function Reference Mistakes Before Runtime — PR

How many times have you written something like this and wondered why it displays [Function] instead of the actual value?

@Component({
  template: `<p>Welcome {{ getUserName }}</p>` // Missing parentheses!
})
class WelcomeComponent {
  getUserName(): string {
    return 'Sarah';
  }
}

The Solution: NG8117 Diagnostic

Angular now automatically detects this pattern and shows a clear warning:

❌ Before: Silent runtime behavior displaying [Function]

✅ Now: Compile-time warning with diagnostic NG8117

6. 🔥 @angular/animations

Say goodbye to @angular/animations (60KB bundle impact) and hello to native CSS animations with animate.enter and animate.leave in Angular 20.2!

Read more here.

7. 🔥 @angular/forms

a) FormArray Gets Efficient Multi-Control Push Support — PR

Previously, adding multiple controls to a FormArray required individual push() calls, each triggering change detection and validation events:

// Old approach - inefficient for large datasets
const formArray = new FormArray([]);
const newControls = [
  new FormControl('value1'),
  new FormControl('value2'),
  new FormControl('value3'),
  // ... potentially hundreds more
];

// Each push triggers valueChanges, statusChanges, and validation
newControls.forEach(control => {
  formArray.push(control); // Triggers events every time!
});

The push() method now supports both individual and batch operations:

// Before: Only single controls
push(control: TControl, options?: { emitEvent?: boolean }): void

// After: Single controls OR arrays of controls  
push(control: TControl | Array<TControl>, options?: { emitEvent?: boolean }): void

8. 🔥 @angular/router

a) Router Goes Reactive: New currentNavigation Signal Replaces Deprecated Method - PR

// Old approach - complex and inefficient
export class App {
  private router = inject(Router);

  // Required complex Observable setup
  isNavigating = toSignal(this.router.events.pipe(
    map(() => !!this.router.getCurrentNavigation()) // Deprecated method
  ));

  // Manual state checking
  checkNavigationState() {
    const nav = this.router.getCurrentNavigation(); // Deprecated
    return nav ? 'Navigating...' : 'Idle';
  }
}

// New approach - simple and reactive
export class App {
  private router = inject(Router);

  // Clean, reactive navigation state
  isNavigating = computed(() => !!this.router.currentNavigation());

  // Derive any navigation state reactively
  navigationState = computed(() => {
    const nav = this.router.currentNavigation();
    return nav ? 'Navigating...' : 'Idle';
  });
}

9. 🔥 @angular/platform-browser

a) Warns About Hydration and Blocking Navigation Conflicts — PR

When building Angular applications with server-side rendering (SSR), developers sometimes unknowingly combine features that don’t work well together:

// This configuration causes subtle runtime issues
bootstrapApplication(AppComponent, {
  providers: [
    provideClientHydration(), // Enable hydration
    provideRouter(routes, 
      withEnabledBlockingInitialNavigation() // Enable blocking navigation
    )
  ]
});

// Console output:
// ⚠️ Configuration error: found both hydration and enabledBlocking initial navigation 
// in the same application, which is a contradiction.

b) Angular Introduces IsolatedShadowDom Encapsulation - PR

Ever tried building a reusable component only to have it break when used in different projects?:

// Your beautiful blue button component
@Component({
  template: '<button class="btn">Click me</button>',
  styles: ['.btn { background: blue; color: white; }'],
  encapsulation: ViewEncapsulation.ShadowDom
})

/* Their global styles */
.btn { background: red !important; }

Your blue button becomes red! 😱 Shadow DOM was supposed to prevent this, but Angular’s implementation leaked. The solution:

// Now with TRUE isolation
@Component({
  template: '<button class="btn">Click me</button>',
  styles: ['.btn { background: blue; color: white; }'],
  encapsulation: ViewEncapsulation.IsolatedShadowDom // 🎉
})

// Result: Your button stays blue EVERYWHERE! 💙

When to Choose Each Mode

Use ViewEncapsulation.ShadowDom when:

  • You need backwards compatibility with existing applications
  • You want some global styles to be available in your component
  • You’re gradually migrating to Shadow DOM

Use ViewEncapsulation.IsolatedShadowDom when:

  • Building reusable component libraries
  • Creating embeddable widgets for third-party sites
  • Need guaranteed style isolation
  • Following web standards precisely
  • Building micro-frontends that must be completely isolated

10. 🔥 @angular/cli

a) New Angular MCP features

Angular CLI just added powerful AI integration capabilities through MCP (Model Context Protocol) server functionality.

Read more here.

Thanks for reading so far 🙏

I’d like to have your feedback, so please leave a comment , clap or follow. 👏

Spread the Angular love! 💜

If you liked it, share it among your community, tech bros and whoever you want! 🚀👥

Don’t forget to follow me and stay updated: 📱

Thanks for being part of this Angular journey! 👋😁

Originally published at https://www.codigotipado.com.

Top comments (0)