Table of Contents
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
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<RouterStateTitle> {
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<RouterStateTitle>;
}
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<RouterStateTitle>) => {
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';
<^>import { InboxComponent } from './inbox/inbox.component';<^>
const routes: Routes = [
{
path: 'gators',
component: GatorsComponent,
data: { title: <^>() => 'Alligators'<^> }
},
{
path: 'crocs',
component: CrocsComponent,
data: { title: <^>() => 'Crocodiles'<^> }
},
{
path: 'inbox',
component: InboxComponent,
data: {
// A dynamic title that shows the current notification count!
title: (ctx) => {
let t = 'Inbox';
if(ctx.notificationCount > 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<RouterStateTitle>, notificationCount: number) => {
// 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<^>!