TL;DR: Since Angular 16+, you can bind route parameters, query parameters, and even route data directly to component inputs using input() or @Input(). No more injecting ActivatedRoute and subscribing to paramMap. Just enable withComponentInputBinding() and you're done.

🤔 Introduction

If you've been working with Angular for a while, you know the drill: inject ActivatedRoute, subscribe to paramMap or queryParamMap, and don't forget to unsubscribe when the component is destroyed. It works, but it's verbose and adds unnecessary boilerplate to every routed component.

Starting with Angular 16, there's a much cleaner way: binding route parameters directly to your component's inputs. Whether you use the classic @Input() decorator or the new signal-based input() function, this approach is simpler, more declarative, and easier to test.

Let me show you how.

😩 The Traditional Approach with ActivatedRoute

Here's how most of us have been reading route parameters for years:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subject, takeUntil } from 'rxjs';

@Component({
  selector: 'app-user-profile',
  template: `<h1>User: {{ userId }}</h1>`
})
export class UserProfileComponent implements OnInit, OnDestroy {
  userId: string = '';
  private destroy$ = new Subject<void>();

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    this.route.paramMap
      .pipe(takeUntil(this.destroy$))
      .subscribe(params => {
        this.userId = params.get('id') ?? '';
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

That's a lot of code just to read a single route parameter. You need to:

  • Inject ActivatedRoute
  • Subscribe to the observable
  • Handle unsubscription to prevent memory leaks
  • Implement both OnInit and OnDestroy

Now imagine doing this for multiple parameters and query params. It gets messy fast.

✨ The New Approach: Binding Route Params to Inputs

With Angular 16+, you can skip all of that and bind route parameters directly to component inputs. The router does the heavy lifting for you.

Using the @Input() Decorator

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  standalone: true,
  template: `<h1>User: {{ id }}</h1>`
})
export class UserProfileComponent {
  @Input() id!: string;
}

Yes, that's it. The id input gets automatically populated from the route parameter :id. No ActivatedRoute, no subscriptions, no cleanup.

Using the Signal-Based input()

If you're already embracing Angular Signals (and you should!), the input() function makes this even more elegant:

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  standalone: true,
  template: `<h1>User: {{ id() }}</h1>`
})
export class UserProfileComponent {
  id = input<string>('');
}

The signal-based input() gives you a reactive, read-only signal that updates automatically when the route parameter changes. You can use it in templates with id() and in computed signals or effects for derived state.

This also works for query parameters and route data:

@Component({
  selector: 'app-search',
  standalone: true,
  template: `
    <p>Searching for: {{ query() }}</p>
    <p>Page: {{ page() }}</p>
    <p>Title: {{ title() }}</p>
  `
})
export class SearchComponent {
  // Binds to query parameter ?query=...
  query = input<string>('');

  // Binds to query parameter ?page=...
  page = input<string>('1');

  // Binds to route data { title: '...' }
  title = input<string>('');
}

🛣️ Route Configuration

Your route configuration stays the same as always. The parameter names in your route path just need to match your input names:

const routes: Routes = [
  {
    path: 'users/:id',
    component: UserProfileComponent,
    data: { title: 'User Profile' }
  },
  {
    path: 'search',
    component: SearchComponent,
    data: { title: 'Search Results' }
  }
];

The router binds in this priority order:

  1. Route data (static data from the route config)
  2. Path parameters (like :id)
  3. Query parameters (like ?query=foo)

Keep this in mind: if you have a path parameter and a query parameter with the same name, the path parameter wins.

⚙️ Setup: Enabling Component Input Binding

This feature doesn't work out of the box — you need to opt in. The setup depends on whether you're using standalone or NgModule-based apps.

In your app.config.ts, add withComponentInputBinding() to the router provider:

import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding())
  ]
};

NgModule-Based Apps

If you're still using NgModule, set bindToComponentInputs in your router module:

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      bindToComponentInputs: true
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}

That's all the configuration you need. Once enabled, it works globally for all routed components in your app.

🔄 Transform Functions for Type Safety

Route parameters are always strings, but sometimes you need a number or boolean. Angular provides transform functions to handle this cleanly.

Since we're talking about TypeScript here, type safety matters a lot. If you want to learn more about why TypeScript is essential, check this out:

Why You Should Only Use TypeScript
TypeScript adds type safety and better tooling to your JavaScript projects. Here's why you should make the switch.

Here's how to use transforms with input():

import { Component, input, numberAttribute, booleanAttribute } from '@angular/core';

@Component({
  selector: 'app-product',
  standalone: true,
  template: `
    <p>Product #{{ productId() }}</p>
    <p>Show details: {{ showDetails() }}</p>
  `
})
export class ProductComponent {
  // Transforms the string "42" into the number 42
  productId = input(0, { transform: numberAttribute });

  // Transforms "true"/"" into boolean true
  showDetails = input(false, { transform: booleanAttribute });
}

You can also write custom transform functions:

// Custom transform: parse comma-separated tags
const parseTags = (value: string): string[] =>
  value ? value.split(',').map(t => t.trim()) : [];

@Component({
  selector: 'app-article',
  standalone: true,
  template: `
    <ul>
      @for (tag of tags(); track tag) {
        <li>{{ tag }}</li>
      }
    </ul>
  `
})
export class ArticleComponent {
  // ?tags=angular,typescript,signals → ['angular', 'typescript', 'signals']
  tags = input<string[]>([], { transform: parseTags });
}

🎯 Benefits: Why You Should Switch

Let's recap why this approach is superior:

  • Less boilerplate: No more ActivatedRoute injection, no subscriptions, no manual cleanup
  • Better testability: You can test your component by simply setting input values — no need to mock ActivatedRoute
  • Deep linking works automatically: Users can bookmark or share URLs, and the component state is restored from the URL
  • Signal integration: With input(), route params become reactive signals, fitting perfectly into Angular's reactive model
  • Type safety: Transform functions give you proper types instead of raw strings everywhere
  • Reusability: The same component can be used as a routed component or as a child component with regular input binding

🏁 Conclusion

The days of verbose ActivatedRoute boilerplate are over. With input() and withComponentInputBinding(), Angular gives you a clean, declarative, and type-safe way to work with route parameters.

If you're starting a new project or refactoring an existing one, make the switch. Your future self (and your code reviewers) will thank you.

Happy coding! 🚀

Artikel teilen:Share article:

Wie fandest du diesen Artikel?How did you like this article?