Обновление заголовков с помощью Angular и ngrx

С помощью сервиса Angular под названием Title можно легко обновить HTMLTitleElement. Довольно часто маршруты в SPA имеют разные заголовки. Обычно это делается вручную в жизненном цикле ngOnInit компонента маршрута. Однако в этом руководстве мы сделаем это декларативно, используя возможности @ngrx/router-store с пользовательским RouterStateSerializer и @ngrx/effects.

План такой:

  • Поместить свойство title в данные определения маршрута.
  • Использовать @ngrx/store, чтобы отслеживать состояние приложения.
  • Использовать @ngrx/router-store с пользовательским RouterStateSerializer, чтобы добавить желаемый заголовок в состояние приложения.
  • Создать эффект updateTitle с помощью @ngrx/effects для обновления HTMLTitleElement при каждом изменении маршрута.

Настройка проекта

Для быстрой и простой настройки мы будем использовать @angular/cli.

# установите @angular-cli, если не сделали этого ранее
npm install @angular/cli -g
# создайте пример с маршрутизацией
ng new title-updater --routing

Определение маршрутов

Давайте создадим пару компонентов:

ng generate component gators
ng generate component crocs

И обновим их маршруты (в файле 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'}
  }
];

Обратите внимание на свойство title в каждом определении маршрута: оно будет использоваться для обновления HTMLTitleElement.

Управление состоянием

@ngrx – очень удобная библиотека для управления состоянием приложения. В нашем примере мы будем использовать @ngrx/router-store для сериализации маршрутизатора в @ngrx/store, чтобы отслеживать изменения маршрута и соответствующим образом обновлять заголовок.

Примечание: Мы будем применять @ngrx версии 4.0+ для поддержки нового RouterStateSerializer

Установим все необходимое:

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

Создайте пользовательский RouterStateSerializer, чтобы добавить желаемый заголовок в состояние (в файле 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 };

Определите редуктор маршрутизатора в 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
};

Каждый раз, когда @ngrx/store отправляет действие (StoreRouterConnectingModule отправляет действия навигации маршрутизатора), редуктор должен обработать это действие и соответствующим образом обновить состояние. Выше мы определили состояние приложения, чтобы правильно настроить свойство маршрутизатора и сохранять там сериализованное состояние маршрутизатора с помощью CustomRouterStateSerializer.

Остался последний шаг – связать все это в файле 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: [
    /**

Настройка @ngrx/effect

Теперь @ngrx/store будет иметь нужный заголовок. Все, что нам нужно сделать, чтобы обновить заголовки, – это прослушать действия ROUTER_NAVIGATION и использовать заголовок в состоянии. Мы можем использовать для этого @ngrx/effects.

Установите @ngrx/effects:

npm install @ngrx/effects --save

Откройте 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);
    });

И теперь подключите эффект updateTitle, импортировав его с помощью EffectsModule.forRoot – когда модуль будет создан, он начнет прослушивать эффект, подписавшись на все @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';

Вот и все! Теперь вы можете определять заголовки в определениях маршрутов, и они будут автоматически обновляться при изменении маршрутов!

От статики к динамике

В большинстве случаев статические заголовки отлично работают. Но что делать, если вы хотите приветствовать пользователя по имени или показывать счетчик уведомлений? В таком случае можно изменить свойство title в данных маршрута, чтобы оно было функцией, принимающей контекст.

Давайте рассмотрим такой условный пример, если бы в store было значение notificationCount (в файле 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: {
    // динамический заголовок, отображающий текущее количество уведомлений
    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 {
  // обновляет заголовок каждый раз при изменении маршрута или контекста, извлекая значение notificationCount из store.
  @Effect({ dispatch: false })
  updateTitle$ = this.actions
    .ofType(ROUTER_NAVIGATION)
    .combineLatest(this.store.select(getNotificationCount),
      (action: RouterNavigationAction<RouterStateTitle>, notificationCount: number) => {
        // контекст, который мы сделаем доступным для функций заголовков, чтобы они могли использовать его по своему усмотрению.
        const ctx = { notificationCount };
        this.titleService.setTitle(action.payload.routerState.title(ctx));
    });

Теперь, когда маршрут папки Inbox загружен, пользователь может видеть количество полученных уведомлений, которое обновляется в режиме реального времени.

Читайте также: Анализ приложений Angular с помощью webpack

Добавить комментарий