DEV Community

Amos Isaila
Amos Isaila

Posted on • Originally published at codigotipado.com on

Angular 20: What’s new

Angular 20 represents a significant milestone in the framework’s evolution, introducing groundbreaking features that enhance developer experience, performance, and application architecture.


Angular 20

🔥 @angular/core

1. New features for Dynamically-Created Components (#60137)

  • Input binding
  • Support listening to outputs
  • Two-way bindings
  • Ability to apply directives
import {createComponent, signal, inputBinding, outputBinding} from '@angular/core';

const canClose = signal(false);

// Create MyDialog
createComponent(MyDialog, {
  bindings: [
    // Bind a signal to the `canClose` input.
    inputBinding('canClose', canClose),

    // Listen for the `onClose` event specifically on the dialog.
    outputBinding<Result>('onClose', result => console.log(result)),
  ],
  directives: [
    // Apply the `FocusTrap` directive to `MyDialog` without any bindings.
    FocusTrap,

    // Apply the `HasColor` directive to `MyDialog` and bind the `red` value to its `color` input.
    // The callback to `inputBinding` is invoked on each change detection.
    {
      type: HasColor,
      bindings: [inputBinding('color', () => 'red')]
    }
  ]
});
Enter fullscreen mode Exit fullscreen mode

2. Enhanced Error Handling: provideBrowserGlobalErrorListeners() (#60704)

Previously, certain types of errors could slip through Angular’s error handling system:

  • Zone.js applications : errors occurring outside the Angular Zone wouldn’t be caught by the framework
  • Zoneless applications : uncaught errors not explicitly handled by the framework could go unnoticed
  • Promise rejections : unhandled promise rejections at the window level weren’t forwarded to your error handler

These “escaped” errors would only appear in the browser console, making them harder to track and debug in production applications.

How It Works

The new provider installs global event listeners on the browser window for two critical error events:

  • unhandledrejection - Catches unhandled promise rejections
  • error - Catches uncaught JavaScript errors

When these events occur, they’re automatically forwarded to your application’s ErrorHandler, ensuring consistent error reporting across your entire application.

Implementation

Adding global error handling to your application is straightforward:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideBrowserGlobalErrorListeners } from '@angular/core';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [
    provideBrowserGlobalErrorListeners(),
    // ... other providers
  ]
});
Enter fullscreen mode Exit fullscreen mode

