TL;DR: Since Angular 16+, you can bind route parameters, query parameters, and even route data directly to component inputs usinginput()or@Input(). No more injectingActivatedRouteand subscribing toparamMap. Just enablewithComponentInputBinding()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
OnInitandOnDestroy
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:
- Route data (static data from the route config)
- Path parameters (like
:id) - 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.
Standalone Apps (recommended)
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:
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
ActivatedRouteinjection, 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! 🚀
Discover more articles
Angular input() für Route-Parameter: Schluss mit ActivatedRoute 🚀
Mit Angular 16+ kannst du Route-Parameter direkt per input() in deine Komponente binden – ganz ohne ActivatedRoute. So geht's!
Angular input() für Route-Parameter: Schluss mit ActivatedRoute 🚀
Mit Angular 16+ kannst du Route-Parameter direkt per input() in deine Komponente binden – ganz ohne ActivatedRoute. So geht's!
Angular: Framework für Single Page Applications🌐
Entdecke Angular, das leistungsstarke Framework von Google, ideal für dynamische Webanwendungen. Erfahre mehr über Komponenten, Datenbindung und Routing! 🌐