Resolve Guard In Angular

Resolve Guard In Angular

Before we go to a Route, we can use the Angular Resolve Guard or Angular Resolvers to load some data. In this article, we will learn what Angular Resolve guard is and how to use it in an Angular app.

Angular Resolve Guard

When we navigate to a route, Angular renders the Angular Component. The component will then make an HTTP call to the back-end server in order to retrieve data and present it to the user. This is usually done in the ngOnInit Life cycle hook.

The disadvantage of the above method is that the user will see an empty component. The component displays the data after it has arrived. One solution is to display some sort of loading indicator.

Another option is to employ the Resolve Guard to remedy the problem. Before traveling to the route, the Resolve Guard pre-fetches the data. As a result, the data and the component are rendered together.

How to Use Resolve Guard

We'll start by creating an Angular Service that implements the Resolve Interface.

The resolve method must be implemented by the service. A resolve method must either return an Observable<any>, a Promise<any>, or simply data. The Resolve interface has the following interface signature.

interface Resolve<T> {
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | T
}

We will get access to the ActivatedRouteSnapshot and RouterStateSnapshot from the Resolve method, which can be used to extract the values of router parameters, query parameters, and so on.

The following is a simple Angular Resolver example.

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot,RouterStateSnapshot } from '@angular/router';
import { ProductService } from './product.service';
import { Observable, of } from 'rxjs';
 
@Injectable()
export class ProductListResolveService implements Resolve<any>{
 
    constructor(private _router:Router , private 
     productService:ProductService ) {
    }
 
    resolve(route: ActivatedRouteSnapshot,
            state: RouterStateSnapshot): Observable<any> {
        
        return this.productService.getProducts();
    }
}

First, we import the @angular/router module's Resolve.

To inject the relevant services in the constructor, we can use Angular Dependency Injection.

Either an Observable<any>, a Promise<any>, or plain data must be returned by the resolve method. In the example above, we're calling the product services getProducts method, which returns an observable.

The resolver instance is not created by the router. Angular dependency injection is in charge of this task. As a result, we'll need to add it to the root module's Providers array.

providers: [ProductListResolveGuardService]

Following the creation of the resolver, we must edit the route specification and include the resolve property as seen below.

{ path: 'product', component: ProductComponent, resolve: {products: ProductListResolveService}  },

The resolve property is a key-value pair of resolvers in JavaScript. The user-defined variable is the key. A resolver service must be the value.

The key in the example above is products, and the resolver is ProductListResolveService. The ProductListResolveService's return value is assigned to the key, and products, and made available to the component via route data.

The angular looks for the resolve attribute of the route when the user navigates to the route product. The angular calls the resolve function for each key-value pair of resolvers. If the resolver's return value is an observable or a promise, the router will wait for it to finish. The value returned is assigned to the key products and added to the total.

constructor(private route: ActivatedRoute){
}
 
ngOnInit() {
   this.products=this.route.snapshot.data['products'];
}

Canceling Navigation

The router will cancel navigation if you return null from the resolver.

If an error occurs, the router will terminate the navigation.

Multiple Resolvers

You can specify several resolvers.

{ path: 'product', component: ProductComponent, 
    resolve: {products: ProductListResolveService, , data:SomeOtherResolverService}
}

Resolve Guard Example

Let's make a simple app that shows how to use Resolve guard.

The simple ProductService that obtains the hard-coded products is shown below.

Add the following code under the product.service.ts file:

import {Product} from './Product'
import { of, Observable, throwError} from 'rxjs';
import { delay, map } from 'rxjs/internal/operators';
 
export class ProductService{
 
    products: Product[];
    
    public constructor() {
        this.products=[
            new Product(1,'Memory Card',500),
            new Product(2,'Pen Drive',750),
            new Product(3,'Power Bank',100),
            new Product(4,'Computer',100),
            new Product(5,'Laptop',100),
            new Product(6,'Printer',100),
        ]
    }
 
    //Return Products List with a delay 
    public getProducts(): Observable<Product[]> {
        return of(this.products).pipe(delay(1500)) ;
    }
 
    // Returning Error
    // This wil stop the route from getting Activated
    //public getProducts(): Observable<Product[]> {
    //    return of(this.products).pipe(delay(1500), map( data => {
    //        throw throwError("errors occurred") ;        
    //    })) 
    //}
 
    public getProduct(id): Observable<Product> {
        var Product= this.products.find(i => i.productID==id)
        return of(Product).pipe(delay(1500)) ;
    }
}

Add the following code under the product.ts file:

export class Product { 
    constructor(productID:number,    name: string ,   price:number) 
   {
        this.productID=productID;
        this.name=name;
        this.price=price;
    }
 
    productID:number ;
    name: string ;
    price:number;
 
}

