Tutorial

Declarative Title Updater with Angular and ngrx

Published on November 13, 2017
author

Mark P. Kennedy

Declarative Title Updater with Angular and ngrx

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:

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:

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:

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:

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:

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:

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:

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;
    }
  }
}
];

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!

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Mark P. Kennedy

author

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.

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


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!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.