17 - Router Store
In this section we examine the router store library from NgRx
1. Add a new presentational components for products
nx generate @nrwl/angular:component containers/product-list --project=products
CREATE libs/products/src/lib/containers/product-list/product-list.component.html
CREATE libs/products/src/lib/containers/product-list/product-list.component.spec.ts
CREATE libs/products/src/lib/containers/product-list/product-list.component.ts
CREATE libs/products/src/lib/containers/product-list/product-list.component.scss
2. Add material module to Products module
libs/products/src/lib/products.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { ProductsComponent } from './containers/products/products.component';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import {
productsReducer,
initialState as productsInitialState,
} from './+state/products.reducer';
import { ProductsEffects } from './+state/products.effects';
import { ProductListComponent } from './containers/product-list/product-list.component';
import { MaterialModule } from '@demo-app/material';
@NgModule({
imports: [
CommonModule,
MaterialModule,
RouterModule.forChild([{ path: '', component: ProductsComponent }]),
StoreModule.forFeature('products', productsReducer, {
initialState: productsInitialState,
}),
EffectsModule.forFeature([ProductsEffects]),
],
declarations: [ProductsComponent, ProductListComponent],
providers: [ProductsEffects],
})
export class ProductsModule {}
3. Add presentational list component onto the container components template
<div fxLayout="row" fxFlexLayout="center center">
<demo-app-product-list [products]="products$ | async"></demo-app-product-list>
</div>
4. Update presentational ProductList component to receive products as inputs
libs/products/src/lib/components/product-list/product-list.component.ts
import { Component, Input, EventEmitter } from '@angular/core';
import { Product } from '@demo-app/data-models';
@Component({
selector: 'demo-app-product-list',
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.scss'],
})
export class ProductListComponent {
@Input() products: Product[];
onFilter(category: string) {
this.filter.emit(category);
}
}
5. Add a material nav list to render a list of Products
libs/products/src/lib/components/product-list/product-list.component.ts
<mat-card>
<mat-form-field>
<mat-select placeholder="Category" (selectionChange)="onFilter($event.value)">
<mat-option [value]="'tools'">Tools</mat-option>
<mat-option [value]="'paint'">Paint</mat-option>
</mat-select>
</mat-form-field>
<mat-nav-list>
<a mat-list-item *ngFor="let product of products">{{product.name}}</a>
</mat-nav-list>
</mat-card>
6. Add material select to filter on category
Add on filter method to component to emit a filter
libs/products/src/lib/components/product-list/product-list.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Product } from '@demo-app/data-models';
@Component({
selector: 'demo-app-product-list',
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.scss'],
})
export class ProductListComponent {
@Input() products: Product[];
@Output() filter = new EventEmitter<string>();
onFilter(category: string) {
this.filter.emit(category);
}
}
Add filter listener to Products container components template
libs/products/src/lib/containers/products/products.component.html
<div fxLayout="row" fxFlexLayout="center center">
<demo-app-product-list
[products]="products$ | async"
(filter)="updateUrlFilters($event)"
></demo-app-product-list>
</div>
7. Update URL on filter change and no not dispatch a LoadProducts action from the Products component
lib\containers\products\products.component.ts
import { Component, OnInit } from '@angular/core';
import { ProductsState } from './../../+state/products.reducer';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Product } from '@demo-app/data-models';
import * as ProductsActions from '../../+state/products.actions';
import { productsQuery } from '../../+state/products.selectors';
import { NavigationExtras, Router } from '@angular/router';
@Component({
selector: 'demo-app-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.scss'],
})
export class ProductsComponent implements OnInit {
products$: Observable<Product[]>;
selectedProduct$: Observable<Product>;
constructor(private store: Store<ProductsState>, private router: Router) {}
ngOnInit() {
this.store.dispatch(ProductsActions.loadProducts());
this.products$ = this.store.pipe(select(productsQuery.getProducts));
this.selectedProduct$ = this.store.pipe(
select(productsQuery.getSelectedProduct)
);
}
updateUrlFilters(category: string): void {
const navigationExtras: NavigationExtras = {
replaceUrl: true,
queryParams: { category },
};
this.router.navigate([`/products`], navigationExtras);
}
}
8. Add an effect to listen for router actions
libs\products\src\lib\+state\products.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { ProductsService } from './../services/products/products.service';
import { ProductsActionTypes } from './../+state/products.actions';
import { mergeMap, map, catchError, filter } from 'rxjs/operators';
import * as ProductActions from './../+state/products.actions';
import { of } from 'rxjs';
import { Product } from '@demo-app/data-models';
import { ROUTER_NAVIGATION, RouterNavigationAction } from '@ngrx/router-store';
@Injectable()
export class ProductsEffects {
login$ = createEffect(() =>
this.actions$.pipe(
ofType(ProductsActionTypes.LoadProducts),
mergeMap(() =>
this.productService.getProducts().pipe(
map((products: Product[]) =>
ProductActions.loadProductsSuccess({ payload: products })
),
catchError((error) =>
of(ProductActions.loadProductsFailure({ error }))
)
)
)
)
);
loadFilteredProducts$ = createEffect(() => this.actions$.pipe(
ofType(ROUTER_NAVIGATION),
filter((r: RouterNavigationAction) =>
r.payload.routerState.url.startsWith('/products')
),
map(
(r: RouterNavigationAction) =>
r.payload.routerState.root.queryParams['category']
),
mergeMap((category: string) =>
this.productService.getProducts(category).pipe(
map(
(products: Product[]) =>
ProductActions.loadProductsSuccess({ payload: products })
),
catchError((error) => of(ProductActions.loadProductsFailure(error)))
)
)
));
constructor(
private actions$: Actions,
private productService: ProductsService
) {}
}
9. Update the Products service to use the new params of category
libs/products/src/lib/services/products/products.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Product } from '@demo-app/data-models';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class ProductsService {
constructor(private httpClient: HttpClient) {}
getProducts(category = null): Observable<Product[]> {
const url =
category !== null
? `http://localhost:3000/products?category=${category}`
: `http://localhost:3000/products`;
return this.httpClient.get<Product[]>(url);
}
}