Using the RxJS operator, the getProducts() method returns an Observable of products. A 1500 ms delay has been added.

Similarly, after 1500 milliseconds, getProduct(id) provides the Product observable.

Product Component without resolver

Next, we'll create two components to display the Products list. Product1Component is the initial component, and it does not employ a resolver. The resolver is used by the second component, Product1Component.

This component uses the ProductService's getProducts() function to retrieve a list of Products.

Add the following code under the product1.component.ts file:

import { Component, OnInit } from '@angular/core';
 
import { ProductService } from './product.service';
import { Product } from './product';
import { ActivatedRoute } from '@angular/router';
 
@Component({
  templateUrl: './product1.component.html',
})
 
export class Product1Component
{
   public products:Product[];
   
   constructor(private route: ActivatedRoute,private productService:ProductService){
   }
 
   ngOnInit() {
       this.productService.getProducts().subscribe(data => {
         this.products=data;
      });
   }
 
}

Resolve Guard

The Resolve Interface is implemented by the ProductListResolverService service. The productService getProducts() is all that is returned.

Add the following code under the product.service.ts file:

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot,RouterStateSnapshot } from '@angular/router';
import { ProductService } from './product.service';
import { Observable, of } from 'rxjs';
import { Product } from './Product';
 
 
@Injectable()
export class ProductListResolverService implements Resolve<Product>{
 
    constructor(private productService:ProductService ) {
    }
 
    resolve(route: ActivatedRouteSnapshot,
            state: RouterStateSnapshot): Observable<any> {
 
        console.log("ProductListResover is called");
        return this.productService.getProducts();
    }
 
}

Product Component with resolver

Product2Component does not use the ProductService and instead obtains the product list from the Route data.

Add the following code under the product-list-resolver.service.ts file:

import { Component, OnInit } from '@angular/core';
 
import { ProductService } from './product.service';
import { Product } from './product';
import { ActivatedRoute } from '@angular/router';
 
@Component({
  templateUrl: './product2.component.html',
})
 
export class Product2Component
{
 
   public products:Product[];
   
   constructor(private route: ActivatedRoute,private productService:ProductService){
   }
 
   ngOnInit() {
      this.products=this.route.snapshot.data['products'];
   }
  
}

The resolve guard must be added to the route definition in the app.routing.module:

export const appRoutes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'contact', component: ContactComponent },
  { path: 'product1', component: Product1Component },
  { path: 'product2', component: Product2Component, resolve: {products: ProductListResolverService}  }
]

In AppModule, add the ProductListResolverService to the Providers array.

providers: [ProductService,ProductListResolverService],

Finally, run the application.

When you click on the Product1 link, you'll notice that the component loads, but the data takes a while to show.

After a brief delay, the component itself appears when you click on Product2. This is due to the fact that it waits for the resolver to complete. Along with the data, the Component renders.


Resolve Guard Example In Angular

ProductDetail Component

We can go on to construct Product2DetailComponent and a resolver that redirects us to Product2Component if the product isn't found.

Add the following code under the product-resolver.service.ts file:

import { Injectable } from '@angular/core';
import { Router, Resolve, ActivatedRouteSnapshot,RouterStateSnapshot } from '@angular/router';
import { ProductService } from './product.service';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/internal/operators';
import { Product } from './Product';
 
 
@Injectable()
export class ProductResolverService implements Resolve<any>{
 
    constructor(private router:Router , private productService:ProductService ) {
    }
 
    resolve(route: ActivatedRouteSnapshot,
           state: RouterStateSnapshot): any {
 
    let id = route.paramMap.get('id');
    console.log("ProductResolverService  called with "+id);
    return this.productService.getProduct(id)
        .pipe(map( data => {
            if (data) {
                console.log(data);
                return data;
            } else {
                console.log('redirecting');
                this.router.navigate(['/product2']);
                return null
            }
        }))
    }
}

Add the following code under the product2-detail.component.ts file:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
 
import { Product } from './product';
 
@Component({
  templateUrl: './product2-detail.component.html',
})
 
export class Product2DetailComponent
{
   product:Product;
  
   constructor(private _Activatedroute:ActivatedRoute){
      this.product=this._Activatedroute.snapshot.data['product'];
   }
 
}

We verify if the product exists in the resolver service, and if it doesn't, we redirect to the product list page and return null.

The following code is the complete code of the example.

Add the following code under the app.module.ts file:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, ErrorHandler } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
 
import { RouterModule } from '@angular/router';
 
import { AppComponent } from './app.component';
import { HomeComponent} from './home.component'
import { ContactComponent} from './contact.component'
 
import { ProductService } from './product.service';
import { ProductListResolverService } from './product-list-resolver.service';
import { ProductResolverService } from './product-resolver.service';
 
import { AppRoutingModule } from './app-routing.module';
 
