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.
Table of contents
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); }
}
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
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']));
}
}
Thank you for reading!