Updating the <^>HTMLTitleElement<^> is easy with Angular's Title service. It is pretty common for each route in a SPA to have a different title. This is often done manually in the <^>ngOnInit<^> lifecycle of the route's component. However, in this post we will do it in a declarative way using the power of the <^>@ngrx/router-store<^> with a custom <^>RouterStateSerializer<^> and <^>@ngrx/effects<^>.

The concept is as follows:

  • Have a <^>title<^> property in a route definition's <^>data<^>.
  • Use <^>@ngrx/store<^> to keep track of the application state.
  • Use <^>@ngrx/router-store<^> with a custom <^>RouterStateSerializer<^> to add the desired <^>title<^> to the application state.
  • Create an <^>updateTitle<^> effect using <^>@ngrx/effects<^> to update the <^>HTMLTitleElement<^> every time the route changes.

Project Setup

title illustration for: Project Setup

For a quick and easy setup, we will be using the <^>@angular/cli<^>.

				
					
# Install @angular-cli if you don't already have it

npm install @angular/cli -g



# Create the example with routing

ng new title-updater --routing

				
			

Defining Some Routes

Create a couple components:

				
					
ng generate component gators

ng generate component crocs

				
			

And define their routes:

				
					
[label title-updater/src/app/app-routing.module.ts]

import { NgModule } from '@angular/core';

import { Routes, RouterModule } from '@angular/router';

import { GatorsComponent } from './gators/gators.component';

import { CrocsComponent } from './crocs/crocs.component';



const routes: Routes = [

 {

 path: 'gators',

 component: GatorsComponent,

 data: { title: 'Alligators'}

 },

 {

 path: 'crocs',

 component: CrocsComponent,

 data: { title: 'Crocodiles'}

 }

];



				
			

Notice the <^>title<^> property in each route definition, it will be used to update the <^>HTMLTitleElement<^>.

Add State Management

<^>@ngrx<^> is a great library to manage application state. For this example application we will use <^>@ngrx/router-store<^> to serialize the router into the <^>@ngrx/store<^> so we can listen for route changes and update the title accordingly.

We will be using <^>@ngrx<^> > 4.0 to leverage the new <^>RouterStateSerializer<^>

Install:

				
					
npm install @ngrx/store @ngrx/router-store --save

				
			

Create a custom <^>RouterStateSerializer<^> to add the desired title to the state:

				
					
[label title-updater/src/app/shared/utils.ts]

import { RouterStateSerializer } from '@ngrx/router-store';

import { RouterStateSnapshot } from '@angular/router';



export interface RouterStateTitle {

 title: string;

}