import { Product1Component } from './product1.component';
import { Product1DetailComponent} from './product1-detail.component'
 
import { Product2Component } from './product2.component';
 
import { Product2DetailComponent } from './product2-detail.component';
 
@NgModule({
  declarations: [
    AppComponent,HomeComponent,ContactComponent,Product1Component, Product2Component,Product1DetailComponent, Product2DetailComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    RouterModule,
    AppRoutingModule
  ],
  providers: [ProductService,ProductListResolverService,ProductResolverService,],
  bootstrap: [AppComponent]
})
export class AppModule { }

Add the following code under the app.component.ts file:

import { Component } from '@angular/core';
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Routing Module - Route Guards Demo';
}

app.component.html
<div class="container">
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <div class="navbar-header">
      <a class="navbar-brand" [routerLink]="['/']"><strong> {{title}} </strong></a>
    </div>
    <ul class="nav navbar-nav">
        <li><a [routerLink]="['home']">Home</a></li>
        <li><a [routerLink]="['product1']">Product1</a></li>
        <li><a [routerLink]="['product2']">Product2</a></li>
        <li><a [routerLink]="['contact']">Contact us</a></li>
    </ul>
  </div>
</nav>
 <router-outlet></router-outlet>
</div>

Add the following code under the app.routing.module.ts file:

import { NgModule} from '@angular/core';
import { Routes,RouterModule } from '@angular/router';
 
import { HomeComponent} from './home.component'
import { ContactComponent} from './contact.component'
 
import { Product1Component} from './product1.component'
import { Product2Component} from './product2.component'
 
import { Product1DetailComponent} from './product1-detail.component'
import { ProductListResolverService } from './product-list-resolver.service';
import { Product2DetailComponent } from './product2-detail.component';
import { ProductResolverService } from './product-resolver.service';
 
 
export const appRoutes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'contact', component: ContactComponent },
  { path: 'product1', component: Product1Component },
  { path: 'product2', component: Product2Component, resolve: {products: ProductListResolverService}  },
  { path: 'product1/:id', component: Product1DetailComponent },
  { path: 'product2/:id', component: Product2DetailComponent, resolve:{product:ProductResolverService} },
  { path: '', redirectTo: 'home', pathMatch: 'full' },
];
 
@NgModule({
  declarations: [
  ],
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  providers: [],
  bootstrap: []
})
export class AppRoutingModule { }

Add the following code under the contact.component.ts file:

import {Component} from '@angular/core';
 
@Component({
     template: `<h1>Contact Us</h1>
                <p>TekarticlesHub </p>
                `
})
export class ContactComponent {
}

Add the following code under the home.components.ts file:

import {Component} from '@angular/core';
 
@Component({
    template: `<h1>Welcome!</h1>
              <p>This is Home Component </p>
             `
})
 
export class HomeComponent {
}

Add the following code under the product.ts file:

export class Product { 
 
    constructor(productID:number,    name: string ,   price:number) 
    {
        this.productID=productID;
        this.name=name;
        this.price=price;
    }
 
    productID:number ;
    name: string ;
    price:number;
 
}

Add the following code under the product.service.ts file:

import {Product} from './Product'
import { of, Observable, throwError} from 'rxjs';
import { delay, map } from 'rxjs/internal/operators';
 
export class ProductService{
 
    products: Product[];
    
    public constructor() {
        this.products=[
            new Product(1,'Memory Card',500),
            new Product(2,'Pen Drive',750),
            new Product(3,'Power Bank',100),
            new Product(4,'Computer',100),
            new Product(5,'Laptop',100),
            new Product(6,'Printer',100),
        ]
    }
 
    //Return Products List with a delay 
    public getProducts(): Observable<Product[]> {
        return of(this.products).pipe(delay(1500)) ;
    }
 
 
    // Returning Error
    // This wil stop the route from getting Activated
    //public getProducts(): Observable<any> {
    //    return of(this.products).pipe(delay(3000), map( data => {
    //        throw throwError("errors occurred") ;        
    //    })) 
    //}
 
    public getProduct(id): Observable<Product> {
        var Product= this.products.find(i => i.productID==id)
        return of(Product).pipe(delay(1500)) ;
    }
}

Add the following code under the product-list-resolver.ts file:

import { Injectable } from '@angular/core';
import { Router, Resolve, ActivatedRouteSnapshot,RouterStateSnapshot } from '@angular/router';
import { ProductService } from './product.service';
import { Observable, of } from 'rxjs';
 
@Injectable()
export class ProductListResolverService implements Resolve<any>{
 
    constructor(private productService:ProductService ) {
    }
 
    resolve(route: ActivatedRouteSnapshot,
            state: RouterStateSnapshot): Observable<any> {
        console.log("ProductListResover is called");
        return this.productService.getProducts();
    }
}

