Intro
I am generally a positive person that endeavors to say nice things about people and frameworks. In the broadest sense, I love Angular, and it has been an amazing tool to build some really cool things. Angular 2 has exceeded my expectations in a lot of ways. But! There has always been some cognitive dissonance when it comes to the router in Angular 2. Its interesting history is no secret, and historically it has personally been a huge source of frustration for me. Having named outlets was something that I completely took for granted with UI-Router, and it drove me crazy that for the longest time, they just did not exist in Angular 2 and I wanted it! Well, dreams do come true! In the newest router, setting up named router outlets is a snap and in fact, they work pretty much exactly like unnamed router outlets. Please give Victor Savkin a hug next time you see him.
Speaking of Victor, I would like to thank him for his valuable input on this post.
Our sample project allows us to see a list of speakers and their bios when you select on them. The speaker list and speaker bio components are mapped to two parallel named router outlets. Grab the sample project below and let’s go!
Sample CodeRouting Module
The first thing that we need to do when setting up routes for our project is to define the routing table. We will start out with a basic home route that maps to the HomeComponent and then add another route to redirect the root path to our home route.
const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', component: HomeComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], providers: [] }) export class RoutingModule { }
We will create another route that maps to our SpeakersComponent that has two children routes that we will use to introduce our named router outlets. Within the speakers route, we want to be able to display a list of speakers and the bio for the currently selected speaker. We accomplish this by adding in two child routes for the SpeakerListComponent and the BioComponent. Notice that both child routes follow the exact same structure as their parent route with the presence of a path and component property. The difference is that we have introduced an outlet property that we will use to map to a router-outlet in our template.
const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', component: HomeComponent }, { path: 'speakers', component: SpeakersComponent, children: [ { path: 'speakersList', component: SpeakersListComponent, outlet: 'list' }, { path: ':id', component: BioComponent, outlet: 'bio' } ] } ];
The path for the bio outlet is :id which we will use to send in a route param to load in a specific speaker.
Router Outlet
We are using an unnamed router outlet to load our home and speakers route as seen in the template below.
<div class="app-content"> <router-outlet></router-outlet> </div>
In our SpeakersComponent template, we are going to do essentially the same thing but instead of one router outlet, we will use two. And so that the router can delineate what component goes into what outlet, we will add a name attribute that matches up to our routing table. The route with the list output will be loaded into the outlet named list and so on.
<div class="columns"> <md-card> <router-outlet name="list"></router-outlet> </md-card> <md-card> <router-outlet name="bio"></router-outlet> </md-card> </div>
Router Navigate
So far this has been a fairly straightforward exercise in the universe matching up with what we expect to see. Things get a little more interesting when we want to navigate to parallel named router outlets.
We can navigate to a route within our template using the routerLink directive and if the route is static then we can simply pass in the value to the route we want to navigate to. Because we do not need to pass in an dynamic values to the home route, adding routerLink=”home” to our button will suffice.
<button routerLink="home" md-button>Home</button>
When a route needs to generate a dynamic URL, we can pass it an array of values that get mapped to the URL. In our case, when we go to the speakers route, we want to map our list outlet to the speakerList path and our bio outlet to have a path of none which evaluates to a speaker id of none.
<button md-button [routerLink]="['/speakers', {outlets: {'list': ['speakersList'], 'bio': ['none']}}]"> Speakers </button>
This will generate a URL that looks like this http://localhost:4200/speakers/(list:speakersList//bio:none) and looks like this in the application.
To navigate to a specific speaker, we will first add a click handler to our speakers list that calls showBio with the speaker.id.
<md-grid-list cols="3" gutterSize="4px"> <md-grid-tile *ngFor="let speaker of speakers" [style.background-image]="'url(assets/' + speaker.src + ')'" (click)="showBio(speaker.id)"></md-grid-tile> </md-grid-list>
And within our SpeakersListComponent, we will call router.navigate to update our bio outlet with a path based on the id parameter.
showBio(id) { this.router.navigate(['/speakers', {outlets: {'bio': [id]}}]); }
This will generate a URL that looks like this http://localhost:4200/speakers/(bio:1//list:speakersList) and looks like this in the application.
For reference, you can see the entire SpeakersListComponent below.
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { SpeakersService, Speaker } from '../shared'; @Component({ selector: 'app-speakers-list', templateUrl: './speakers-list.component.html', styleUrls: ['./speakers-list.component.css'] }) export class SpeakersListComponent implements OnInit { speakers: Speaker[] = []; constructor( private router: Router, private service: SpeakersService ) { } ngOnInit() { this.speakers = this.service.getSpeakers(); } showBio(id) { this.router.navigate(['/speakers', {outlets: {'bio': [id]}}]); } }
Route Params
We are successfully generating a URL that communicates appropriate paths to our named router outlets but how do we use that information to do something useful? We can use ActivatedRoute to get the route parameters off of the active route to perform logic within our components. In our case, we want to use the id parameter to retrieve the correct speaker information so that we can display it in our template. We can use Activated to get all sorts of useful information about the route loaded within a component but in our case, we are most interested in the params property. The params property is an observable that we can subscribe to that will give us the parameters that have been scoped to the current route.
ngOnInit() { this.route.params.subscribe((params: {id: string}) => { this.currentSpeaker = this.service.getSpeakerByID(params.id); }); }
Within the subscribe method, we will use params.id to set this.currentSpeaker by calling this.service.getSpeakerByID.
For reference, you can see the entire BioComponent below.
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { SpeakersService, Speaker } from '../shared'; @Component({ selector: 'app-bio', templateUrl: './bio.component.html', styleUrls: ['./bio.component.css'] }) export class BioComponent implements OnInit { currentSpeaker: Speaker; constructor( private route: ActivatedRoute, private service: SpeakersService ) { } ngOnInit() { this.route.params.subscribe((params: {id: string}) => { this.currentSpeaker = this.service.getSpeakerByID(params.id); }); } }
And just to wrap things up, within our BioComponent template, we are binding to currentSpeaker and using the safe navigation operator to keep our template stable when there isn’t a speaker selected.
<div *ngIf="!currentSpeaker"> <md-card-title>Please select a speaker</md-card-title> <md-card-subtitle>No speaker selected</md-card-subtitle> </div> <md-card-title>{{currentSpeaker?.name}}</md-card-title> <md-card-subtitle>{{currentSpeaker?.bio}}</md-card-subtitle>
Review
And this is how we can have parallel, named router outlets within our Angular 2 application! I am so happy!
Let’s do a quick recap of what we covered.
- A named outlet route works just like an unnamed outlet route with the difference being the addition of the outlet property on the route definition.
- We add a name property to the router-outlet element so the router knows where to load the component.
- When navigating to a route with named outlets, we need to add in an outlet object that defines the approprioate path for each outlet
- We can use Router.navigate to navigate to a route within our component.
- We can use ActivatedRoute to get route parameter values to perform logic within our component.
Resources
A super awesome resource in general. Victor Savkin’s Blog
Well worth the $25 investment. Buy it now! Angular 2 Router Book by Victor Savkin
And just in case… Router API Documentation
Sample Code
Lukas, as always, really great work. Many thanks for sharing your experience with the community.
Steve
Thanks Steve! #highFive
Hi Lukas, thanks! This is a good article!
I’m trying to do a similar app but using loadChildren with the primary router-outlet, I’ve some problem with child routes. I resolved (http://stackoverflow.com/questions/39893428/auxiliary-router-outlet-inside-primary-router-outlet) but I don’t understand completely this behavior…
Do you tryied with lazy loading?
Hello, thank you for your great post.
I’m pretty much willing to do the same, except that I’d like to start on the speakers page.
What would you change in order to do this ?
Kind regards.
Very nice article! Thank you
Hi, great work!
I have a question related. How can I do to have the same router-outlet for my children and parent routes?
Regards
What is the use case for this? A router outlet has a 1 to 1 relationship with a component and so I need context before I answer.
Very well done. I’ve been looking for a while how to tie multiple routers with multiple modules, and was coming up empty entire reading your article.
Very good article – thank you so much!
Hi, thanks for this great article.
However, I would have a question. Say that in my root controller, I have two router-outlet. The primary one and an “aside” on. What I want to do is, for every route, be able to specify what component I load in the two router-outlet without having an URL such as “/foo(aside:bar)”. Indeed, having this allows the user to temper with the page, he could replace “bar” by something else and therefore have something else displayed, which I don’t want. The idea behind is to be able to compose the pages via routes configuration.
Thanks
Really interesting, now that I am reviewing the unnamed routing, to see how it can be also named. Thanks a lot for the effort. 😀
Hi Lukas – do you know if it is possible to navigate from one lazy loaded component in the primary router to a different lazy loaded component in the secondary named router? The use case is an invoice application (Module A) rendered in the primary router outlet where the user would click a button to bring up a Contact (Module B) in a named popup router outlet.
Thanks,
Deb
Terrific explanation. I’m trying to take it another level deeper: I want to have a child in an outlet that can have a route to grand-children in grand-child outlets. Is this possible. I haven’t been able to get that to work.
Thanks.
Thanks for this tutorial
A very well written article and easy to follow.
I have one question though. In this case, wouldn’t it be easier to have a ‘Speakers’ component which contains the ‘Speakers List’ component and the ‘Bio’ component. When you select a different speaker, just update the contents of the Bio component?
Hey Chris,
This is a very good question! We have used that pattern of container/presentational components many times with much success. What we have gained through using routes here is persistence, meaning that if we select a speaker to view their bio, we will still see that specific bio again when we refresh the page. This also users to deep link and share that specific bio in our app.
Ultimately, project requirements dictate which strategy to use, so pick the one that best suits what you are trying to accomplish.
#highfive
Hi,
I also using this auxiliary routing for my application. This article was really helpful for me. Is there a way to put alias for auxiliary routing.
e.g.
localhost:4200/speakers/(bio:6//list:speakerList)
instead of this can we put a alias for /speakers/(bio:6//list:speakerList) and make this router as,
localhost:4200/speakerList or something shorter
Hi, Lukas. I tried for several hours today to get named outlets on root routes to work (I want to click on a left-side tab bar and have the contents lazy-load into named outlets on hidden divs, then show the divs) but to no avail. Then I saw a link on stack overflow indicating that this may currently be a bug in Angular 4. I am very new to Angular – starting a training class tomorrow, in fact – and don’t know whether to blame Angular for my troubles or my own lack of knowledge. I think* I was doing it right but was wondering if you could comment on this post from stack overflow and whether you are currently seeing similar problems or it if works for you. Thanks so much. https://stackoverflow.com/questions/47482230/angular-4-lazy-loading-with-named-router-outlet-not-working