24. Router store
In this section we will use NgRx's router store library to listen to url or router events as actions in our effects.
1. npm i @ngrx/router-store
npm i @ngrx/router-store
2. Add to AppModule
Add StoreRouterConnectingModule
to the AppModule.
Often it is needed to create your own router serilaizer but we will skip this step.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { EffectsModule } from '@ngrx/effects';
import { StoreRouterConnectingModule } from '@ngrx/router-store';
import { HomeComponent } from './home/containers/home/home.component';
import { AppComponent } from './app.component';
import { InMemoryDataService } from './app.db';
import { reducer } from './state/spinner/spinner.reducer';
import { environment } from '../environments/environment.prod';
@NgModule({
declarations: [AppComponent, HomeComponent],
imports: [
BrowserModule,
RouterModule.forRoot([
{ path: '', pathMatch: 'full', redirectTo: 'home' },
{ path: 'home', component: HomeComponent },
{ path: 'event', loadChildren: './event/event.module#EventModule' }
]),
HttpClientModule,
HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, {
delay: 1000
}),
StoreModule.forRoot({ spinner: reducer }),
EffectsModule.forRoot([]),
StoreDevtoolsModule.instrument({
name: 'NgRx Demo App',
logOnly: environment.production
}),
StoreRouterConnectingModule.forRoot(),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
3. Add a select box to EventComponent to update URL
src/app/event/event.component.html
<app-add-attendee (addAttendee)="addAttendee($event)"></app-add-attendee>
<select name="Guests filter" (change)="navigate($event.target.value)">
<option [value]="'all'">All</option>
<option [value]="'withGuests'">With Guests</option>
<option [value]="'withoutGuests'">Without Guests</option>
</select>
<app-event-list *ngIf="!(spinner$ | async)" [attendees]="attendees$ | async"></app-event-list>
4. Add navigate method to component
Add a navigate method to the component to update the URL.
Delete dispatched LoadAttendees action.
src/app/event/event.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Store, select } from '@ngrx/store';
import { Attendee } from '../../../models';
import { EventService } from '../../services/event.service';
import {
StartSpinner,
StopSpinner
} from '../../../state/spinner/spinner.actions';
import { getSpinner } from '../../../state/spinner/spinner.selectors';
import {
LoadAttendees,
AddAttendee
} from '../../state/attendees/attendees.actions';
import { EventState } from '../../state';
import { getAttendees } from '../../state/attendees/attendees.selectors';
import { Router } from '@angular/router';
@Component({
selector: 'app-event',
templateUrl: './event.component.html',
styleUrls: ['./event.component.scss']
})
export class EventComponent implements OnInit {
spinner$: Observable<boolean>;
attendees$: Observable<Attendee[]>;
constructor(
private store: Store<EventState>,
private eventService: EventService,
private router: Router
) {}
ngOnInit() {
this.attendees$ = this.store.pipe(select(getAttendees));
}
addAttendee(attendee: Attendee) {
this.store.dispatch(new AddAttendee(attendee));
}
navigate(filterBy: string) {
this.router.navigateByUrl(`/event?filterBy=${filterBy}`);
}
}
5. Add next action creator for filterBy property
Add new AttendeesActionTypes
for FilterBy
.
Add new class for FilterBy
Add new AttendeesActions
for FilterBy
.
src/app/event/state/attendees/attendees.actions.ts
import { Action } from '@ngrx/store';
import { Attendee } from '../../../models';
export enum AttendeesActionTypes {
LoadAttendees = '[Attendees Page] Load Attendees',
LoadAttendeesSuccess = '[Attendees Page] Load Attendees Success',
LoadAttendeesFail = '[Attendees Page] Load Attendees Fail',
AddAttendee = '[Attendee Page] Add Attendee',
AddAttendeeSuccess = '[Attendee API] Add Attendee Success',
AddAttendeeFail = '[Attendee API] Add Attendee Fail',
FilterBy = '[Attendee Page] FilterBy'
}
export class LoadAttendees implements Action {
readonly type = AttendeesActionTypes.LoadAttendees;
}
export class LoadAttendeesSuccess implements Action {
readonly type = AttendeesActionTypes.LoadAttendeesSuccess;
constructor(public payload: Attendee[]) {}
}
export class LoadAttendeesFail implements Action {
readonly type = AttendeesActionTypes.LoadAttendeesFail;
constructor(public payload: any) {}
}
export class AddAttendee implements Action {
readonly type = AttendeesActionTypes.AddAttendee;
constructor(public payload: Attendee) {}
}
export class AddAttendeeSuccess implements Action {
readonly type = AttendeesActionTypes.AddAttendeeSuccess;
constructor(public payload: Attendee) {}
}
export class AddAttendeeFail implements Action {
readonly type = AttendeesActionTypes.AddAttendeeFail;
constructor(public payload: any) {}
}
export class FilterBy implements Action {
readonly type = AttendeesActionTypes.FilterBy;
constructor(public payload: string) {}
}
export type AttendeesActions =
| FilterBy
| AddAttendee
| AddAttendeeSuccess
| AddAttendeeFail
| LoadAttendees
| LoadAttendeesSuccess
| LoadAttendeesFail;
6. Add an effect to listen to router events
Add an effect listening to RouterEvents
that begin with /events
.
src/app/event/state/attendees.effects.ts
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { ofType } from '@ngrx/effects';
import { switchMap, map, catchError, filter, tap } from 'rxjs/operators';
import { of } from 'rxjs';
import { EventService } from '../../services/event.service';
import {
AttendeesActionTypes,
LoadAttendees,
LoadAttendeesSuccess,
LoadAttendeesFail,
AddAttendee,
AddAttendeeSuccess,
AddAttendeeFail,
FilterBy
} from './attendees.actions';
import { Attendee } from '../../../models';
import { ROUTER_NAVIGATION } from '@ngrx/router-store';
import { RouterNavigationAction } from '@ngrx/router-store';
@Injectable()
export class AttendeesEffects {
constructor(private actions$: Actions, private eventService: EventService) {}
@Effect()
getAttendees$ = this.actions$.pipe(
ofType(AttendeesActionTypes.LoadAttendees),
switchMap((action: LoadAttendees) =>
this.eventService.getAttendees().pipe(
map((attendees: Attendee[]) => new LoadAttendeesSuccess(attendees)),
catchError(error => of(new LoadAttendeesFail(error)))
)
)
);
@Effect()
addAttendee$ = this.actions$.pipe(
ofType(AttendeesActionTypes.AddAttendee),
switchMap((action: AddAttendee) =>
this.eventService.addAttendee(action.payload).pipe(
map((attendee: Attendee) => new AddAttendeeSuccess(attendee)),
catchError(error => of(new AddAttendeeFail(error)))
)
)
);
@Effect()
loadDiaryHealthActions$ = this.actions$.pipe(
ofType(ROUTER_NAVIGATION),
map((r: RouterNavigationAction) => ({
url: r.payload.routerState.url,
filterBy: r.payload.routerState.root.queryParams['filterBy']
})),
filter(({ url, filterBy }) => url.startsWith('/event')),
map(({ filterBy }) => new FilterBy(filterBy))
);
}
7. Update reducer to have new filterBy
Add switch case to set filterBy property.
src/app/event/state/attendees/attendees.reducer.ts
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
import { AttendeesActions, AttendeesActionTypes } from './attendees.actions';
import { Attendee } from '../../../models';
export interface State extends EntityState<Attendee> {
loading: boolean;
error: any;
filterBy: string;
}
const adapter: EntityAdapter<Attendee> = createEntityAdapter<Attendee>();
export const intitalState: State = adapter.getInitialState({
loading: false,
error: null,
filterBy: 'all'
});
export function reducer(state = intitalState, action: AttendeesActions): State {
switch (action.type) {
case AttendeesActionTypes.LoadAttendees: {
return adapter.removeAll({
...state,
loading: false,
error: null
});
}
case AttendeesActionTypes.LoadAttendeesSuccess: {
return adapter.addAll(action.payload, {
...state,
loading: false,
error: null
});
}
case AttendeesActionTypes.LoadAttendeesFail: {
return adapter.removeAll({
...state,
loading: false,
error: action.payload
});
}
case AttendeesActionTypes.AddAttendeeSuccess: {
return adapter.addOne(action.payload, { ...state, error: null });
}
case AttendeesActionTypes.AddAttendeeFail: {
return { ...state, error: action.payload };
}
case AttendeesActionTypes.FilterBy: {
return { ...state, filterBy: action.payload };
}
default: {
return state;
}
}
}
export const {
selectIds,
selectEntities,
selectAll,
selectTotal
} = adapter.getSelectors();
8. Add new selector for filtering attendees.
Add selector to get filterBY state property.
Use two getAttendees and getFilterBy to filter the list.
src/app/event/state/attendees/attendees.selectors.ts
---------- ABBREVIATED CODE SNIPPET ----------
export const getFilterBy = createSelector(
getAttendeeState,
state => state.filterBy
);
export const getFilteredAttendees = createSelector(
getAttendees,
getFilterBy,
(attendees, filterBy) =>
attendees.filter(
attendee =>
filterBy === 'all'
? true
: filterBy === 'withGuests'
? attendee.guests >= 1
: attendee.guests === 0
)
);
---------- ABBREVIATED CODE SNIPPET ----------
9. Use new selector in EventComponent
Swap getAttendees selector for getFilteredAttendees selector.
src/app/event/containers/event/event.component.ts
---------- ABBREVIATED CODE SNIPPET ----------
ngOnInit() {
this.attendees$ = this.store.pipe(select(getFilteredAttendees));
this.store.dispatch(new LoadAttendees());
}
---------- ABBREVIATED CODE SNIPPET ----------
StackBlitz Link