In this article, I will implement an angular router resolver with an explanation. So we will create two component product list and production description. Product List will have a product list resolver and the other will be a product description resolver which takes the product slug from the current route param.
So what is an angular resolver?
Angular resolver is an injectable service class that gives pre-data before component init. Resolver used for pre-fetching data after angular route end.
So how is an angular resolver implemented?
An angular resolver implements by Implementing an angular Resolve interface. and angular Resolve takes two parameters, ActivatedRouteSnapshot, and other RouterStateSnapshot
export declare interface Resolve<T> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | T; }
Note: In this tutorial, we will make some HTTP calls. So we will use storerestapi prototype, Store Rest Api provide fake store data
Let’s start the angular resolver tutorial
Step 01. Generate an angular project and set up
A new angular project is generated with SCSS style and with @angular/router, You can generate other style options like CSS or LESS. Actually, I always use SCSS for styling.
ng new angular-router-resolver --style=scss --routing
Go to project directory angular-router-resolver
cd angular-router-resolver
Edit the app.component.html file to add router-outlet for multiple routes
<h1>This Is Angular Router Resolver</h1> <ul> <li><a [routerLink]="'/'">Home</a></li> <li><a [routerLink]="'/products'">Products</a></li> </ul> <router-outlet></router-outlet>
Step 02. Generate some necessary router component
Generate ProductList and ProductDescription Component.
ng g c product/productList ng g c product/productDescription
add product list and product description to our app routing, In th app-routing.module.ts file
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { ProductDescriptionComponent } from './product/product-description/product-description.component'; import { ProductListComponent } from './product/product-list/product-list.component'; const routes: Routes = [ { path: 'products', pathMatch: 'full', component: ProductListComponent }, { path: 'products/:slug', component: ProductDescriptionComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
Step 03. Generate Product Service and implement product list and product description HTTP methods
Generate Product Service inside the product directory
ng g s product/product --skip-tests
edit the product.service.ts file, and implement getProducts and getProduct methods
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class ProductService { baseUrl: string = 'https://api.storerestapi.com/v1' constructor(private http: HttpClient) { } getProducts(): Observable<any> { return this.http.get(this.baseUrl + '/products') } getProduct(slug: string): Observable<any> { return this.http.get(this.baseUrl + '/products/' + slug) } }
Add HttpClientModule for use of HttpClient class. In the app.module.ts file
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpClientModule } from '@angular/common/http'; ... @NgModule({ ... imports: [ ... HttpClientModule ], ... }) export class AppModule { }
Step 04. Implement product-list resolver
Generate product-list resolver inside product/resolver directory
ng g resolver product/resolver/product-list --skip-tests
Implement ProductListResolver class to return product list observable
import { Injectable } from '@angular/core'; import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { map, Observable, of } from 'rxjs'; import { ProductService } from '../product.service'; @Injectable({ providedIn: 'root' }) export class ProductListResolver implements Resolve<any> { constructor(private productService: ProductService) { } resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> { return this.productService.getProducts().pipe(map(res => res?.data)); } }
Add Product-list resolver to ProductList route
In the app-routing.component.ts file.
... import { ProductListResolver } from './product/resolver/product-list.resolver'; const routes: Routes = [ { path: 'products', component: ProductListComponent, pathMatch: 'full', resolve: { products: ProductListResolver } }, ... ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
Step 05. Access Product List data from ProductList Component
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.scss'] }) export class ProductListComponent implements OnInit { data: any; constructor(private route: ActivatedRoute) { } ngOnInit(): void { console.log(this.route.snapshot.data?.products) this.data = this.route.snapshot.data; } }
Render ProductList data to HTML, Edit product/product-list/product-list.component.html file
<h2>Product List</h2> <ul> <li *ngFor="let product of data?.products"> <a [routerLink]="'/products/' + product?.slug">{{product?.title}}</a> </li> </ul>
Step 06. Implement product resolver
In the product resolver, we will take the product slug from the route param
So Generate a product solver inside product/resolver directory
ng g resolver product/resolver/product
Implement Product Resolver
import { Injectable } from '@angular/core'; import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { map, Observable, of } from 'rxjs'; import { ProductService } from '../product.service'; @Injectable({ providedIn: 'root' }) export class ProductResolver implements Resolve<any> { constructor(private productService: ProductService) { } resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> { return this.productService.getProduct(route.params['slug']).pipe(map(res => res?.data)); } }
Add Product Resolve to the product description route
... import { ProductResolver } from './product/resolver/product.resolver'; const routes: Routes = [ ... { path: 'products/:slug', component: ProductDescriptionComponent, resolve: { product: ProductResolver } } ]; ... export class AppRoutingModule { }
Access Product data from the product description component, In the product-description.component.ts file
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-product-description', templateUrl: './product-description.component.html', styleUrls: ['./product-description.component.scss'] }) export class ProductDescriptionComponent implements OnInit { product: any; constructor(private route: ActivatedRoute) { } ngOnInit(): void { this.product = this.route.snapshot.data['product'] } }
Render product in product description HTML file
<h1>Product Description</h1> <div *ngIf="product != null"> <h2>Title: {{product?.title}}</h2> <h3>Price: {{product?.price}}</h3> </div>
Handling Resolver Api Error
You can handle error via RxJs catchError operator,
import { Injectable } from '@angular/core'; import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { catchError, map, Observable, of } from 'rxjs'; import { ProductService } from '../product.service'; @Injectable({ providedIn: 'root' }) export class ProductResolver implements Resolve<any> { constructor(private productService: ProductService) { } resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> { return this.productService.getProduct(route.params['slug']).pipe( map(res => res?.data), catchError(() => { return of("Product is not found!") }) ); } }
In the case of an error, I will show an alert error message and route it to the product list, and for resolver return EMPTY
import { Injectable } from '@angular/core'; import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { catchError, EMPTY, map, Observable, of } from 'rxjs'; import { ProductService } from '../product.service'; @Injectable({ providedIn: 'root' }) export class ProductResolver implements Resolve<any> { constructor(private productService: ProductService, private router: Router) { } resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> { return this.productService.getProduct(route.params['slug']).pipe( map(res => res?.data), catchError(() => { // return of("Product is not found!") alert("Product Not Found") this.router.navigate(['/products']) return EMPTY }) ); } }
Implement Resolver base Loader
import { Component, OnInit } from '@angular/core'; import { ResolveEnd, ResolveStart, Router } from '@angular/router'; import { filter, mapTo, merge, Observable } from 'rxjs'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { title = 'angular-router-resolver'; // Custom Loader Trigger isLoading$!: Observable<boolean>; constructor(private router: Router) { } ngOnInit() { // Custom Loader Trigger const loaderStart$ = this.router.events.pipe( filter((event) => event instanceof ResolveStart), mapTo(true) ) const loaderEnd$ = this.router.events.pipe( filter((event) => event instanceof ResolveEnd), mapTo(false) ) this.isLoading$ = merge(loaderStart$, loaderEnd$) } }
Add loader in HTML
<svg *ngIf="isLoading$ | async" width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#000"> <g fill="none" fill-rule="evenodd"> <g transform="translate(1 1)" stroke-width="2"> <circle stroke-opacity=".5" cx="18" cy="18" r="18"/> <path d="M36 18c0-9.94-8.06-18-18-18"> <animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="1s" repeatCount="indefinite"/> </path> </g> </g> </svg>