Angular v21 brings valuable refinements to the core framework, including enhancements to the Router and significant improvements in type safety, making our applications more robust and our development experience smoother.
Router: Fine-Grained Scroll Control
The Angular Router has powerful features for managing navigation, including scroll position restoration. In v21, the router gains a new scroll option that provides more fine-grained control over scrolling behavior during navigation. This allows you to override global scroll restoration settings for specific routes.
Previously, you might enable global scroll restoration like this:
// src/app/app.config.ts (Global scroll restoration)
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withInMemoryScrolling } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withInMemoryScrolling({ scrollPositionRestoration: 'enabled' })),
],
};
With scrollPositionRestoration: 'enabled', the router attempts to restore the scroll position when navigating back or forward. However, there might be cases where you want to disable this behavior for a specific navigation or control it manually.
The new scroll option (available when calling router.navigate() or router.navigateByUrl()) allows you to do just that:
'manual': Prevents scrolling, even ifscrollPositionRestorationis enabled globally. This is useful if you have custom scroll logic or want to keep the current scroll position.'after-transition': Follows the globalscrollPositionRestorationbehavior (this is the default ifscrollis not specified).
Example: Overriding Scroll Behavior
Let’s assume global scroll restoration is enabled.
// src/app/home/home.component.ts
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-home',
standalone: true,
imports: [CommonModule],
template: `
<h2>Home Page</h2>
<p>Scroll down to see the navigation buttons.</p>
<div style="height: 1000px;"></div> <!-- Spacer to create scrollable content -->
<button (click)="navigateToDetails()">Go to Details (Default Scroll)</button>
<button (click)="navigateToAboutManualScroll()">Go to About (Manual Scroll)</button>
`,
styles: [`
div { margin-bottom: 20px; }
button { margin: 10px; padding: 10px 15px; cursor: pointer; }
`]
})
export class HomeComponent {
private router = inject(Router);
navigateToDetails(): void {
// This will follow the global 'scrollPositionRestoration: enabled' setting
this.router.navigate(['/details']);
}
navigateToAboutManualScroll(): void {
// This will NOT scroll, even if global scroll restoration is enabled
this.router.navigate(['/about'], { scroll: 'manual' });
}
}
You would also need DetailsComponent and AboutComponent and define them in your routes.
Why this matters:
- Flexibility: Gives developers more granular control over user experience, especially important for single-page applications with complex layouts.
- Improved UX: Prevents unwanted scrolling on specific navigations, which can sometimes be jarring for users.
Type Safety Improvements: Generic SimpleChanges
The ngOnChanges lifecycle hook is used to react to changes in input properties. Historically, the SimpleChanges parameter provided to ngOnChanges was typed as any, meaning you had no type safety when accessing changes.propertyName.currentValue or previousValue.
In Angular v21, the SimpleChanges type is now generic. This means you can provide the type of your component as a type parameter to SimpleChanges, and TypeScript will provide full type safety for your input changes. To maintain backward compatibility, it defaults to any if no type parameter is provided.
The Old Way (Angular <= v20):
// Angular <= v20
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `<p>User: {{ name }} ({{ age }})</p>`
})
export class UserProfileComponent implements OnChanges {
@Input() name!: string;
@Input() age!: number;
ngOnChanges(changes: SimpleChanges): void {
// 'changes' is of type 'SimpleChanges', which previously didn't know the types of 'name' or 'age'
if (changes['name']) {
// nameChange is 'SimpleChange | undefined' with 'any' for currentValue/previousValue
const nameChange = changes['name'];
console.log(`Name changed from ${nameChange.previousValue} to ${nameChange.currentValue}`);
}
}
}
The New Way (Angular v21 - Type Safe SimpleChanges):
// Angular v21
import { Component, Input, OnChanges, SimpleChanges, input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-profile',
standalone: true,
imports: [CommonModule],
template: `
<h3>User Profile</h3>
<p>Name: {{ userDisplayName() }}</p>
<p>Age: {{ userAge() }}</p>
`,
styles: [`
:host { display: block; padding: 20px; border: 1px solid #007bff; border-radius: 8px; }
`]
})
export class UserProfileComponent implements OnChanges<UserProfileComponent> {
// Using new input() signal-based API (recommended in modern Angular)
userDisplayName = input.required<string>();
userAge = input.required<number>();
ngOnChanges(changes: SimpleChanges<UserProfileComponent>): void {
// Now, 'changes' is type-safe!
const nameChange = changes.userDisplayName; // Typed as `SimpleChange<string> | undefined`
const ageChange = changes.userAge; // Typed as `SimpleChange<number> | undefined`
if (nameChange) {
// nameChange.previousValue is typed as 'string | undefined'
console.log(`Name changed from ${nameChange.previousValue} to ${nameChange.currentValue}`);
}
if (ageChange) {
console.log(`Age changed from ${ageChange.previousValue} to ${ageChange.currentValue}`);
}
}
}
Explanation:
- By adding
<UserProfileComponent>toOnChanges<UserProfileComponent>, we explicitly tellSimpleChangeswhat component it’s observing. - The
changesobject now correctly infers the types ofuserDisplayNameanduserAge(from ourinput()definitions). - Accessing
nameChange.previousValueorcurrentValuenow provides proper type checking, preventing potential runtime errors.
Why this matters:
- Improved Type Safety: Catches errors related to input property changes at compile-time instead of runtime.
- Better Developer Experience: IDE autocompletion and type checking for
ngOnChangesbecome much more helpful. - Robust Applications: Reduces the likelihood of subtle bugs caused by incorrect assumptions about input data types.
Other Compiler & Diagnostic Improvements
Angular v21 also includes general compiler (ngtsc) and diagnostic enhancements:
typeCheckHostBindingsby Default: The compiler optiontypeCheckHostBindingsis now enabled by default. This provides stricter type checking for host bindings, catching potential issues earlier.- Uninitialized Required Input Detection: The compiler now detects when a required input, model, viewChild, or contentChild property is read before it’s initialized. This prevents runtime errors by flagging these issues during compilation.
- Unreachable/Duplicated
@deferTriggers: New diagnostics detect inefficient or impossible@defertriggers, helping optimize lazy loading.
These compiler improvements contribute to a more robust and error-resistant development process.
Mini-Challenge: Implement Type-Safe ngOnChanges
Create a new standalone component called ProductCardComponent with two inputs: productName: string and price: number.
- Implement
ngOnChangesto log changes to both inputs. - Ensure
SimpleChangesis type-safe by applying the generic type parameter. - Add the component to
AppComponentand pass different values forproductNameandpriceto observe thengOnChangeslogs in your console.
// HINT: ProductCardComponent
import { Component, Input, OnChanges, SimpleChanges, input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-product-card',
standalone: true,
imports: [CommonModule],
template: `
<div class="card">
<h4>{{ productName() }}</h4>
<p>Price: \${{ price() | number:'1.2-2' }}</p>
</div>
`,
styles: [`
.card {
border: 1px solid #ddd;
padding: 15px;
margin: 10px;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
max-width: 250px;
}
h4 { color: #333; margin-top: 0; }
p { color: #666; }
`]
})
export class ProductCardComponent implements OnChanges<ProductCardComponent> {
productName = input.required<string>();
price = input.required<number>();
ngOnChanges(changes: SimpleChanges<ProductCardComponent>): void {
if (changes.productName) {
console.log(`Product Name changed: ${changes.productName.previousValue} -> ${changes.productName.currentValue}`);
}
if (changes.price) {
console.log(`Price changed: ${changes.price.previousValue} -> ${changes.price.currentValue}`);
}
}
}
// In AppComponent to test:
// import { ProductCardComponent } from './product-card/product-card.component';
// @Component({ /* ... */ imports: [ProductCardComponent], /* ... */ template: `<app-product-card [productName]="'Laptop'" [price]="999.99"></app-product-card>`})
Summary/Key Takeaways
- Angular v21’s Router introduces a new
scrolloption ('manual'or'after-transition') forrouter.navigate()to provide more fine-grained control over scroll behavior during navigation, overriding global settings. - The
SimpleChangestype forngOnChangesis now generic, allowing for type-safe access to input property changes by providing the component’s type. - Other compiler improvements like default
typeCheckHostBindingsand better detection of uninitialized required inputs further enhance type safety and developer diagnostics.
These updates collectively contribute to building more predictable, maintainable, and robust Angular applications with an even better developer experience. In the final learning chapter, we’ll discuss the overall migration process to Angular v21.