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/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')]
}
]
});
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 rejectionserror
- 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
]
});
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
⚠️ 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() ⚠️
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
})
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
})
Understanding the Configuration Options
Non-Exhaustive Mode (exhaustive: false
)
provideCheckNoChangesConfig({ exhaustive: false })
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
})
Behaviour: Treats ALL components as if they use
ChangeDetectionStrategy.Default
Coverage: Checks all views attached to
ApplicationRef
and their descendantsBenefits: 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
]
});
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
})
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
]
};
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 documentSSR 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';
8. Zoneless Change Detection: From Experimental to Developer Preview (#60748)
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(),
// ... other providers
]
});
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);
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 });
Affected APIs
All public injection APIs no longer accept InjectFlags
:
inject()
functionInjector.get()
methodEnvironmentInjector.get()
methodTestBed.get()
methodTestBed.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
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
});
}
}
What Remains in Developer Preview
// ⚠️ Still in developer preview
this.pendingTasks.run(async () => {
// Async work here
await this.performAsyncWork();
});
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"
}
}
⚠️ 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"
}
}
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
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>
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
]
});
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!
}
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;
}
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
});
}
}
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!)
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" ✓
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
When Validation Triggers
Angular validates patterns and throws errors for:
Week-based year without week indicator: Using
Y
withoutw
Mixed date components: Using day-of-year
D
with monthM
// ❌ 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"
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');
}
}
🔥 @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!
}
}
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 */)"
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
}
}
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');
}
}
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;
}
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]}**` : '');
}, '');
};
}
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
};
}
🔥 @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);
}
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);
}
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' }];
}
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 expressionsIDE integration with hover information, autocomplete, and renaming support
This feature is controlled by the typeCheckHostBindings
compiler flag:
{
"angularCompilerOptions": {
"strictTemplates": true,
"typeCheckHostBindings": true
}
}
🔥 @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();
}
}
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');
}
}
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
}
🔥 @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...
]
});
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();
}
}
🔥 @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));
2. Deprecate the HammerJS integration (#60257)
What's Being Deprecated
The following HammerJS-related APIs and providers are now deprecated:
HammerGestureConfig
HAMMER_GESTURE_CONFIG
tokenHammerModule
Built-in HammerJS gesture directives
Automatic HammerJS gesture recognition
Why This Change?
Native Browser Support: Modern browsers provide comprehensive touch and gesture APIs
Bundle Size Reduction: Removing HammerJS dependency reduces application bundle size
Performance: Native browser events are more performant than library-based solutions
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?
Better Testing Practices: E2E tests provide more realistic SSR verification
Real Environment Testing: E2E tests run in actual browser/server environments
Simplified Maintenance: Reduces complexity in the platform-server package
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');
});
});
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;
}
}
}
2. Asynchronous Router Redirects (#60863)
The RedirectFunction
type now supports returning MaybeAsync<string | UrlTree>
, which means redirect functions can return:
Synchronous values:
string | UrlTree
Promises:
Promise<string | UrlTree>
Observables:
Observable<string | UrlTree>
// 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')
);
}
}
];
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
}
]
}
]
}
];
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 <a>
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 {}
🔥 @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!');
});
}
}
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'
tomoduleResolution: '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
}
}
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)
withprovideServerRendering(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
)
]
};
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
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
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
}
}
🔥 @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
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
}
}
}
}
}
3. Smart Default Output Path - No More Configuration Needed (PR)
The
outputPath
field is no longer required in Angular application configurationsA default value
dist/<project_name>
is now used whenoutputPath
is not specifiedBefore 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 usedist/<project_name>
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
}
}
}
}
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;
🔥 @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 ⚠️
Deprecations Summary ⚠️
Style guide updates
Angular 20 introduces a new file naming convention but automatically preserves your existing project's style when upgrading.
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 directivesModern 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
Another initiative is to start with AI experiences using modern Angular, which Hashbrown created.
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 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! 👋😁