ActivatedRoute
just left the chat. Say hello to router input bindings straight to your components.
Angular has seen a surge of love lately, especially with the addition of standalone components, signals and many other features.
But, one less-likely superhero hiding away in the source code are router bindings directly to @Input
decorators. What does this mean then?
tl;dr - use
withComponentInputBinding
and slap an@Input
on your routed component. But come on, you’re better than that, take a moment to explore below.
Well, route params are the building blocks of a well structured Angular application, where the route is the source of truth as to where we are in the application, at which point we can make assumptions about the state that lives in the application too.
Route params are typically dynamic values, for example if we need to get an id
of a current page we’re viewing.
So, we could assume we have something like these in the URL:
/pizza/pepperoni
/pizza/pineapple
/pizza/hot-and-spicy
The way we’d define these within an Angular route remains the same when dealing with component inputs, but first let’s understand why this feature is a great addition to the core of Angular.
In a standalone Angular app, our routes could look like this:
export const PizzaRoutes: Routes = [
{
path: 'pizzas',
loadComponent: () => import('./containers/pizzas.component').then((x) => x.PizzasComponent),
},
{
path: 'pizza/:id',
loadComponent: () => import('./containers/pizza-single.component').then((x) => x.PizzaSingleComponent),
},
];
We have two example routes, pizzas
and pizza/:id
. Our application could route to pizzas
, the user clicks a pizza and is redirected to that single pizza.
The /:id
portion of the URL then becomes our route parameter, where /:id
sits as a placeholder for an actual id
field from each pizza - as demonstrated with pepperoni
or pineapple
above.
That’s great, and I’m sure this configuration looks fairly trivial to you, so what gives with these route param as @Input
bindnigs?
Well, gone are the days of injecting the ActivatedRoute
class as a dependency and setting up some router logic.
I’ll keep this simple, but here’s how it’d typically look:
@Component({...})
export class PizzaSingleComponent implements OnInit {
private route = inject(ActivatedRoute);
pizza$: Observable<Pizza>;
ngOnInit() {
this.pizza$ = this.route.paramMap.pipe(
switchMap((params) => this.pizzaStore.select(params.get('id')))
);
}
}
We’d then pass pizza$
off to an async pipe and be on our way to getting our route params.
Now, you may wish to subscribe to the route.paramMap
or you could also opt to use the static route.snapshot.paramMap
.
If you want to keep observing route changes that may occur whilst the component is still mounted, then using
route.paramMap
is the way to go, otherwise using theroute.snapshot
could be a simpler alternative.
So, with this in mind it’s time to end the career of ActivatedRoute
.
First, you’ll need to setup your routing to enable component input bindings via the router, do this by importing withComponentInputBinding
:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { AppComponent } from './app/app.component';
import { AppRoutes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(
AppRoutes,
withComponentInputBinding()
)
],
});
If you’re not cool enough to be using a fully standalone Angular yet, then here’s the NgModule
approach:
@NgModule({
imports: [
RouterModule.forRoot(AppRoutes, { bindToComponentInputs: true })
],
})
export class AppModule {}
Either way, I believe you’re cool enough. NgModule
was a great way to architect Angular apps, and still is. Migrating to standalone isn’t exactly an easy job given the complexity of shared modules and components that need batch-migrating before being used. Hopefully the Angular team will be able to offer some more support on this in the future to make migrations easier.
🚀 If you’d like to learn all about “modern Angular” then my Angular Courses are for you. You’ll also migrate an application we build over to standalone components - no surprises or crazy steps to take.
With this in mind, let’s refactor our PizzaSingleComponent
to use an @Input
binding and the name of the route param that we setup in the routes (remember, that was :id
)…
Instead of that verbose ActivatedRoute
(how dare they make us add an extra import
). How’s this for a slice of fried gold?
@Component({...})
export class PizzaSingleComponent implements OnInit {
@Input() id!: string;
pizza$: Observable<Pizza>;
ngOnInit() {
this.pizza$ = this.pizzaStore.select(this.id);
}
}
I love it, but what I don’t love it how it magically is called id
and we have no actual way of telling where the @Input
came from… BUT…
This is a container component, right? So really it shouldn’t have any inputs. The fact it does means that it’s very likely going to be from the router.
Either way, a comment added to your codebase on router input bindings will probably go a long way and your future self will thank you when you’ve forgotten all about them.
And that’s it, simple and concise, I like it.
Even though it feels a bit dirty using an @Input
on a container component that’s routed.
It’s kind of like being on a Zoom call and wearing your pyjamas with a suit jacket.
For more, drop by my Angular courses and I’m sure you’ll learn a thing or three.
Happy binding, dear ng-friend.