Todd Motto

Todd Motto

Owner, Ultimate Angular

Dynamic page titles in Angular 2 with router events
Nov 17, 2016
5 mins read
Edit post

Updating page titles in AngularJS (1.x) was a little problematic and typically was done via a global $rootScope property that listened for route change events to fetch the current route and map across a static page title. In Angular (v2+), the solution is far easier as it provides a single API, however we can actually tie this API into route change events to dynamically update the page titles.

Title Service

In Angular, we can request the Title from platform-browser (we’re also going to import the router too):

import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router';

Once imported, we can inject them both:

@Component({
  selector: 'app-root',
  templateUrl: `
    <div>
      Hello world!
    </div>
  `
})
export class AppComponent {
  constructor(private router: Router, private titleService: Title) {}
}

To use the titleService, we must check out the source:

export class Title {
  /**
   * Get the title of the current HTML document.
   * @returns {string}
   */
  getTitle(): string { return getDOM().getTitle(); }

  /**
   * Set the title of the current HTML document.
   * @param newTitle
   */
  setTitle(newTitle: string) { getDOM().setTitle(newTitle); }
}

So we have two methods, getTitle and setTitle, easy enough!

The Title class is currently experimental, so if it changes I’ll update this post.

To update a page title statically, we can simply call setTitle like so:

@Component({...})
export class AppComponent implements OnInit {
  constructor(private router: Router, private titleService: Title) {}
  ngOnInit() {
    this.titleService.setTitle('My awesome app');
  }
}

One thing I liked about ui-router in AngularJS was the ability to add a custom data: {} Object to each route, which could be inherited down the chain of router states:

// AngularJS 1.x + ui-router
.config(function ($stateProvider) {
  $stateProvider
    .state('about', {
      url: '/about',
      component: 'about',
      data: {
        title: 'About page'
      }
    });
});

In Angular we can do the exact same however we need to add some custom logic around route changes to get it working. First, assume the following routes in a pseudo-calendar application:

const routes: Routes = [{
  path: 'calendar',
  component: CalendarComponent,
  children: [
    { path: '', redirectTo: 'new', pathMatch: 'full' },
    { path: 'all', component: CalendarListComponent },
    { path: 'new', component: CalendarEventComponent },
    { path: ':id', component: CalendarEventComponent }
  ]
}];

Here we have a base path /calendar with the opportunity to hit three child URLs, /all to view all calendar entries as a list, /new to create a new calendar entry and a unique /:id which can accept unique hashes to correspond with user data on the backend. Now, we can add some page title information under a data Object:

const routes: Routes = [{
  path: 'calendar',
  component: CalendarComponent,
  children: [
    { path: '', redirectTo: 'new', pathMatch: 'full' },
    { path: 'all', component: CalendarListComponent, data: { title: 'My Calendar' } },
    { path: 'new', component: CalendarEventComponent, data: { title: 'New Calendar Entry' } },
    { path: ':id', component: CalendarEventComponent, data: { title: 'Calendar Entry' } }
  ]
}];

That’s it. Now back to our component!

Routing events

The Angular router is great for setting up basics, but it’s also extremely powerful in supporting routing events, through Observables.

Note: we’re using the AppComponent because it’s the root component, therefore will always be subscribing to all route changes.

To subscribe to the router’s events, we can do this:

ngOnInit() {
  this.router.events
    .subscribe((event) => {
      // example: NavigationStart, RoutesRecognized, NavigationEnd
      console.log(event);
    });
}

The way that we can check which events are the ones we need, ideally NavigationEnd, we can do this:

this.router.events
  .subscribe((event) => {
    if (event instanceof NavigationEnd) {
      console.log('NavigationEnd:', event);
    }
  });

This is a fine approach, but because the Angular router is reactive, we’ll implement more logic using RxJS, let’s import:

import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';

Now we’ve added filter, map and mergeMap to our router Observable, we can filter out any events that aren’t NavigationEnd and continue the stream if so:

this.router.events
  .filter((event) => event instanceof NavigationEnd)
  .subscribe((event) => {
    console.log('NavigationEnd:', event);
  });

Secondly, because we’ve injected the Router class, we can access the routerState:

this.router.events
  .filter((event) => event instanceof NavigationEnd)
  .map(() => this.router.routerState.root)
  .subscribe((event) => {
    console.log('NavigationEnd:', event);
  });

However, as a perhaps better alternative to accessing the routerState.root directly, we can inject the ActivatedRoute into the class:

import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';

@Component({...})
export class AppComponent implements OnInit {
  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute,
    private titleService: Title
  ) {}
  ngOnInit() {
    // our code is in here
  }
}

So let’s rework that last example:

this.router.events
  .filter((event) => event instanceof NavigationEnd)
  .map(() => this.activatedRoute)
  .subscribe((event) => {
    console.log('NavigationEnd:', event);
  });

By returning a new Object into our stream (this.activatedRoute) we essentially swap what we’re observing - so at this point we are only running the .map() should the filter() successfully return us the event type of NavigationEnd.

Now comes the interesting part, we’ll create a while loop to traverse over the state tree to find the last activated route, and then return it to the stream:

this.router.events
  .filter((event) => event instanceof NavigationEnd)
  .map(() => this.activatedRoute)
  .map((route) => {
    while (route.firstChild) route = route.firstChild;
    return route;
  })
  .subscribe((event) => {
    console.log('NavigationEnd:', event);
  });

Doing this allows us to essentially dive into the children property of the routes config to fetch the corresponding page title(s). After this, we want two more operators:

this.router.events
  .filter((event) => event instanceof NavigationEnd)
  .map(() => this.activatedRoute)
  .map((route) => {
    while (route.firstChild) route = route.firstChild;
    return route;
  })
  .filter((route) => route.outlet === 'primary')
  .mergeMap((route) => route.data)
  .subscribe((event) => {
    console.log('NavigationEnd:', event);
  });

Now our titleService just needs implementing:

.subscribe((event) => this.titleService.setTitle(event['title']));

Now we have a fully working piece of code that updates the page title based on route changes. You can check the full source below.

Final code

import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';

import { Component, OnInit } from '@angular/core';
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';

@Component({...})
export class AppComponent implements OnInit {
  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute,
    private titleService: Title
  ) {}
  ngOnInit() {
    this.router.events
      .filter((event) => event instanceof NavigationEnd)
      .map(() => this.activatedRoute)
      .map((route) => {
        while (route.firstChild) route = route.firstChild;
        return route;
      })
      .filter((route) => route.outlet === 'primary')
      .mergeMap((route) => route.data)
      .subscribe((event) => this.titleService.setTitle(event['title']));
  }
}
Nov 16, 2016

Updating Angular 2 Forms with patchValue or setValue

Setting model values in Angular (v2+) can be done in a few different ways, however...

Dec 6, 2016

Angular 1.6 is here, this is what you need to know

AngularJS 1.6 was just released! Here’s the low down on what to expect for the...