Обновление заголовков с помощью Angular и ngrx
Development | Комментировать запись
С помощью сервиса 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