3. TypeScript versions less than 5.8 are no longer supported. (#60197)

4. New TestBed.tick() method (#60993)

The TestBed.tick() method is designed to mirror the behaviour of ApplicationRef.tick() in your unit tests. It synchronises the application state with the DOM, ensuring that all pending changes are processed and reflected in your test environment.

Previously, Angular testing relied on TestBed.flushEffects() to handle pending effects in tests. However, this method had limitations:

  • Limited scope : only handled impacts, not the complete synchronisation cycle
  • Inconsistent behaviour : didn’t match the production application’s synchronisation logic
  • Confusing naming : the method name didn’t indicate its broader impact on the test state
// Before (Angular 19 and earlier)
TestBed.flushEffects(); // Only flushes effects
fixture.detectChanges(); // Separate step for change detection

// After (Angular 20)
TestBed.tick(); // Handles effects, change detection, and DOM sync
Enter fullscreen mode Exit fullscreen mode

⚠️ BREAKING CHANGE : TestBed.flushEffects() has been removed in Angular 20. All existing usages must be updated to use TestBed.tick().

5. New Stable APIs

  • toObservable()
  • effect()
  • linkedSignal()
  • toSignal()
  • incremental hydration api
  • withI18nSupport()
  • Enables support for hydrating i18n blocks
  • afterRender() ➜ renamed to afterEveryRender () ⚠️
    -
    Learn more about afterEveryRender() here

6. Enhanced Change Detection Debugging: provideCheckNoChangesConfig() (#60906)

This provider helps developers catch subtle bugs related to change detection by periodically verifying that no expressions have changed after they were checked. It’s particularly valuable for:

  • Zoneless applications : ensuring state changes are properly detected
  • OnPush component debugging : surfacing hidden errors in optimised components
  • Unidirectional data flow validation : catching side effects that violate Angular’s change detection model
// Before (Angular 19 - Experimental)
import { provideExperimentalCheckNoChangesForDebug } from '@angular/core';

// After (Angular 20 - Developer Preview)
import { provideCheckNoChangesConfig } from '@angular/core';

// Exhaustive checking (recommended for thorough debugging)
provideCheckNoChangesConfig({
  exhaustive: true,
  interval: 1000 // Check every second
})

// Non-exhaustive checking (lighter performance impact)
provideCheckNoChangesConfig({
  exhaustive: false
})
Enter fullscreen mode Exit fullscreen mode

The useNgZoneOnStable option has been removed as it wasn't found to be generally more helpful than the interval approach:

// ❌ No longer available in Angular 20
provideExperimentalCheckNoChangesForDebug({
  useNgZoneOnStable: true // This option is removed
})

// ✅ Use interval-based checking instead
provideCheckNoChangesConfig({
  exhaustive: true,
  interval: 1000
})
Enter fullscreen mode Exit fullscreen mode

Understanding the Configuration Options

Non-Exhaustive Mode (exhaustive: false)

provideCheckNoChangesConfig({ exhaustive: false })
Enter fullscreen mode Exit fullscreen mode
  • Behaviour : only checks components marked for change detection
  • Performance : lighter impact, similar to production behaviour
  • Use case : general debugging without deep inspection of OnPush components

Exhaustive Mode (exhaustive: true)

provideCheckNoChangesConfig({ 
  exhaustive: true,
  interval: 2000 // Optional: periodic checking
})
Enter fullscreen mode Exit fullscreen mode
  • Behaviour : Treats ALL components as if they use ChangeDetectionStrategy.Default
  • Coverage : Checks all views attached to ApplicationRef and their descendants
  • Benefits : Surfaces errors hidden by OnPush optimisation
  • Use case : Thorough debugging, especially for OnPush component issues

Practical Usage Examples

Basic Setup for Zoneless Applications

import { bootstrapApplication } from '@angular/platform-browser';
import { provideCheckNoChangesConfig } from '@angular/core';

bootstrapApplication(AppComponent, {
  providers: [
    provideCheckNoChangesConfig({
      exhaustive: true,
      interval: 5000 // Check every 5 seconds
    }),
    // ... other providers
  ]
});
Enter fullscreen mode Exit fullscreen mode

Debugging OnPush Component Issues

@Component({
  selector: 'app-problematic',
  template: `<div>{{ computedValue }}</div>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProblematicComponent {
  private _counter = 0;

  get computedValue() {
    // ⚠️ This violates unidirectional data flow
    return ++this._counter;
  }
}

// Configuration to catch this issue
provideCheckNoChangesConfig({
  exhaustive: true // Will detect the violation in OnPush components
})
Enter fullscreen mode Exit fullscreen mode

Development vs Production Configuration

// app.config.ts
import { isDevMode } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    // Only enable in development
    ...(isDevMode() ? [
      provideCheckNoChangesConfig({
        exhaustive: true,
        interval: 3000
      })
    ] : []),
    // ... other providers
  ]
};
Enter fullscreen mode Exit fullscreen mode

7. DOCUMENT Token Moves to Core (#60663)

Previously, the DOCUMENT token was located in @angular/common, which created several issues:

  • Unnecessary dependency : applications needed to install @angular/common to access the document
  • SSR complications : Server-side rendering scenarios often only need document access without the full common package
  • Architectural inconsistency : a fundamental browser API token was placed in a package focused on common directives and pipes

The DOCUMENT token now lives in @angular/core where it belongs, alongside other fundamental injection tokens and platform abstractions.

// Before (Angular 19 and earlier)
import { DOCUMENT } from '@angular/common';
import { inject } from '@angular/core';

// After (Angular 20 - recommended)
import { DOCUMENT, inject } from '@angular/core';
Enter fullscreen mode Exit fullscreen mode

8. Zoneless Change Detection: From Experimental to Developer Preview (#60748)

bootstrapApplication(AppComponent, {
  providers: [
    provideZonelessChangeDetection(),
    // ... other providers
  ]
});
Enter fullscreen mode Exit fullscreen mode

9. InjectFlags Removal (#60318)

InjectFlags controlled how and where Angular's dependency injection system would search for dependencies. They were essentially configuration options that modified the injection behaviour.

// Only search up to the host component
const service = inject(MyService, InjectFlags.Host);
Enter fullscreen mode Exit fullscreen mode

Angular 20 removes the deprecated InjectFlags enum, completing the transition to a more modern, type-safe dependency injection API using object literals.

// ❌ No longer available in Angular 20
import { inject, InjectFlags } from '@angular/core';

// Before (deprecated approach)
const service = inject(MyService, InjectFlags.Optional | InjectFlags.Host);

// ✅ Modern approach (Angular 20)
import { inject } from '@angular/core';

const service = inject(MyService, { optional: true, host: true });
Enter fullscreen mode Exit fullscreen mode

Affected APIs

All public injection APIs no longer accept InjectFlags:

  • inject() function
  • Injector.get() method
  • EnvironmentInjector.get() method
  • TestBed.get() method
  • TestBed.inject() method

10. Complete Removal of TestBed.get() (#60414)

// TestBed.get() - Not type safe
const service = TestBed.get(MyService); // Returns 'any'
service.someMethod(); // No TypeScript checking, runtime errors possible

// TestBed.inject() - Fully type safe
const service = TestBed.inject(MyService); // Returns MyService
service.someMethod(); // TypeScript validates this exists
Enter fullscreen mode Exit fullscreen mode

11. Task Management Made Stable: PendingTasks Injectable (#60716)

PendingTasks is an Angular service that allows you to track ongoing asynchronous operations in your application. It's particularly valuable for:

  • Server-Side Rendering : Ensuring all async operations complete before serialization
  • Testing : Waiting for all pending operations to finish
  • Application State Management : Tracking when your app is “idle”
  • Performance Monitoring : Understanding async operation lifecycle

What’s Now Stable

import { PendingTasks } from '@angular/core';

@Component({...})
export class MyComponent {
  private pendingTasks = inject(PendingTasks);

  // ✅ Stable API
  trackAsyncOperation() {
    const removeTask = this.pendingTasks.add();

    this.performAsyncWork().finally(() => {
      removeTask(); // Clean up when done
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

What Remains in Developer Preview

// ⚠️ Still in developer preview
this.pendingTasks.run(async () => {
  // Async work here
  await this.performAsyncWork();
});
Enter fullscreen mode Exit fullscreen mode

The run() method remains in developer preview due to ongoing questions about:

  • Return value handling
  • Error handling strategies
  • Potential replacement with a more context-aware task API

12. Node.js Version Support Update (#60545)

Angular 20 updates its Node.js version requirements, dropping support for Node.js v18 (which reaches End-of-Life in April 2025) and establishing new minimum version requirements to ensure developers benefit from the latest Node.js features, security updates, and performance improvements.

// Angular 20
{
  "engines": {
    "node": "^20.11.1 || >=22.11.0"
  }
}
Enter fullscreen mode Exit fullscreen mode


Version Support Matrix

⚠️ You must check that your pipelines (CI/CD) work correctly with the new Node.js version.

# Install and use Node.js v20 LTS
nvm install 20
nvm use 20

# Or install Node.js v22 LTS
nvm install 22
nvm use 22

# Verify version
node --version

// package.json
{
  "name": "my-angular-app",
  "engines": {
    "node": ">=20.11.1",
    "npm": ">=10.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

13. Deprecate the ngIf/ngFor/ngSwitch structural directives (#60492)

// ❌ Deprecated in Angular 20, removal planned for Angular 22
*ngIf
*ngFor  
*ngSwitch, *ngSwitchCase, *ngSwitchDefault

// ✅ Recommended: Control Flow Blocks
@if
@for
@switch, @case, @default
Enter fullscreen mode Exit fullscreen mode

14. DOM Optimization: ng-reflect Attributes Removed by Default (#60973)

ng-reflect-* attributes were originally introduced as a debugging aid to help developers understand what values Angular was binding to component properties and directives. They appeared in the DOM during development to show the current state of data bindings.

<!-- Example of ng-reflect attributes (old behavior) -->
<div my-directive [someProperty]="currentValue">
  <!-- Angular would add: -->
  <!-- ng-reflect-some-property="current-value" -->
</div>

<ng-template [ngIf]="showContent">
  <!-- Angular would add: -->
  <!-- bindings={ "ng-reflect-ng-if": "true" } -->
</ng-template>
<!-- Before: Complex components created verbose DOM -->
<my-component 
  [data]="complexObject"
  [config]="configuration"
  [options]="userOptions">

  <!-- Multiple ng-reflect attributes made DOM hard to read -->
  <!-- ng-reflect-data="[object Object]" -->
  <!-- ng-reflect-config="[object Object]" -->
  <!-- ng-reflect-options="[object Object]" -->
</my-component>
<!-- After: Clean, readable DOM -->
<my-component 
  [data]="complexObject"
  [config]="configuration"
  [options]="userOptions">
  <!-- Clean HTML for better developer experience -->
</my-component>
Enter fullscreen mode Exit fullscreen mode

If you want to restore the behaviour use provideNgReflectAttributes():

// To restore ng-reflect behavior (development only)
import { bootstrapApplication } from '@angular/platform-browser';
import { provideNgReflectAttributes } from '@angular/core';

bootstrapApplication(AppComponent, {
  providers: [
    provideNgReflectAttributes(), // Enables ng-reflect in dev mode
    // ... other providers
  ]
});
Enter fullscreen mode Exit fullscreen mode

15. Chrome DevTools Performance Integration (#60789)

Angular 20 adds the ng.enableProfiling() global utility that exposes Angular's internal performance data to Chrome DevTools, creating a dedicated Angular track in the Performance timeline alongside other browser metrics.

Unified Performance Analysis

  • Single Tool : No more switching between Angular DevTools and Chrome DevTools
  • Correlated Data : See Angular events in context with other browser performance metrics
  • Production Debugging : Works with minified production code

Angular-Specific Insights

  • Component rendering lifecycle
  • Change detection cycles
  • Event listener execution
  • Component instantiation
  • Provider instantiation
  • Dependency injection profiling

Visual Indicators

  • Color-coded entries to distinguish between:
  • Developer-authored TypeScript code
  • Angular compiler-generated code
  • Dedicated Angular track at the bottom of the performance timeline

🔥 @angular/common

1. NgTemplateOutlet Accepts Undefined Inputs (#61404)

Angular 20 improves the NgTemplateOutlet directive by accepting undefined inputs alongside null, addressing a long-standing TypeScript compatibility issue and making the directive more ergonomic to use with modern Angular patterns like signals and ViewChild.

// Before (Angular 19 and earlier)
export class NgTemplateOutlet<C = unknown> {
  @Input() ngTemplateOutlet: TemplateRef<C> | null = null;
  @Input() ngTemplateOutletContext: C | null = null;
  @Input() ngTemplateOutletInjector: Injector | null = null;
}

// After (Angular 20)
export class NgTemplateOutlet<C = unknown> {
  @Input() ngTemplateOutlet: TemplateRef<C> | null | undefined = null;
  @Input() ngTemplateOutletContext: C | null | undefined = null;
  @Input() ngTemplateOutletInjector: Injector | null | undefined = null;
}

// undefined: represents non-existence, state of being unset
let template: TemplateRef | undefined; // Not initialized
if (template) { /* use template */ } // Natural check
// null: represents an explicit value of "nothingness"
let template: TemplateRef | null = null; // Explicitly set to null
if (template !== null) { /* use template */ } // Explicit null check needed

// Before: Required nullish coalescing or type assertion
@Component({
  template: `
    <ng-template #myTemplate>Content</ng-template>
    <ng-container [ngTemplateOutlet]="template ?? null"></ng-container>
  `
})
export class MyComponent {
  @ViewChild('myTemplate') template?: TemplateRef<any>;

  // Had to use ?? null to satisfy type checker
  get templateOrNull() {
    return this.template ?? null;
  }
}

// After: Direct usage without type gymnastics
@Component({
  template: `
    <ng-template #myTemplate>Content</ng-template>
    <ng-container [ngTemplateOutlet]="template"></ng-container>
  `
})
export class MyComponent {
  @ViewChild('myTemplate') template?: TemplateRef<any>;

  // Works directly - no type conversion needed!
}
Enter fullscreen mode Exit fullscreen mode

2. ViewportScroller with ScrollOptions Support (#61002)

// Before (Angular 19 and earlier)
abstract class ViewportScroller {
  abstract scrollToPosition(position: [number, number]): void;
  abstract scrollToAnchor(anchor: string): void;
}

// After (Angular 20)
abstract class ViewportScroller {
  abstract scrollToPosition(position: [number, number], options?: ScrollOptions): void;
  abstract scrollToAnchor(anchor: string, options?: ScrollOptions): void;
}
Enter fullscreen mode Exit fullscreen mode

ScrollOptions Interface

interface ScrollOptions {
  behavior?: 'auto' | 'instant' | 'smooth';
  block?: 'start' | 'center' | 'end' | 'nearest';
  inline?: 'start' | 'center' | 'end' | 'nearest';
}

@Component({
  selector: 'app-landing-page',
  template: `
    <nav class="fixed-header">
      <a (click)="navigateToSection('hero')">Home</a>
      <a (click)="navigateToSection('about')">About</a>
      <a (click)="navigateToSection('services')">Services</a>
      <a (click)="navigateToSection('contact')">Contact</a>
    </nav>

    <section id="hero" class="hero-section">...</section>
    <section id="about" class="about-section">...</section>
    <section id="services" class="services-section">...</section>
    <section id="contact" class="contact-section">...</section>
  `
})
export class LandingPageComponent {
  private viewportScroller = inject(ViewportScroller);
  navigateToSection(sectionId: string) {
    this.viewportScroller.scrollToAnchor(sectionId, {
      behavior: 'smooth',
      block: 'start' // Align to top of viewport
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Suspicious Date Pattern Validation (#59798)

Angular 20 introduces runtime validation for date formatting patterns in development mode, helping developers catch common mistakes that lead to incorrect date displays. This enhancement specifically targets the misuse of week-based year patterns that cause subtle but critical date formatting errors.

Common Mistake: Y vs y

// ❌ WRONG: Using week-based year (Y) instead of calendar year (y)
formatDate(new Date('2024-12-31'), 'YYYY-MM-dd', 'en');
// Returns: "2025-12-31" (INCORRECT!)

// ✅ CORRECT: Using calendar year (y)
formatDate(new Date('2024-12-31'), 'yyyy-MM-dd', 'en');
// Returns: "2024-12-31" (CORRECT!)
Enter fullscreen mode Exit fullscreen mode

Why This Happens

Week-based year (Y) differs from calendar year (y) for a few days around January 1st:

// Week-based year examples (using Y)
formatDate('2024-01-01', 'YYYY', 'en'); // Returns "2024" ✓
formatDate('2024-12-30', 'YYYY', 'en'); // Returns "2025" ❌ Wrong!
formatDate('2024-12-31', 'YYYY', 'en'); // Returns "2025" ❌ Wrong!

// Calendar year examples (using y)
formatDate('2024-01-01', 'yyyy', 'en'); // Returns "2024" ✓
formatDate('2024-12-30', 'yyyy', 'en'); // Returns "2024" ✓
formatDate('2024-12-31', 'yyyy', 'en'); // Returns "2024" ✓
Enter fullscreen mode Exit fullscreen mode

Development Mode Error Detection

// Angular 20 now throws errors for suspicious patterns in development
import { formatDate } from '@angular/common';

// ❌ This will throw an error in development mode
try {
  formatDate(new Date(), 'YYYY/MM/dd', 'en');
} catch (error) {
  console.error(error.message);
  // "Suspicious use of week-based year "Y" in date pattern "YYYY/MM/dd". 
  // Did you mean to use calendar year "y" instead?"
}
// ✅ This works correctly
formatDate(new Date(), 'yyyy/MM/dd', 'en'); // No error
Enter fullscreen mode Exit fullscreen mode

When Validation Triggers

Angular validates patterns and throws errors for:

  1. Week-based year without week indicator : Using Y without w
  2. Mixed date components : Using day-of-year D with month M
// ❌ Will throw error - week-based year without week
formatDate(date, 'YYYY-MM-dd', 'en');

// ✅ Valid - week-based year WITH week indicator
formatDate(date, `YYYY 'W'ww`, 'en'); // "2024 W52"

// ✅ Valid - calendar year (most common use case)
formatDate(date, 'yyyy-MM-dd', 'en'); // "2024-12-31"
Enter fullscreen mode Exit fullscreen mode

Practical Example

@Component({
  selector: 'app-date-display',
  template: `
    <div class="date-info">
      <!-- ❌ Before: Potential bug with Y -->
      <!-- <p>Date: {{ currentDate | date:'YYYY-MM-dd' }}</p> -->

      <!-- ✅ After: Correct calendar year -->
      <p>Date: {{ currentDate | date:'yyyy-MM-dd' }}</p>

      <!-- ✅ Valid: Week-based year with week -->
      <p>Week: {{ currentDate | date:`YYYY 'W'ww` }}</p>
    </div>
  `
})
export class DateDisplayComponent {
  currentDate = new Date();

  // ❌ This method would throw in development
  // getFormattedDate() {
  // return formatDate(this.currentDate, 'YYYY-MM-dd', 'en');
  // }

  // ✅ Correct implementation
  getFormattedDate() {
    return formatDate(this.currentDate, 'yyyy-MM-dd', 'en');
  }
}
Enter fullscreen mode Exit fullscreen mode

🔥 @angular/compiler

1. @for Track Function Diagnostics (#60495)

Angular 20 introduces a new extended diagnostic that warns developers when track functions in @for loops are not properly invoked. This compile-time validation helps prevent performance issues when track functions are referenced but not called, causing unnecessary DOM recreation.

@Component({
  template: `
    <!-- ❌ WRONG: Track function not invoked -->
    @for (item of items; track trackByName) {
      <div>{{ item.name }}</div>
    }
  `
})
export class ListComponent {
  items = [{ name: 'Alice' }, { name: 'Bob' }];

  trackByName(item: any) {
    return item.name; // This function is never called!
  }
}
Enter fullscreen mode Exit fullscreen mode

Compile-Time Warning

// Angular 20 now shows a warning during compilation:
// Error: The track function in the @for block should be invoked: trackByName(/* arguments */)

@Component({
  template: `
    <!-- ❌ This triggers the diagnostic -->
    @for (item of items; track trackByName) {
      <div>{{ item.name }}</div>
    }
  `
})
export class DiagnosticExample {
  trackByName(item: any) {
    return item.name;
  }
}
// Error Code: 8115 - UNINVOKED_TRACK_FUNCTION
// Extended Diagnostic Name: uninvokedTrackFunction
// Category: Warning
// Message: "The track function in the @for block should be invoked: trackByName(/* arguments */)"
Enter fullscreen mode Exit fullscreen mode

2. Void and Exponentiation Operators Support in Templates (#59894)

When a method returns false in an event handler, it can unintentionally prevent the default event behavior (similar to calling preventDefault()). The void operator ensures the expression always returns undefined, avoiding this issue.

Examples:

In Host Bindings:

@Directive({
  selector: '[trackClicks]',
  host: { 
    '(mousedown)': 'void handleMousedown($event)',
    '(click)': 'void logClick($event)'
  }
})
export class ClickTrackerDirective {
  handleMousedown(event: MouseEvent) {
    console.log('Mouse down tracked');
    return false; // Won't prevent default behavior due to void
  }

  logClick(event: MouseEvent) {
    console.log('Click tracked');
    // Any return value is ignored
  }
}
Enter fullscreen mode Exit fullscreen mode

In Template Event Bindings:

@Component({
  template: `
    <button (click)="void saveData()">Save</button>
    <form (submit)="void handleSubmit($event)">
      <!-- form content -->
    </form>
  `
})
export class MyComponent {
  saveData() {
    // Save logic here
    return false; // Won't affect event propagation
  }

  handleSubmit(event: Event) {
    // Handle form submission
    console.log('Form submitted');
  }
}
Enter fullscreen mode Exit fullscreen mode

Angular 20 adds support for the exponentiation operator (**) in template expressions, bringing mathematical operations in templates closer to standard JavaScript.

Examples:

Basic Mathematical Calculations:

@Component({
  template: `
    <div>
      <p>2 to the power of 3: {{2 ** 3}}</p>
      <p>Base squared: {{base ** 2}}</p>
      <p>Scientific notation: {{10 ** -3}}</p>
      <p>Complex calculation: {{(value + 1) ** exponent}}</p>
    </div>
  `
})
export class MathComponent {
  base = 5;
  value = 4;
  exponent = 3;
}
Enter fullscreen mode Exit fullscreen mode

3. Tagged Template Literals Support (#59947)

Tagged template literals allow you to parse template literals with a function. The tag function receives the string parts and interpolated values separately, giving you complete control over how the final string is constructed.

@Component({
  template: `
    <div>No interpolations: {{ tag\`hello world\` }}</div>
    <span>With interpolations: {{ greet\`Hello \${name}, it's \${timeOfDay}!\` }}</span>
    <p>With pipe: {{ format\`Welcome \${username}\` | uppercase }}</p>
  `
})
export class MyComponent {
  name = 'Alice';
  timeOfDay = 'morning';
  username = 'developer';

  // Simple tag function
  tag = (strings: TemplateStringsArray, ...args: any[]) => {
    return strings.join('') + ' (processed)';
  };
  // Tag function with interpolation processing
  greet = (strings: TemplateStringsArray, name: string, time: string) => {
    return `${strings[0]}${name.toUpperCase()}${strings[1]}${time}${strings[2]}`;
  };
  // Formatting tag function
  format = (strings: TemplateStringsArray, ...args: string[]) => {
    return strings.reduce((result, string, i) => {
      return result + string + (args[i] ? `**${args[i]}**` : '');
    }, '');
  };
}
Enter fullscreen mode Exit fullscreen mode

4. Support the in keyword in Binary expression (#58432)

The in operator returns true if a specified property exists in an object or its prototype chain. It's a fundamental JavaScript operator that's now available in Angular templates with full type safety.

@Component({
  template: `
    <div>{{ 'name' in user ? 'Has name' : 'No name' }}</div>
    <div>{{ 'email' in user ? user.email : 'No email provided' }}</div>
    <div>{{ 'admin' in permissions ? 'Admin user' : 'Regular user' }}</div>
  `
})
export class UserProfileComponent {
  user = {
    name: 'Alice',
    email: 'alice@example.com'
  };

  permissions = {
    read: true,
    write: true,
    admin: false
  };
}
Enter fullscreen mode Exit fullscreen mode

🔥 @angular/compiler-cli

1. Enhanced Template Expression Diagnostics: Unparenthesized Nullish Coalescing (#60279)

Angular 20 introduces a new extended diagnostic (NG8114) that helps developers write more robust template expressions by detecting potentially ambiguous operator precedence when mixing nullish coalescing (??) with logical operators (&& and ||).

What is the Unparenthesized Nullish Coalescing Diagnostic?

This diagnostic identifies cases where the nullish coalescing operator (??) is used alongside logical AND (&&) or logical OR (||) operators without parentheses to clarify precedence. This pattern can lead to confusion and unexpected behavior, as the operator precedence may not be immediately obvious to developers.

Why This Matters

In JavaScript and TypeScript, mixing these operators without parentheses is considered an error because it creates ambiguous expressions. Angular templates have historically allowed this pattern, but it can lead to bugs and maintenance issues.

@Component({
  template: `
    <!-- Ambiguous: Is it (hasPermission() && task()?.disabled) ?? true 
         or hasPermission() && (task()?.disabled ?? true)? -->
    <button [disabled]="hasPermission() && task()?.disabled ?? true">
      Run Task
    </button>

    <!-- Another ambiguous case -->
    <div>{{ name || user?.name ?? 'Anonymous' }}</div>
  `
})
export class ProblematicComponent {
  hasPermission = input(false);
  task = input<Task | undefined>(undefined);
  name = '';
  user = input<User | null>(null);
}
Enter fullscreen mode Exit fullscreen mode

The Solution: Clear Parentheses

Always use parentheses to explicitly define the intended order of operations:

@Component({
  template: `
    <!-- ❌ Ambiguous precedence -->
    <div class="error" *ngIf="form.invalid && field.touched ?? showAllErrors">
      Field is required
    </div>

    <!-- ✅ Clear intent: Show error if form invalid AND (field touched OR show all) -->
    <div class="error" *ngIf="form.invalid && (field.touched ?? showAllErrors)">
      Field is required
    </div>

    <!-- ✅ Alternative: Show error if (form invalid AND field touched) OR show all -->
    <div class="error" *ngIf="(form.invalid && field.touched) ?? showAllErrors">
      Field is required (alternative logic)
    </div>
  `
})
export class FormValidationComponent {
  form = inject(FormBuilder).group({
    field: ['', Validators.required]
  });

  get field() { return this.form.get('field')!; }
  showAllErrors = signal(false);
}
Enter fullscreen mode Exit fullscreen mode

2. Enhanced Template Diagnostics: Missing Structural Directive Detection (#59443)

Angular 20 introduces a new extended diagnostic (NG8116) that helps developers identify missing imports for custom structural directives in standalone components. This diagnostic prevents runtime errors and improves the developer experience when working with custom structural directives.

What is the Missing Structural Directive Diagnostic?

This diagnostic detects when a standalone component uses custom structural directives (like *select, *featureFlag, or *permission) in its template without importing the corresponding directive. This helps catch import oversights that would otherwise cause runtime failures.

@Component({
  // ✅ Proper imports for custom structural directives
  imports: [SelectDirective, FeatureFlagDirective],
  template: `
    <div *select="let item from items">
      {{ item.name }}
    </div>

    <section *featureFlag="'newDashboard'">
      <new-dashboard />
    </section>
  `
})
export class MyComponent {
  items = [{ name: 'Item 1' }, { name: 'Item 2' }];
}
Enter fullscreen mode Exit fullscreen mode

3. Enhanced Type Checking for Host Bindings (#60267)

Previously, Angular’s type checking was limited to component templates. Now, host bindings in directives and components receive full type checking support, including:

  • Host object literals in @Component and @Directive decorators
  • @HostBinding decorator expressions
  • @hostlistener decorator expressions
  • IDE integration with hover information, autocomplete, and renaming support

This feature is controlled by the typeCheckHostBindings compiler flag:

{
  "angularCompilerOptions": {
    "strictTemplates": true,
    "typeCheckHostBindings": true
  }
}
Enter fullscreen mode Exit fullscreen mode

🔥 @angular/forms

1. Enhanced Form Control Management: markAllAsDirty Method (#58663)

The markAllAsDirty method recursively marks a form control and all of its child controls as dirty. This is particularly useful when you need to trigger validation display for an entire form or form section at once.

import { Component } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';

@Component({
  template: `
    <input [formControl]="emailControl" placeholder="Email">
    <div *ngIf="emailControl.dirty && emailControl.invalid" class="error">
      Email is required and must be valid
    </div>
    <button (click)="markDirty()">Mark as Dirty</button>
    <button (click)="reset()">Reset</button>

    <div class="status">
      <p>Dirty: {{ emailControl.dirty }}</p>
      <p>Valid: {{ emailControl.valid }}</p>
    </div>
  `
})
export class FormControlExample {
  emailControl = new FormControl('', [
    Validators.required,
    Validators.email
  ]);

  markDirty(): void {
    this.emailControl.markAllAsDirty();
  }

  reset(): void {
    this.emailControl.reset();
  }
}
Enter fullscreen mode Exit fullscreen mode

Comparison with Existing Methods:

2. Enhanced Form Reset Controls: Silent Reset Option (#60354)

The resetForm method in FormGroupDirective now accepts an optional parameter that gets passed to the underlying FormGroup.reset() method, allowing you to control event emission during reset operations.

The Problem

Previously, resetting a form would always trigger change events, which could cause unwanted side effects:

@Component({
  template: `
    <form #formDir="ngForm" [formGroup]="userForm">
      <input formControlName="name" placeholder="Name">
      <input formControlName="email" placeholder="Email">
      <button type="submit">Submit</button>
      <button type="button" (click)="resetForm(formDir)">Reset</button>
    </form>

    <div class="debug">
      <p>Value changes count: {{ valueChangesCount }}</p>
      <p>Form submitted: {{ formDir.submitted }}</p>
    </div>
  `
})
export class ProblematicComponent {
  userForm: FormGroup;
  valueChangesCount = 0;

  constructor(private fb: FormBuilder) {
    this.userForm = this.fb.group({
      name: [''],
      email: ['']
    });

    // This subscription would fire unnecessarily during reset
    this.userForm.valueChanges.subscribe(() => {
      this.valueChangesCount++;
      // Expensive operations triggered on every change
      this.performExpensiveCalculation();
    });
  }

  resetForm(formDir: FormGroupDirective): void {
    // ❌ Old behavior: Always emits events
    formDir.resetForm();
    // This would increment valueChangesCount unnecessarily
  }

  private performExpensiveCalculation(): void {
    // Some expensive operation that shouldn't run during reset
    console.log('Expensive calculation triggered');
  }
}
Enter fullscreen mode Exit fullscreen mode

The Solution

Now you can reset forms without triggering change events:

resetFormSilent(formDir: FormGroupDirective): void {
  // ✅ New behavior: Reset without emitting events
  formDir.resetForm(undefined, { emitEvent: false });
  // valueChangesCount won't increment
}
Enter fullscreen mode Exit fullscreen mode

🔥 @angular/http

1. HTTP Client Keep-Alive Support for Fetch Requests (#60621)

The keepalive option, when set to true, instructs the browser to keep the request alive even if the page that initiated it is unloaded. This is particularly useful for sending final analytics data, logging events, or performing cleanup operations when users navigate away from or close a page.

Keep-alive support requires using the Fetch API backend. Make sure to configure your application with withFetch():

import { provideHttpClient, withFetch } from '@angular/common/http';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withFetch()), // Required for keepalive support
    // other providers...
  ]
});
Enter fullscreen mode Exit fullscreen mode

Simple GET Request with Keep-Alive:

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-analytics',
  template: `
    <button (click)="sendAnalytics()">Send Analytics</button>
    <button (click)="trackPageView()">Track Page View</button>
  `
})
export class AnalyticsComponent {
  private http = inject(HttpClient);

  sendAnalytics(): void {
    // This request will persist even if the page is unloaded
    this.http.get('/api/analytics/session-end', { 
      keepalive: true 
    }).subscribe({
      next: (response) => console.log('Analytics sent:', response),
      error: (error) => console.error('Analytics failed:', error)
    });
  }

  trackPageView(): void {
    const pageData = {
      url: window.location.href,
      timestamp: Date.now(),
      userAgent: navigator.userAgent
    };

    this.http.post('/api/analytics/pageview', pageData, {
      keepalive: true,
      headers: { 'Content-Type': 'application/json' }
    }).subscribe();
  }
}
Enter fullscreen mode Exit fullscreen mode

🔥 @angular/platform-browser

1. Deprecate the platform-browser-dynamic package (#61043)

The @angular/platform-browser-dynamic package is now deprecated. All its functionality has been moved to @angular/platform-browser, providing a unified package for browser platform operations.

// ❌ Deprecated approach
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

// ✅ New recommended approach
import { platformBrowser } from '@angular/platform-browser';
import { AppModule } from './app/app.module';
platformBrowser()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));
Enter fullscreen mode Exit fullscreen mode

2. Deprecate the HammerJS integration (#60257)

What’s Being Deprecated

The following HammerJS-related APIs and providers are now deprecated:

  • HammerGestureConfig
  • HAMMER_GESTURE_CONFIG token
  • HammerModule
  • Built-in HammerJS gesture directives
  • Automatic HammerJS gesture recognition

Why This Change?

  1. Native Browser Support : Modern browsers provide comprehensive touch and gesture APIs
  2. Bundle Size Reduction : Removing HammerJS dependency reduces application bundle size
  3. Performance : Native browser events are more performant than library-based solutions
  4. Maintenance : Reduces Angular’s dependency on external libraries

🔥 @angular/platform-server

1. Platform Server Testing Entry Point Deprecation (#60915)

The following APIs from @angular/platform-server/testing are now deprecated:

  • platformServerTesting
  • ServerTestingModule
  • All related testing utilities for server-side rendering

Why This Change?

  1. Better Testing Practices : E2E tests provide more realistic SSR verification
  2. Real Environment Testing : E2E tests run in actual browser/server environments
  3. Simplified Maintenance : Reduces complexity in the platform-server package
  4. More Accurate Results : Tests actual SSR behavior rather than mocked environments
// ❌ Deprecated approach
import { TestBed } from '@angular/core/testing';
import { platformServerTesting, ServerTestingModule } from '@angular/platform-server/testing';
import { AppComponent } from './app.component';

describe('AppComponent SSR', () => {
  beforeEach(() => {
    TestBed.initTestEnvironment(
      ServerTestingModule,
      platformServerTesting()
    );

    TestBed.configureTestingModule({
      declarations: [AppComponent]
    });
  });
  it('should render on server', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toContain('Hello');
  });
});
Enter fullscreen mode Exit fullscreen mode

You can use Cypress, Playwright, Jest + Puppeteer and other configurations.

🔥 @angular/router

1. Add ability to directly abort a navigation (#60380)

The Navigation interface now includes an abort() method that allows you to cancel an ongoing navigation before it completes. This is particularly useful for scenarios where you need to cancel navigations based on user actions, external events, or application state changes.

// Aborting current navigation
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
  selector: 'app-navigation-control',
  template: `
    <div class="navigation-controls">
      <button (click)="startNavigation()">Start Navigation</button>
      <button (click)="abortNavigation()" [disabled]="!isNavigating">
        Abort Navigation
      </button>
      <p>Status: {{ navigationStatus }}</p>
    </div>
  `
})
export class NavigationControlComponent {
  private router = inject(Router);

  isNavigating = false;
  navigationStatus = 'Ready';

  async startNavigation(): Promise<void> {
    this.isNavigating = true;
    this.navigationStatus = 'Navigating...';

    try {
      const result = await this.router.navigate(['/slow-loading-page']);
      this.navigationStatus = result ? 'Navigation completed' : 'Navigation failed';
    } catch (error) {
      this.navigationStatus = 'Navigation error';
    } finally {
      this.isNavigating = false;
    }
  }

  abortNavigation(): void {
    const currentNavigation = this.router.getCurrentNavigation();
    if (currentNavigation) {
      currentNavigation.abort();
      this.navigationStatus = 'Navigation aborted';
      this.isNavigating = false;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Asynchronous Router Redirects (#60863)

The RedirectFunction type now supports returning MaybeAsync, which means redirect functions can return:

  • Synchronous values: string | UrlTree
  • Promises: Promise
  • Observables: Observable
// Synchronous Redirects (Existing)
const routes: Routes = [
  {
    path: 'old-page',
    redirectTo: '/new-page' // Static redirect
  },
  {
    path: 'user/:id',
    redirectTo: (route) => `/profile/${route.params['id']}` // Sync function
  }
];

// Asynchronous Redirects (New)
import { inject } from '@angular/core';
import { Routes } from '@angular/router';
import { UserService } from './user.service';
const routes: Routes = [
  {
    path: 'dashboard',
    redirectTo: async (route) => {
      const userService = inject(UserService);
      const user = await userService.getCurrentUser();

      if (user.isAdmin) {
        return '/admin/dashboard';
      } else {
        return '/user/dashboard';
      }
    }
  },
  {
    path: 'profile',
    redirectTo: (route) => {
      const userService = inject(UserService);

      // Return Observable
      return userService.getCurrentUser().pipe(
        map(user => user.isActive ? '/profile/active' : '/profile/inactive')
      );
    }
  }
];
Enter fullscreen mode Exit fullscreen mode

3. Allow resolvers to read resolved data from ancestors (#59860)

Previously, resolvers could only access their own resolved data. Now, child resolvers can read resolved data from any ancestor route in the route tree, enabling better data composition and reducing redundant API calls.

// Example: Accessing Parent Resolver Data
import { ActivatedRouteSnapshot } from '@angular/router';
import { inject } from '@angular/core';
const routes: Routes = [
  {
    path: 'company/:id',
    resolve: {
      company: (route: ActivatedRouteSnapshot) => {
        const dataService = inject(DataService);
        return dataService.getCompany(route.params['id']);
      }
    },
    children: [
      {
        path: 'departments',
        resolve: {
          // Child resolver can access parent's resolved data
          departments: (route: ActivatedRouteSnapshot) => {
            const dataService = inject(DataService);
            const company = route.data['company']; // Access parent's resolved company data
            return dataService.getDepartments(company.id);
          }
        },
        component: DepartmentsComponent,
        children: [
          {
            path: ':deptId/employees',
            resolve: {
              // Grandchild resolver can access both parent and grandparent data
              employees: (route: ActivatedRouteSnapshot) => {
                const dataService = inject(DataService);
                const company = route.data['company']; // From grandparent
                const departments = route.data['departments']; // From parent
                const deptId = route.params['deptId'];

                return dataService.getEmployees(company.id, deptId);
              }
            },
            component: EmployeesComponent
          }
        ]
      }
    ]
  }
];
Enter fullscreen mode Exit fullscreen mode

4. Support custom elements for RouterLink (#60290)

When RouterLink is used on a registered custom element, Angular now checks if the custom element's observedAttributes static property includes 'href'. If it does, the element is treated as a navigational element similar to native tags, with proper href updates and accessibility handling.

import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';

@Component({
  selector: 'app-navigation',
  standalone: true,
  imports: [RouterModule],
  template: `
    <nav class="custom-navigation">
      <h2>Custom Element Navigation</h2>

      <!-- Custom elements with RouterLink - href will be automatically managed -->
      <custom-link routerLink="/home" routerLinkActive="active">
        Home
      </custom-link>

      <custom-link routerLink="/products" routerLinkActive="active">
        Products
      </custom-link>

      <custom-link routerLink="/about" routerLinkActive="active">
        About
      </custom-link>

      <custom-link 
        routerLink="/contact" 
        routerLinkActive="active"
        [routerLinkActiveOptions]="{ exact: true }">
        Contact
      </custom-link>

      <!-- Disabled custom link -->
      <custom-link disabled>
        Coming Soon
      </custom-link>

      <!-- Custom link with query parameters -->
      <custom-link 
        routerLink="/search" 
        [queryParams]="{ q: 'angular', category: 'docs' }"
        routerLinkActive="active">
        Search Angular Docs
      </custom-link>
    </nav>
  `,
})
export class CustomNavigationComponent {}
Enter fullscreen mode Exit fullscreen mode

🔥 @schematics/angular

1. Global Error Listeners for Better Error Handling (PR)

Previously, errors happening outside Angular’s zone or in asynchronous operations might go unnoticed or crash your application silently. With this new provider, Angular automatically registers global error handlers to catch and manage these scenarios more gracefully.

The provider is now automatically included in new Angular applications generated through the CLI, ensuring better error handling.

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    // New global error listeners provider - catches unhandled errors
    provideBrowserGlobalErrorListeners(),
  ]
};

// app.ts
export class AppComponent {

  triggerAsyncError() {
    // This error would previously go unhandled
    setTimeout(() => {
      throw new Error('Async error caught by global listener!');
    }, 100);
  }

  triggerPromiseRejection() {
    // Unhandled promise rejection
    Promise.reject(new Error('Promise rejection caught by global listener!'));
  }

  triggerZoneError() {
    // Error outside Angular zone
    Zone.current.runOutsideAngular(() => {
      throw new Error('Zone error caught by global listener!');
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Automatic TypeScript Module Resolution Migration (PR)

The CLI now automatically migrates existing projects to use TypeScript’s 'bundler' module resolution instead of the older 'node' resolution. This change aligns with modern build tools and package managers, providing better support for ESM modules and contemporary JavaScript patterns.

The migration intelligently scans all TypeScript configuration files in your workspace and updates them appropriately, while respecting specific configurations like module: 'preserve'.

The migration is smart about when to apply changes:

  • ✅ Updates moduleResolution: 'node' to moduleResolution: 'bundler'
  • ❌ Skips files that already use 'bundler' resolution
  • ❌ Preserves module: 'preserve' configurations unchanged
  • 🔍 Scans all TypeScript configs in your workspace automatically
// BEFORE Migration - tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "node", // ❌ Old resolution strategy
  }
}

// AFTER Migration - tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "bundler", // ✅ Modern bundler resolution
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Server-Side Rendering API Modernization (PR)

Two major changes streamline SSR setup:

Import Migration :

  • ✅ Moves provideServerRendering from @angular/platform-server to @angular/ssr
  • ✅ Preserves other platform-server imports
  • ✅ Merges with existing @angular/ssr imports

API Consolidation :

  • ✅ Replaces provideServerRouting(routes) with provideServerRendering(withRoutes(routes))
  • ✅ Removes duplicate provideServerRendering() calls
  • ✅ Preserves additional arguments and configurations
// ❌ BEFORE - Angular 19 SSR Configuration
// Old location
import { provideServerRendering } from '@angular/platform-server'; 
// Separate provider 
import { provideServerRouting } from '@angular/ssr';  

export const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering(),
    provideServerRouting(routes) // Two separate calls
  ]
};
// ✅ AFTER - Angular 20 Unified SSR
// Single import
import { provideServerRendering, withRoutes } from '@angular/ssr';  
export const advancedServerConfig: ApplicationConfig = {
  providers: [
    provideServerRendering(
      withRoutes(routes),
      withAppShell(AppShellComponent), // App shell for instant loading
      withPrerendering(['/home', '/about']) // Static prerendering
    )
  ]
};
Enter fullscreen mode Exit fullscreen mode

4. File Naming Style Guide Migration (PR)

Angular 20 introduces a new file naming style guide but automatically preserves your existing project’s naming conventions when upgrading. The migration ensures backward compatibility while new projects adopt the modern naming standards.

// 📊 FILE NAMING COMPARISON TABLE

| Schematic Type | Angular 19 (Old) | Angular 20 (New) | Command Example |
|---------------|------------------|------------------|-----------------|
| Service | user.service.ts | user-service.ts | ng g service user |
| Guard | auth.guard.ts | auth-guard.ts | ng g guard auth |
| Interceptor | data.interceptor.ts | data-interceptor.ts | ng g interceptor data |
| Module | shared.module.ts | shared-module.ts | ng g module shared |
| Pipe | custom.pipe.ts | custom-pipe.ts | ng g pipe custom |
| Resolver | api.resolver.ts | api-resolver.ts | ng g resolver api |
// 🔧 MIGRATION CONFIGURATION
// angular.json - Added automatically during ng update to preserve old naming
{
  "schematics": {
    // Keep .component.ts
    "@schematics/angular:component": { "type": "component" },  
    // Keep .directive.ts     
    "@schematics/angular:directive": { "type": "directive" }, 
    // Keep .service.ts    
    "@schematics/angular:service": { "type": "service" }, 
    // Keep .guard.ts        
    "@schematics/angular:guard": { "typeSeparator": "." },  
    // Keep .interceptor.ts      
    "@schematics/angular:interceptor": { "typeSeparator": "." },  
    // Keep .module.ts
    "@schematics/angular:module": { "typeSeparator": "." },  
    // Keep .pipe.ts     
    "@schematics/angular:pipe": { "typeSeparator": "." },         
    // Keep .resolver.ts
    "@schematics/angular:resolver": { "typeSeparator": "." }      
  }
}
// 📂 PRACTICAL EXAMPLES
// 🔴 UPGRADED PROJECTS (with migration defaults):
ng generate service user-data → user-data.service.ts
ng generate guard auth → auth.guard.ts  
ng generate pipe currency-format → currency-format.pipe.ts
// 🟢 NEW PROJECTS (Angular 20 defaults):
ng generate service user-data → user-data-service.ts
ng generate guard auth → auth-guard.ts
ng generate pipe currency-format → currency-format-pipe.ts
// 🎯 COMPONENT STRUCTURE COMPARISON
ng generate component user-profile
// Upgraded projects: // New projects:
user-profile/ user-profile/
├── user-profile.component.ts ├── user-profile-component.ts
├── user-profile.component.html ├── user-profile-component.html
├── user-profile.component.css ├── user-profile-component.css
└── user-profile.component.spec.ts └── user-profile-component.spec.ts
// 🔄 MIGRATION BEHAVIOR
// ✅ Preserves existing settings if you already have custom configurations
// ✅ Only adds defaults for missing configurations
// ✅ Maintains your current project structure
// ✅ Zero breaking changes - all existing files remain unchanged
Enter fullscreen mode Exit fullscreen mode

5. Modern Build System with @angular/build (PR)

📦 New Build Package : @angular/build replaces @angular-devkit/build-angular for new projects

Smaller Install Size : ~115 MB vs ~291 MB (60% reduction)

🚀 Faster Builds : No Webpack dependencies for modern esbuild-based builds

🔄 Backward Compatible : Existing projects can still use the old builder

6. TypeScript Project References for Better IDE Support (PR)

🔗 Project References : TypeScript configs now use composite projects and references

🧠 Better IDE Support : IDEs can accurately discover types across different project areas

📁 Solution Style : Root tsconfig.json becomes a TypeScript solution file

Zero Build Impact : Angular build process remains completely unaffected

The Key Insight

TypeScript project references were designed for vanilla TypeScript , not framework projects. Here’s why:

🔍 Pure TypeScript : tsc --build works perfectly - it just compiles .ts to .js and .d.ts

🎭 Angular Reality : Needs template compilation, dependency injection metadata, AOT compilation, style processing, and bundling

Why Nx Exists

Nx realized this fundamental limitation years ago and built their own solution:

  • 🎯 Dependency Graph : Manually tracks project relationships
  • Build Orchestration : Runs builds in the correct order
  • 🎪 Framework Aware : Understands Angular’s compilation needs
  • 💾 Smart Caching : Avoids rebuilding unchanged projects

The Honest Truth

Angular 20’s “project references” are marketing over substance. They give you:

  • ✅ Better IDE experience
  • ❌ Same build performance
  • ❌ No incremental compilation
  • ❌ No dependency-aware building

For real Angular monorepo performance, you still need Nx. TypeScript project references alone just aren’t enough for framework projects! 🎯

Interesting PRs for Angular Project References:

7. SSR Routing Simplification — No More Manual Configuration (PR)

Before Angular 20, developers had to understand the difference between “basic SSR” and “SSR with server routing.”

Now it’s simple: SSR means full SSR with everything enabled. One less decision, better defaults, happier developers! 🚀

# Update these commands:
# ❌ ng add @angular/ssr --server-routing
# ✅ ng add @angular/ssr

# ❌ ng generate app-shell --server-routing  
# ✅ ng generate app-shell
Enter fullscreen mode Exit fullscreen mode

8. TypeScript Module Preserve for Modern Bundling (PR)

🔄 Module Preserve : New projects use "module": "preserve" instead of "module": "ES2022"

Auto-Enabled Options : Automatically sets esModuleInterop, moduleResolution: "bundler", and resolveJsonModule

🗑️ Cleaner Config : Removes redundant explicit options from tsconfig.json

📦 Better Bundler Compatibility : Matches modern bundler behavior exactly

// ❌ BEFORE - Angular 19 tsconfig.json
{
  "compilerOptions": {
    "esModuleInterop": true, // ❌ Explicitly needed
    "moduleResolution": "bundler", // ❌ Explicitly needed
    "module": "ES2022" // ❌ Old module system
  }
}

// ✅ AFTER - Angular 20 tsconfig.json
{
  "compilerOptions": {
    // ✅ These are now automatically enabled by "preserve":
    // "esModuleInterop": true,
    // "moduleResolution": "bundler", 
    // "resolveJsonModule": true,
    "importHelpers": true,
    "target": "ES2022",
    "module": "preserve" // ✅ Modern bundler-aware module system
  }
}
Enter fullscreen mode Exit fullscreen mode

🔥 @angular/build

1. Experimental Vitest Unit Testing Support (PR1, PR2)

Angular 20 introduces experimental Vitest support as an alternative to Karma for unit testing, bringing modern testing capabilities with browser support and faster execution to Angular projects.

🧪 Experimental Vitest Builder : New @angular/build:unit-test builder with Vitest support

🌐 Browser Testing : Optional browser execution with Playwright or WebDriverIO

Faster Tests : Modern test runner with better performance than Karma

🔧 Build Target Integration : Leverages existing build configurations

// ❌ Not supported yet:
// - Watch mode  
// - Custom vitest configuration
// - Some Angular-specific testing features

// ✅ Supported:
// - Basic unit testing
// - Code coverage  
// - Browser testing
// - Angular TestBed integration
Enter fullscreen mode Exit fullscreen mode

Here is a cool guide about implementing Vitest into NX monorepo by Younes Jaaidi.

2. Source Map Sources Content Control (PR)

Source maps are files that map the compiled/minified code back to the original source files, making debugging easier. By default, source maps include the actual content of the original source files (known as “sourcesContent”). This makes the source maps self-contained but can significantly increase their size.

The Angular team has added the ability to exclude the original source content from generated source maps through a new sourcesContent option. This applies to both JavaScript and CSS source maps.

// 🧭 WHAT ARE SOURCE MAPS?
// Think of them like a GPS for your code:
// 
// Your browser sees: `function a(b){return b+1}` (minified/ugly)
// Source map says: "This came from line 15 in user.service.ts: getUserAge(user)"
// DevTools shows: Your original, readable code!

// 🎯 THE NEW CHOICE: FULL GUIDE vs LIGHTWEIGHT GUIDE
// Option 1: Full Guide (sourcesContent: true) - DEFAULT
// ✅ Includes your original source code IN the source map file
// ✅ Works everywhere, even offline
// ❌ Source map files are 3x larger
// ❌ Your source code is exposed in production
// Option 2: Lightweight Guide (sourcesContent: false) - NEW!
// ✅ Source map files are 60% smaller
// ✅ Your source code stays private
// ❌ Debugging requires access to original files
// 📁 SIMPLE CONFIGURATION
// angular.json - Choose your approach
{
  "build": {
    "builder": "@angular/build:application",
    "options": {
      "sourceMap": {
        "scripts": true,
        "styles": true,
        "sourcesContent": false // 🎯 NEW: Choose lightweight guide
      }
    },
    "configurations": {
      "development": {
        "sourceMap": {
          "sourcesContent": true // 🛠️ FULL guide for debugging
        }
      },
      "production": {
        "sourceMap": {
          "sourcesContent": false // 🚀 LIGHTWEIGHT guide for production
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Smart Default Output Path — No More Configuration Needed (PR)

  • The outputPath field is no longer required in Angular application configurations
  • A default value dist/ is now used when outputPath is not specified
  • Before this change, you had to explicitly specify the outputPath in your Angular project configuration (angular.json). Now, if you don't specify it, the build will automatically use dist/ relative to your workspace root.

4. Custom Package Resolution Conditions (PR)

Angular 20 adds a new conditions option that gives you fine-grained control over how npm packages are resolved, allowing you to specify exactly which version of a package to use when it offers multiple build outputs.

// Modern npm packages often ship multiple versions:
// package.json (example: lodash-es)
{
  "name": "lodash-es",
  "exports": {
    ".": {
      "node": "./node.js", // For Node.js environments
      "browser": "./browser.js", // For browsers
      "module": "./esm.js", // Modern ES modules
      "import": "./esm.js", // ES module imports
      "require": "./cjs.js", // CommonJS requires
      "development": "./dev.js", // Development builds
      "production": "./prod.js", // Production builds (minified)
      "default": "./index.js" // Fallback
    }
  }
}

// 🔧 ANGULAR 20 CONDITIONS CONFIGURATION
// angular.json - Control package resolution
{
  "build": {
    "builder": "@angular/build:application",
    "options": {
      "conditions": ["module", "browser", "production"] // ✅ NEW: Custom conditions
    },
    "configurations": {
      "development": {
        "conditions": ["module", "browser", "development"] // ✅ Dev-specific conditions
      },
      "production": {
        "conditions": ["module", "browser", "production"] // ✅ Prod-specific conditions
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Sass Package Importers (PR)

Angular 20 adds support for Sass’s new pkg: importers, making it easier to import Sass files from npm packages without messy path configurations or ~ prefixes.

Think of pkg: importers as a modern, clean way to import Sass from npm packages. Instead of dealing with complex paths and configurations, you can directly reference any npm package in your Sass files.

// Old way (still works):
@import '~@angular/material/theming';

// New way (recommended):
@use 'pkg:@angular/material' as mat;
Enter fullscreen mode Exit fullscreen mode

🔥 @angular/ssr

1. Stabilize AngularNodeAppEngine, AngularAppEngine, and provideServerRouting APIs (PR)

These APIs enhance server-side rendering (SSR) capabilities in Angular applications, improving routing and server integration for better performance and reliability.

Breaking Changes Summary ⚠️

Breaking Changes in Angular 20

Deprecations Summary ⚠️

Deprecations Summary in Angular 20

Style guide updates

Angular 20 introduces a new file naming convention but automatically preserves your existing project’s style when upgrading.

File Naming Style Guide in Angular 20

Check the RFC.

If you want to check the modern Angular folder structure, you can read this post by Gerome Grignon. Gerome also created a library called ngx-boomer to update your angular.json to keep the previous behaviour.

Pioneering AI-First Development

Angular 20 marks a pivotal moment where framework evolution meets artificial intelligence, establishing Angular as the premier choice for developers building in the age of GenAI. The Angular team has recognized that the future of web development isn’t just about better frameworks — it’s about frameworks that work seamlessly with AI tools.

The AI Development Challenge

Modern developers face a unique paradox: while AI tools can dramatically accelerate development, they often generate outdated code patterns. Picture this scenario — you ask an AI assistant to create an Angular component, and it returns code using NgModules and *ngIf directives, patterns that were cutting-edge five years ago but are now legacy approaches. This disconnect between AI knowledge and current best practices creates friction in what should be a smooth development experience.

Angular’s AI-First Strategy

1. The llms.txt Initiative: Teaching AI About Modern Angular

Angular has introduced a groundbreaking llms.txt file—consider it a curriculum designed specifically for large language models. This isn't just documentation; it's a carefully curated learning path that helps AI systems understand:

  • Current syntax patterns : Control flow blocks (@if, @for, @switch) instead of structural directives
  • Modern architecture : Standalone components over NgModule-based approaches
  • Latest APIs : Signal-based reactivity and new lifecycle hooks
  • Best practices : Contemporary file naming and organization standards

2. AI-Enhanced Development Guidelines

The second pillar of Angular’s AI strategy focuses on empowering developers who are building AI-powered applications. This goes beyond fixing AI-generated code — it’s about creating applications that leverage AI as a core feature.

A guide about AI in the Angular official docs.

Genkit Integration Patterns

Angular has pioneered integration patterns with Google’s Genkit, creating templates for common AI use cases.

Another initiative is to start with AI experiences using modern Angular, which Hashbrown created.

Recently, Minko Gechev talked JSNation and showed a demo with an e-commerce and Gemini integration as AI using GenKit.

Here are the slides and the demo.

Angular Mascot

Check the RFC to vote for the favourite one. For me, it is the number 1.

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 really 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)