Войти Зарегистрироваться
Войти Зарегистрироваться
04.01.2018 в 15:44 Блог

Декларативное обновление заголовка с Angular и ngrx

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

Источник

Концепция такова:

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

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

Для быстрой и простой настройки мы будем использовать @angle/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

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

Создайте пару компонент:

ng generate component gators
ng generate component crocs

И добавьте их роуты:

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

@NgModule({
 imports: [RouterModule.forRoot(routes)],
 exports: [RouterModule]
})
export class AppRoutingModule { }

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

Добавляем State Manager

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

Мы будем использовать @ngrx> 4.0 для использования нового RouterStateSerializer

Установка:

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

Создайте собственный RouterStateSerializer, чтобы добавить желаемый заголовок в состояние:

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

 // Использовать наиболее специвичный title
 const title = childRoute.data['title'];
 return { title };
 }
}

Добавляем 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
};

export const getRouterState = createFeatureSelector<fromRouter.RouterReducerState<RouterStateTitle>>('router');

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

Последний шаг необходим, чтобы перехватить все это:

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: [
 / **
* «RouterStateSnapshot», предоставляемый «Router», представляет собой очень сложную структуру.
* Пользовательский RouterStateSerializer используется для анализа `RouterStateSnapshot`, предоставленного
* в `@ngrx/router-store`, чтобы включить только нужные фрагменты состояния название.
* /
{ provide: RouterStateSerializer, useClass: CustomRouterStateSerializer }
 ],
 bootstrap: [AppComponent]
})
export class AppModule { }

Добавим магии через @ngrx/effect

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

Установка:

npm install @ngrx/effects --save

Создаем эффект:

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

 constructor(private actions: Actions,
 private titleService: Title) {}
}

Наконец, подключите эффект updateTitle, импортировав его с помощью EffectsModule.forRoot, это начнет прослушивать эффект, когда модуль будет создан, подписавшись на все @Effect():

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';

@NgModule({
 declarations: [
 AppComponent,
 CrocsComponent,
 GatorsComponent
 ],
 imports: [
 BrowserModule,
 AppRoutingModule,
 StoreModule.forRoot(reducers),
 StoreRouterConnectingModule,
 EffectsModule.forRoot([TitleUpdaterEffects])
 ],
 providers: [
 { provide: RouterStateSerializer, useClass: CustomRouterStateSerializer }
 ],
 bootstrap: [AppComponent]
})
export class AppModule { }

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

От статичных заголовоков к динамическим

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

Вот потенциальный пример, если notificationCount был в 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: {
 // Динамическое название, отображающее текущее количество уведомлений!
 title: (ctx) => {
 let t = 'Inbox';
 if(ctx.notificationCount > 0) {
 t += ` (${ctx.notificationCount})`;
 }
 return t;
 }
 }
}
];

@NgModule({
 imports: [RouterModule.forRoot(routes)],
 exports: [RouterModule]
})
export class AppRoutingModule { }

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 {

 // Обновление заголовка при каждом изменении маршрута или контекста, вытягивая уведомление из хранилища.
 @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));
 });

 constructor(private actions: Actions,
 private store: Store,
 private titleService: Title) {}
}

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

Автор Дмитрий Цирульников
Коментарии (0)
Для того что-бы оставить комментарий, авторизуйтесь