Tutorial

Declarative Title Updater with Angular and ngrx

Angular

While this tutorial has content that we believe is of great benefit to our community, we have not yet tested or edited it to ensure you have an error-free learning experience. It's on our list, and we're working on it! You can help us out by using the "report an issue" button at the bottom of the tutorial.

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!

0 Comments

Creative Commons License