export class CustomRouterStateSerializer

 implements RouterStateSerializer&lt;RouterStateTitle&gt; {

 serialize(routerState: RouterStateSnapshot): RouterStateTitle {

 let childRoute = routerState.root;

 while (childRoute.firstChild) {

 childRoute = childRoute.firstChild;

 }

// Use the most specific title

const title = childRoute.data['title'];

return { title };

				
			

Define the router reducer:

				
					
[label title-updater/src/app/reducers/index.ts]

import * as fromRouter from '@ngrx/router-store';

import { RouterStateTitle } from '../shared/utils';

import { createFeatureSelector } from '@ngrx/store';



export interface State {

 router: fromRouter.RouterReducerState&lt;RouterStateTitle&gt;;

}

export const reducers = {

 router: fromRouter.routerReducer

};



				
			

Every time the <^>@ngrx/store<^> dispatches an action (router navigation actions are sent by the <^>StoreRouterConnectingModule<^>), a reducer needs to handle that action and update the state accordingly. Above we define our application state to have a router property and to keep the serialized router state there using the <^>CustomRouterStateSerializer<^>.

One last step is needed to hook it all up:

				
					
[label title-updater/src/app/app.module.ts]

import { NgModule } from '@angular/core';

import { BrowserModule } from '@angular/platform-browser';

import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';

import { StoreModule } from '@ngrx/store';



import { AppRoutingModule } from './app-routing.module';

import { AppComponent } from './app.component';

import { CrocsComponent } from './crocs/crocs.component';

import { GatorsComponent } from './gators/gators.component';

import { reducers } from './reducers/index';

import { CustomRouterStateSerializer } from './shared/utils';

@NgModule({

 declarations: [

 AppComponent,

 CrocsComponent,

 GatorsComponent

 ],

 imports: [

 BrowserModule,

 AppRoutingModule,

 StoreModule.forRoot(reducers),

StoreRouterConnectingModule

 ],

 providers: [

 /**



				
			

Sprinkle in the Magic @ngrx/effect

Now when we switch routes, our <^>@ngrx/store<^> will have the title we want. To update the title all we have to do now is listen for <^>ROUTER_NAVIGATION<^> actions and use the title on the state. We can do this with <^>@ngrx/effects<^>.

Install:

				
					
npm install @ngrx/effects --save

				
			

Create the effect:

				
					
[label title-updater/src/app/effects/title-updater.ts]

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

import { Actions, Effect } from '@ngrx/effects';

import { ROUTER_NAVIGATION, RouterNavigationAction } from '@ngrx/router-store';

import 'rxjs/add/operator/do';

import { RouterStateTitle } from '../shared/utils';



@Injectable()

export class TitleUpdaterEffects {

 @Effect({ dispatch: false })

 updateTitle$ = this.actions

 .ofType(ROUTER_NAVIGATION)

 .do((action: RouterNavigationAction&lt;RouterStateTitle&gt;) =&gt; {

 this.titleService.setTitle(action.payload.routerState.title);

 });



				
			

Finally, hookup the <^>updateTitle<^> effect by importing it with <^>EffectsModule.forRoot<^>, this will start listening for the effect when the module is created by subscribing to all <^>@Effect()s<^>:

				
					
[label title-updater/src/app/app.module.ts]

import { NgModule } from '@angular/core';

import { BrowserModule } from '@angular/platform-browser';

import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';

import { StoreModule } from '@ngrx/store';



import { AppRoutingModule } from './app-routing.module';

import { AppComponent } from './app.component';

import { CrocsComponent } from './crocs/crocs.component';

import { GatorsComponent } from './gators/gators.component';

import { reducers } from './reducers/index';

import { CustomRouterStateSerializer } from './shared/utils';

import { EffectsModule } from '@ngrx/effects';

import { TitleUpdaterEffects } from './effects/title-updater';



				
			

And that's it! You can now define <^>titles<^> in route definitions and they will automatically be updated when the route changes!

Going Further, from Static to Dynamic ⚡️

Static titles are great for most use cases, but what if you wanted to welcome a user by name or display a notification count as well? We can modify the <^>title<^> property in route data to be a function that accepts a context.

Here is a potential example if <^>notificationCount<^> was on the store:

				
					
[label title-updater/src/app/app-routing.module.ts]

import { NgModule } from '@angular/core';

import { Routes, RouterModule } from '@angular/router';

import { GatorsComponent } from './gators/gators.component';

import { CrocsComponent } from './crocs/crocs.component';

&lt;^&gt;import { InboxComponent } from './inbox/inbox.component';&lt;^&gt;



const routes: Routes = [

 {

 path: 'gators',

 component: GatorsComponent,

 data: { title: &lt;^&gt;() =&gt; 'Alligators'&lt;^&gt; }

 },

 {

 path: 'crocs',

 component: CrocsComponent,

 data: { title: &lt;^&gt;() =&gt; 'Crocodiles'&lt;^&gt; }

 },

 {

 path: 'inbox',

 component: InboxComponent,

 data: {

 // A dynamic title that shows the current notification count!

 title: (ctx) =&gt; {

 let t = 'Inbox';

 if(ctx.notificationCount &gt; 0) {

 t += (${ctx.notificationCount});

 }

 return t;

 }

 }

}

];



				
			
				
					
[label title-updater/src/app/effects/title-updater.ts]

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

import { Actions, Effect } from '@ngrx/effects';

import { ROUTER_NAVIGATION, RouterNavigationAction } from '@ngrx/router-store';

import { Store } from '@ngrx/store';

import 'rxjs/add/operator/combineLatest';

import { getNotificationCount } from '../selectors.ts';

import { RouterStateTitle } from '../shared/utils';



@Injectable()

export class TitleUpdaterEffects {

 // Update title every time route or context changes, pulling the notificationCount from the store.

 @Effect({ dispatch: false })

 updateTitle$ = this.actions

 .ofType(ROUTER_NAVIGATION)

 .combineLatest(this.store.select(getNotificationCount),

 (action: RouterNavigationAction&lt;RouterStateTitle&gt;, notificationCount: number) =&gt; {

 // The context we will make available for the title functions to use as they please.

 const ctx = { notificationCount };

 this.titleService.setTitle(action.payload.routerState.title(ctx));

 });



				
			

Now when the <^>Inbox<^> route is loaded, the user can see their notification count that is updated real-time as well! 💌

🚀 Continue to experiment and explore custom <^>RouterStateSerializers<^> and <^>@ngrx<^>!