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 if scrollPositionRestoration is enabled globally. This is useful if you have custom scroll logic or want to keep the current scroll position.
  • 'after-transition': Follows the global scrollPositionRestoration behavior (this is the default if scroll is 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> to OnChanges<UserProfileComponent>, we explicitly tell SimpleChanges what component it’s observing.
  • The changes object now correctly infers the types of userDisplayName and userAge (from our input() definitions).
  • Accessing nameChange.previousValue or currentValue now 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 ngOnChanges become 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:

  • typeCheckHostBindings by Default: The compiler option typeCheckHostBindings is 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 @defer Triggers: New diagnostics detect inefficient or impossible @defer triggers, 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.

  1. Implement ngOnChanges to log changes to both inputs.
  2. Ensure SimpleChanges is type-safe by applying the generic type parameter.
  3. Add the component to AppComponent and pass different values for productName and price to observe the ngOnChanges logs 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 scroll option ('manual' or 'after-transition') for router.navigate() to provide more fine-grained control over scroll behavior during navigation, overriding global settings.
  • The SimpleChanges type for ngOnChanges is now generic, allowing for type-safe access to input property changes by providing the component’s type.
  • Other compiler improvements like default typeCheckHostBindings and 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.