Add the following code under the product-resolver.service.ts file:

import { Injectable } from '@angular/core';
import { Router, Resolve, ActivatedRouteSnapshot,RouterStateSnapshot } from '@angular/router';
import { ProductService } from './product.service';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/internal/operators';
import { Product } from './Product';
 
@Injectable()
export class ProductResolverService implements Resolve<any>{
 
    constructor(private router:Router , private productService:ProductService ) {
    }
 
    resolve1(route: ActivatedRouteSnapshot,
            state: RouterStateSnapshot): Observable<any> {
 
        console.log("ProductResolverService  called");
        let id = route.paramMap.get('id');
        return this.productService.getProduct(id);
    }
 
    resolve(route: ActivatedRouteSnapshot,
           state: RouterStateSnapshot): any {
 
    let id = route.paramMap.get('id');
    console.log("ProductResolverService  called with "+id);
    return this.productService.getProduct(id)
        .pipe(map( data => {
            if (data) {
                console.log(data);
                return data;
            } else {
                console.log('redirecting');
                this.router.navigate(['/product2']);
                return null
            }
        }))
}
}

Add the following code under the product1.component.ts file:

import { Component, OnInit } from '@angular/core';
 
import { ProductService } from './product.service';
import { Product } from './product';
import { ActivatedRoute } from '@angular/router';
 
@Component({
  templateUrl: './product1.component.html',
})
 
export class Product1Component
{
 
   public products:Product[];
   
   constructor(private route: ActivatedRoute,private productService:ProductService){
   }
 
   ngOnInit() {
      console.log('ngOnInit');
 
      this.productService.getProducts().subscribe(data => {
         this.products=data;
      });
   }
}

Add the following code under the product1.component.html file:

<h1> Without Resolve</h1>
<div class='table-responsive'>
    <table class='table'>
        <thead>
            <tr>
                <th>Name</th>
                <th>Price</th>
            </tr>
        </thead>
        <tbody>
            <tr *ngFor="let product of products;">
                <td><a [routerLink]="['/product1',product.productID]">{{product.name}} </a> </td>
                <td>{{product.price}}</td>
            </tr>
        </tbody>
    </table>
</div>

Add the following code under the product1-detail.component.ts file:

import { Component } from '@angular/core';
import { Router,ActivatedRoute } from '@angular/router';
 
import { ProductService } from './product.service';
import { Product } from './product';
 
@Component({
  templateUrl: './product1-detail.component.html',
})
 
export class Product1DetailComponent
{
   product:Product;
 
   constructor(private _Activatedroute:ActivatedRoute,
               private _router:Router,
               private _productService:ProductService){
 
      let id=this._Activatedroute.snapshot.params['id'];
      console.log(id);
      this._productService.getProduct(id)
         .subscribe( data => { 
            this.product=data 
            console.log(this.product);
         })
   }
}

Add the following code under the product1-detail.component.html file:

<h1>Product Details Page [Without resolve]</h1>
 
Product : {{ product?.name}}  <br>
Price : {{product?.price}}

Add the following code under the product2.component.ts file.
import { Component, OnInit } from '@angular/core';
 
import { ProductService } from './product.service';
import { Product } from './product';
import { ActivatedRoute } from '@angular/router';
 
@Component({
  templateUrl: './product2.component.html',
})
export class Product2Component
{
   public products:Product[];
   
   constructor(private route: ActivatedRoute,private productService:ProductService){
   }
 
   ngOnInit() {
      this.products=this.route.snapshot.data['products'];
   }
  
}

Add the following code under the product2.component.html file:

<h1> With Resolve</h1>
<div class='table-responsive'>
    <table class='table'>
        <thead>
            <tr>
                <th>Name</th>
                <th>Price</th>
            </tr>
        </thead>
        <tbody>
            <tr *ngFor="let product of products;">
                <td><a [routerLink]="['/product2',product.productID]">{{product.name}} </a> </td>
                <td>{{product.price}}</td>
            </tr>
        </tbody>
    </table>
</div>

Add the following code under the product2-detail.component.ts file:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
 
import { Product } from './product';
 
@Component({
  templateUrl: './product2-detail.component.html',
})
export class Product2DetailComponent
{
   product:Product;
   constructor(private _Activatedroute:ActivatedRoute){
      this.product=this._Activatedroute.snapshot.data['product'];
   }
}

Add the following code under the product2-detail.component.html file:

<h1>Product Details Page [With resolve]</h1>
 
Product : {{ product?.name}}  <br>
Price : {{product?.price}}

Conclusion

In this article, we learned what Angular Resolve guard is and how to use it in an Angular app.

I hope this article helps you and you will like it.👍

If you have any doubt or confusion then free to ask in the comment section.

Post a Comment

Previous Post Next Post