Tech Incent
Angular

How to implement angular router resolver?

router resolver

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>

Related posts

Angular Production-Ready Project Setup

Sajal Mia

Angular Pure Bootstrap 5 Sidenav

Sajal Mia

Dockerize angular app with example

Sajal Mia

Create forms using angular FormBulder

Sajal Mia

Implement angular (product) details page with reactive service

Sajal Mia

Explained RxJs switchMap operator with example

Sajal Mia