Dreams Do Come True! Named Router Outlets in Angular 2

Named Outlets

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.

Victor Quote

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 Code

Routing 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.

Outlet

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.

Lukas Outlet

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>

Victor Outlet

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

Leave a Comment