Bind Route Info to Component Inputs (✨ New Router feature)
Pass router info to routed component inputs
Topics covered in this article:
- How it works today
- How it will work in Angular v16
- How to use it
- How to migrate to the new API
- How to test it
- Caveats
When building applications with Angular, most of the time we use the Router to render different pages for different urls.
And based on the url we also load the data based on its path parameters or query parameters.
In the latest version of Angular v16, we will get a new feature that will simplify the process of retrieving route information in the component and make it way easier.
How it works today
Let's say we have a routes array like this one:
const routes: Routes = [
{
path: "search",
component: SearchComponent,
},
];
And inside the component we need to read the query params in order to fill a search form.
With an URL like this: http://localhost:4200/search?q=Angular
;
@Component({})
export class SearchComponent implements OnInit {
// here we inject the ActivatedRoute class that contains info about our current route
private route = inject(ActivatedRoute);
query$ = this.route.queryParams.pipe(map(queryParams) => queryParams['q']);
ngOnInit() {
this.query$.subscribe(query => { // do something with the query });
}
}
As you can see, we need to inject the ActivatedRoute
service and then we can access the query params from it. But we can also access the path params and the data, or even the resolved data, as we can see in the following example:
const routes: Routes = [
{
path: "search/:id",
component: SearchComponent,
data: { title: "Search" },
resolve: { searchData: SearchDataResolver }
},
];
@Component({})
export class SearchComponent implements OnInit {
private route = inject(ActivatedRoute);
query$ = this.route.queryParams.pipe(map(queryParams) => queryParams['q']);
id$ = this.route.params.pipe(map(params) => params['id']);
title$ = this.route.data.pipe(map(data) => data['title']);
searchData$ = this.route.data.pipe(map(data) => data['searchData']);
ngOnInit() {
this.query$.subscribe(query => { // do something with the query });
this.id$.subscribe(id => { // do something with the id });
this.title$.subscribe(title => { // do something with the title });
this.searchData$.subscribe(searchData => { // do something with the searchData });
}
}
How it will work in Angular v16
In Angular v16 we will get a new feature that will simplify the process of retrieving route information in the component and make it way easier.
We will be able to pass the route information to the component inputs, so we don't need to inject the ActivatedRoute
service anymore.
const routes: Routes = [
{
path: "search",
component: SearchComponent,
},
];
@Component({})
export class SearchComponent implements OnInit {
/*
We can use the same name as the query param, for example 'query'
Example url: http://localhost:4200/search?query=Angular
*/
@Input() query?: string; // we can use the same name as the query param
/*
Or we can use a different name, for example 'q', and then we can use the @Input('q')
Example url: http://localhost:4200/search?q=Angular
*/
@Input('q') queryParam?: string; // we can also use a different name
ngOnInit() {
// do something with the query
}
}
And we can also pass the path params, the data and resolved data to the component inputs:
const routes: Routes = [
{
path: "search/:id",
component: SearchComponent,
data: { title: "Search" },
resolve: { searchData: SearchDataResolver }
},
];
@Component({})
export class SearchComponent implements OnInit {
@Input() query?: string; // this will come from the query params
@Input() id?: string; // this will come from the path params
@Input() title?: string; // this will come from the data
@Input() searchData?: any; // this will come from the resolved data
ngOnInit() {
// do something with the query
// do something with the id
// do something with the title
// do something with the searchData
}
}
And of course we can rename the inputs to whatever we want:
const routes: Routes = [
{
path: "search/:id",
component: SearchComponent,
data: { title: "Search" },
resolve: { searchData: SearchDataResolver }
},
];
@Component({})
export class SearchComponent implements OnInit {
@Input() query?: string;
@Input('id') pathId?: string;
@Input('title') dataTitle?: string;
@Input('searchData') resolvedData?: any;
ngOnInit() {
// do something with the query
// do something with the pathId
// do something with the dataTitle
// do something with the resolvedData
}
}
How to use it
In order to use this new feature, we need to enable it in the RouterModule
:
@NgModule({
imports: [
RouterModule.forRoot([], {
//... other features
bindToComponentInputs: true // <-- enable this feature
})
],
})
export class AppModule {}
Or if we are in a standalone application, we can enable it like this:
bootstrapApplication(App, {
providers: [
provideRouter(routes,
//... other features
withComponentInputBinding() // <-- enable this feature
)
],
});
How to migrate to the new api
If we have a component that is using the ActivatedRoute
service, we can migrate it to the new api by doing the following:
- Remove the
ActivatedRoute
service from the component constructor. - Add the
@Input()
decorator to the properties that we want to bind to the route information. - Enable the
bindToComponentInputs
feature in theRouterModule
orprovideRouter
function.
Example with before and after for path params, with url: http://localhost:4200/search/123
// Before
@Component({})
export class SearchComponent implements OnInit {
private route = inject(ActivatedRoute);
id$ = this.route.params.pipe(map(params) => params['id']);
ngOnInit() {
this.id$.subscribe(id => { // do something with the id });
}
}
// After
@Component({})
export class SearchComponent implements OnInit {
@Input() id?: string; // this will come from the path params
ngOnInit() {
// do something with the id
}
}
How to test it
In order to test the new feature, we can use the RouterTestingHarness
and let it handle the navigation for us.
Here is an example of how to test the route info bound to component inputs with the RouterTestingHarness
:
@Component({})
export class SearchComponent {
@Input() id?: string;
@Input() query?: string;
}
it('sets id and query inputs from matching query params and path params', async () => {
TestBed.configureTestingModule({
providers: [ provideRouter(
[{ path: 'search/:id', component: SearchComponent }],
withComponentInputBinding()
) ],
});
const harness = await RouterTestingHarness.create();
const instance = await harness.navigateByUrl(
'/search/123?query=Angular',
TestComponent
);
expect(instance.id).toEqual('123');
expect(instance.query).toEqual('Angular');
await harness.navigateByUrl('/search/2?query=IsCool!');
expect(instance.id).toEqual('2');
expect(instance.query).toEqual('IsCool!');
});
It's as simple as that!
Caveats
- Sometimes we want the
id
orqueryParams
to be observables, so we can combine them with other observable to get some data.
For example, let's say we have a component that is using the id
and queryParams
to get some data from the server:
@Component({})
export class SearchComponent implements OnInit {
private dataService = inject(DataService);
@Input() id?: string;
@Input() query?: string;
ngOnInit() {
this.dataService.getData(this.id, this.query).subscribe(data => {
// do something with the data
});
}
}
If we want to use the async pipe in order to subscribe to the data, we need to make sure that the id
and query
are observables instead of strings, otherwise this example below will not work:
@Component({})
export class SearchComponent implements OnInit {
private dataService = inject(DataService);
@Input() id?: string;
@Input() query?: string;
// this will not work because the id and the query don't have a value yet (they are undefined)
// they will have a value only after the component is initialized and the inputs are set
data$ = this.dataService.getData(this.id, this.query);
}
In order to make the id
and query
observables, we can use the BehaviorSubject
:
@Component({
template: `
<div *ngIf="data$ | async as data">
{{ data }}
</div>
`
})
export class SearchComponent implements OnInit {
private dataService = inject(DataService);
id$ = new BehaviorSubject<string | null>(null);
query$ = new BehaviorSubject<string | null>(null);
@Input() set id(id: string) { this.id$.next(id); }
@Input() set query(query: string) { this.query$.next(query); }
data$ = combineLatest([
this.id$.pipe(filter(id => id !== null)),
this.query$.pipe(filter(query => query !== null))
]).pipe(
switchMap(([id, query]) => this.dataService.getData(id, query))
);
}
As you can see, we are using the BehaviorSubject
to make the id
and query
observables, and we are using the combineLatest
operator to combine them with the switchMap
operator to get the data from the server.
Personally, I think that this is a bit too much code for a simple example, so I would recommend to use the ActivatedRoute
service instead of the new api in this case.
- Priority of the route information when the route infos have the same name. For example, let's say we have a route with the following configuration:
const routes: Routes = [
{
path: 'test/:value',
component: TestComponent,
data: { value: 'Hello from data' },
}
];
@Component({ template: `{{ value }}` })
export class TestComponent {
@Input() value?: string;
}
The new api will bind the route information to the component inputs in the following order:
- Data
- Path params
- Query params
If there's no data, it will use the path params, if there's no path params, it will use the query params If there's no query params, the value input will be undefined!
- We don't know where the input value will come from 😬
In my opinion, for this "issue" what we can do is to rename the Input in imports and use it like this:
import { Input as RouteInput, Component } from "@angular/core";
@Component({ template: `{{ value }}` })
export class TestComponent {
@RouteInput() value?: string;
}
// OR
import { Input as QueryParamInput, Component } from "@angular/core";
@Component({ template: `{{ value }}` })
export class TestComponent {
@QueryParamInput() value?: string;
}
Not the best way possible, but we can see that it's not a normal input and that it is connected with the router info.
Play with the feature here: https://stackblitz.com/edit/angular-jb85mb?file=src/main.ts 🎮
Thanks for reading!
If this article was interesting and useful to you, and you want to learn more about Angular, support me by buying me a coffee ☕️ or follow me on X (formerly Twitter) @Enea_Jahollari where I tweet and blog a lot about Angular
latest news, signals, videos, podcasts, updates, RFCs, pull requests and so much more. 💎