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:
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
Create a couple components:
ng generate component gators
ng generate component crocs
And define their routes:
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.
@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:
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:
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:
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: [
/**
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:
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:
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!
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:
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;
}
}
}
];
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!
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!