Write Angular like a pro. Angular Icon

Follow the ultimate Angular roadmap.

Binding Angular Route Params to Component @Input

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 the route.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.

Learn Angular the right way.

The most complete guide to learning Angular ever built.
Trusted by 82,951 students.

Todd Motto

with Todd Motto

Google Developer Expert icon Google Developer Expert

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover