Angular resolve Guard

By Arvind Rai, February 29, 2024
This page will walk through Angular resolve guard example. resolve guard is used in the scenario when we want to ensure whether there is data available or not before navigating to any route. If there is no data then it has no meaning to navigate there. Hence we have to resolve data before navigating to that route. Here comes the role of Angular resolve guard.
To create resolve guard, Angular provides
1. resolve property in Route interface that accepts a map of ResolveFn instnaces.
2. ResolveFn function signature to define route guard.
3. mapToResolve function to shorten the resolve guard code.
To create resolve guard, we need to create an injectable service with resolve function that returns Observable or Promise or a synchronous value. After creating injectable resolver guard class, configure it in route using resolve property. We can use mapToResolve that shorten the code to configure resolve guard.
Here on this page we will provide complete example to use resolve guard with Observable as well as Promise step-by-step.

ResolveFn

ResolveFn is a signature of function used as resolve guard on a Route. Find its signature from Angular doc.
type ResolveFn<T> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) 
      => Observable<T> | Promise<T> | T; 
The function arguments are ActivatedRouteSnapshot and RouterStateSnapshot and returns Observable or Promise or a synchronous value.

Suppose we have to display country list and on the click of any country, we need to display its detail on the basis of country id. If we want to resolve country detail before going to country detail route, we need to define a function in such a way that first we will fetch country detail by id and if there is the data then that country detail will be returned and if there is no data then we will navigate to previous route and return null.

ResolveFn with Observable

We will provide here how to use ResolveFn with Observable. We will discuss it step-by-step.
Step-1: Create an injectable class. Suppose we want to display country list and on the click of country, we will display country detail using country id. If there is no data for the country id then we want to remain on the same route. So we need to resolve country detail before navigating to country detail route. Here we have created CountryDetailResolver as following.
@Injectable()
export class CountryDetailResolver {
  constructor(private countryService: CountryService, private router: Router) { }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Country | null> {
    const id = route.paramMap.get('country-id') ?? '';
    console.log('Resolving for country id:' + id);

    return this.countryService.getCountry(id).pipe(
      map(country => {
        if (country) {
          return country;
        } else {
          this.router.navigate(['/country/countryList']);
          return null;
        }
      }));
  }
} 
In the above code, resolve is returning Observable<Country>. If country detail is available for the country id then country detail will be returned and if country detail is not available then we are navigating to previous route. In our service we have created a method that will return country object for the country id as Observable.
getCountry(id: string): Observable<Country | undefined> {
    return countriesObservable.pipe(
      map(countries => countries.find(country => country.countryId === id))
    );
} 
Step-2: We need to configure this resolver class in providers metadata of @NgModule decorator as following.
@NgModule({
   providers: [ CountryDetailResolver ],
   ------
})
export class CountryRoutingModule { } 
Step-3: Instantiate ResolveFn.
const countryResolver: ResolveFn<Country | null> = (
	route: ActivatedRouteSnapshot,
	state: RouterStateSnapshot,
) => inject(CountryDetailResolver).resolve(route, state); 
Step-4: Now we will configure our countryResolver in route configurations using resolve property.
const countryRoutes: Routes = [
	{
		path: '',
		component: CountryComponent,
		children: [
			{
				path: 'countryList',
				component: CountryListComponent,
				children: [
					{
						path: 'detail/:country-id',
						component: CountryDetailComponent,
						resolve: {
							countryDetail: countryResolver
						}
					}
				]
			}
		]
	}
]; 


ResolveFn with Promise

Here I will provide how to use resolve guard with Promise . Find the code step-by-step.
Step-1: Suppose we have person list and on the click of person we want to display person detail by person id. If there is no data for the person id then we want to remain on the same route. So we need to resolve person detail before navigating to person detail route. To resolve person detail we have created PersonDetailResolver as following.
@Injectable()
export class PersonDetailResolver implements Resolve<Person | null> {
  constructor(private personService: PersonService, private router: Router) { }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Person | null> {
    let id = route.paramMap.get('person-id') ?? '';
    console.log('Resolving for person id:' + id);

    return this.personService.getPerson(id).then(person => {
      if (person) {
        return person;
      } else {
        this.router.navigate(['/person/personList']);
        return null;
      }
    });
  }
} 
The method resolve is returning Promise . What we are doing here is that when we try to display person detail, first our resolver runs and resolves the person detail by person id before navigating to person detail route. If person is not found for the person id then we navigate to previous route and return null otherwise we return person detail object. In our service we have created a method that returns person object for the person id as Promise .
getPerson(id: string): Promise<Person | undefined> {
    return personsPromise
      .then(persons => persons.find(person => person.personId === id));
} 
Step-2: We need to configure this resolver class in providers metadata of @NgModule decorator as following.
@NgModule({
   providers: [ PersonDetailResolver ],
   ------
})
export class PersonRoutingModule { } 
Step-3: Instantiate ResolveFn.
const personResolver: ResolveFn<Person | null> = (
	route: ActivatedRouteSnapshot,
	state: RouterStateSnapshot,
) => inject(PersonDetailResolver).resolve(route, state); 
Step-4: Now we will configure our personResolver in route configurations using resolve property.
const personRoutes: Routes = [
	{
		path: '',
		component: PersonComponent,
		children: [
			{
				path: 'personList',
				component: PersonListComponent,
				children: [
					{
						path: 'detail/:person-id',
						component: PersonDetailComponent,
						resolve: {
							personDetail: personResolver
						}
					}
				]
			}
		]
	}
]; 

Using mapToResolve

mapToResolve function maps an injectable class containing resolve function to an equivalent ResolveFn.
To use mapToResolve, create an injectable class that must have function with name resolve that will act as resolve guard. In CountryDetailResolver service, we have a function named as resolve. Now we can directly use this class with mapToResolve to perform resolve guard.
path: 'countryList',
component: CountryListComponent,
children: [
	{
		path: 'detail/:country-id',
		component: CountryDetailComponent,
		resolve: {countryDetail: mapToResolve(CountryDetailResolver)}
	}
] 


Complete Example

country-detail.resolver.ts
import { Injectable } from '@angular/core';
import { Router, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { CountryService } from './services/country.service';
import { Country } from './country';

@Injectable()
export class CountryDetailResolver {
  constructor(private countryService: CountryService, private router: Router) { }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Country | null> {
    const id = route.paramMap.get('country-id') ?? '';
    console.log('Resolving for country id:' + id);

    return this.countryService.getCountry(id).pipe(
      map(country => {
        if (country) {
          return country;
        } else {
          this.router.navigate(['/country/countryList']);
          return null;
        }
      }));
  }
} 
country.service.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { Country } from '../country';

const COUNTRIES: Country[] = [
  { "countryId": "1", "name": "India", "capital": "New Delhi", "currency": "INR" },
  { "countryId": "2", "name": "China", "capital": "Beijing", "currency": "RMB" }
];
let countriesObservable = of(COUNTRIES);
let countriesToDisplayObservable =
  of(COUNTRIES.concat({ "countryId": "3", "name": "UK", "capital": "London", "currency": "GBP" }));

@Injectable()
export class CountryService {
  getCountries(): Observable<Country[]> {
    return countriesToDisplayObservable;
  }
  getCountry(id: string): Observable<Country | undefined> {
    return countriesObservable.pipe(
      map(countries => countries.find(country => country.countryId === id))
    );
  }
} 
country.ts
export interface Country { 
  countryId: string;
  name: string;
  capital: string;
  currency: string;
} 
country-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes, mapToResolve } from '@angular/router';
import { CountryComponent } from './country.component';
import { CountryListComponent } from './country-list/country.list.component';
import { CountryDetailComponent } from './country-list/detail/country.detail.component';
import { CountryDetailResolver } from './country-detail.resolver';

const countryRoutes: Routes = [
	{
		path: '',
		component: CountryComponent,
		children: [
			{
				path: 'countryList',
				component: CountryListComponent,
				children: [
					{
						path: 'detail/:country-id',
						component: CountryDetailComponent,
						resolve: { countryDetail: mapToResolve(CountryDetailResolver) }
					}
				]
			}
		]
	}
];

@NgModule({
	imports: [RouterModule.forChild(countryRoutes)],
	exports: [RouterModule],
	providers: [CountryDetailResolver]
})
export class CountryRoutingModule { } 
country.module.ts
import { NgModule }   from '@angular/core';
import { CommonModule }   from '@angular/common';
import { CountryComponent }  from './country.component';
import { CountryListComponent }  from './country-list/country.list.component';
import { CountryDetailComponent }  from './country-list/detail/country.detail.component';
import { CountryService } from './services/country.service';
import { CountryRoutingModule }  from './country-routing.module';

@NgModule({
  imports: [     
        CommonModule,
	CountryRoutingModule
  ], 
  declarations: [
	CountryComponent,
	CountryListComponent,
	CountryDetailComponent
  ],
  providers: [ CountryService ]
})
export class CountryModule { } 
country.component.ts
import { Component } from '@angular/core';

@Component({
  template: `<h2>Welcome to Country Home</h2>
  	     <div ngClass = "child-container">	
	       <router-outlet></router-outlet>	
  	     </div>
  `
})
export class CountryComponent { 
} 
country.list.component.html
<h3>Country List</h3>
<div *ngFor="let country of countries | async" ngClass= "sub-child-menu">
  <p> {{country.name}}
	| <a [routerLink]="['detail', country.countryId]" routerLinkActive="active">Detail</a>
  </p>
</div>
<div ngClass= "sub-child-container">
  <router-outlet></router-outlet>  
</div> 
country.list.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { CountryService } from '../services/country.service';
import { Country } from '../country';

@Component({
  templateUrl: './country.list.component.html'
})
export class CountryListComponent implements OnInit {
  countries: Observable<Country[]>;
  constructor(private countryService: CountryService) {
    this.countries = this.countryService.getCountries();
  }
  ngOnInit() {
  }
} 
country.detail.component.html
<h3>Country Details</h3>
<div *ngIf="country">
  <p><b>Id:</b> {{country.countryId}},  
  <b>Name:</b> {{country.name}} </p>
  <p><b>Capital:</b> {{country.capital}},
  <b>Currency:</b> {{country.currency}}</p>
</div> 
country.detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { switchMap } from 'rxjs/operators';
import { CountryService } from '../../services/country.service';
import { Country } from '../../country';

@Component({
    templateUrl: './country.detail.component.html'
})
export class CountryDetailComponent implements OnInit {
    country = {} as Country | undefined;
    constructor(
        private countryService: CountryService,
        private route: ActivatedRoute) { }
    ngOnInit() {
        this.route.params.pipe(
            switchMap((params: Params) => this.countryService.getCountry(params['country-id']))
        ).subscribe(country => this.country = country);
    }
} 
Find the code for person feature.
person-detail.resolver.ts
import { Injectable } from '@angular/core';
import { Router, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { PersonService } from './services/person.service';
import { Person } from './person';

@Injectable()
export class PersonDetailResolver {
  constructor(private personService: PersonService, private router: Router) { }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Person | null> {
    let id = route.paramMap.get('person-id') ?? '';
    console.log('Resolving for person id:' + id);

    return this.personService.getPerson(id).then(person => {
      if (person) {
        return person;
      } else {
        this.router.navigate(['/person/personList']);
        return null;
      }
    });
  }
} 
person.service.ts
import { Injectable } from '@angular/core';
import { Person } from '../person';

const PERSONS: Person[] = [
  { "personId": "1", "name": "Mahesh", "city": "Varanasi" },
  { "personId": "2", "name": "Ram", "city": "Ayodhya" }
];
let personsPromise = Promise.resolve(PERSONS);
let personsToDisplayPromise =
  Promise.resolve(PERSONS.concat({ "personId": "3", "name": "Krishn", "city": "Mathura" }));

@Injectable()
export class PersonService {
  getPersons(): Promise<Person[]> {
    return personsToDisplayPromise;
  }
  getPerson(id: string): Promise<Person | undefined> {
    return personsPromise
      .then(persons => persons.find(person => person.personId === id));
  }
} 
person.ts
export interface Person { 
  personId: string;
  name: string;
  city: string;
} 
person-routing.module.ts
import { NgModule, inject } from '@angular/core';
import { ActivatedRouteSnapshot, ResolveFn, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { PersonComponent } from './person.component';
import { PersonListComponent } from './person-list/person.list.component';
import { PersonDetailComponent } from './person-list/detail/person.detail.component';
import { PersonDetailResolver } from './person-detail.resolver';
import { Person } from './person';

const personResolver: ResolveFn<Person | null> = (
	route: ActivatedRouteSnapshot,
	state: RouterStateSnapshot,
) => inject(PersonDetailResolver).resolve(route, state);

const personRoutes: Routes = [
	{
		path: '',
		component: PersonComponent,
		children: [
			{
				path: 'personList',
				component: PersonListComponent,
				children: [
					{
						path: 'detail/:person-id',
						component: PersonDetailComponent,
						resolve: {
							personDetail: personResolver
						}
					}
				]
			}
		]
	}
];

@NgModule({
	imports: [RouterModule.forChild(personRoutes)],
	exports: [RouterModule],
	providers: [PersonDetailResolver]
})
export class PersonRoutingModule { } 
person.module.ts
import { NgModule }   from '@angular/core';
import { CommonModule }   from '@angular/common';
import { PersonComponent }  from './person.component';
import { PersonListComponent }  from './person-list/person.list.component';
import { PersonDetailComponent }  from './person-list/detail/person.detail.component';
import { PersonService } from './services/person.service';
import { PersonRoutingModule }  from './person-routing.module';

@NgModule({
  imports: [     
       CommonModule,
       PersonRoutingModule
  ], 
  declarations: [
       PersonComponent,
       PersonListComponent,
       PersonDetailComponent
  ],
  providers: [ PersonService ]
})
export class PersonModule { } 
person.component.ts
import { Component } from '@angular/core';

@Component({
	template: `
	  <h2>Welcome to Person Home</h2>
	  <div ngClass = "child-container">	
	     <router-outlet></router-outlet>	
	  </div>
  `
})
export class PersonComponent { 
} 
person.list.component.html
<h3>Person List</h3>
<div *ngFor="let person of persons | async" ngClass= "sub-child-menu">
  <p>{{person.name}} | 
  <a [routerLink]="['detail', person.personId]" routerLinkActive="active">Detail</a>
</div>
<div ngClass= "sub-child-container">
   <router-outlet></router-outlet>  
</div> 
person.list.component.ts
import { Component, OnInit } from '@angular/core';
import { PersonService } from '../services/person.service';
import { Person } from '../person';

@Component({
  templateUrl: './person.list.component.html'
})
export class PersonListComponent implements OnInit {
  persons: Promise<Person[]>;
  constructor(private personService: PersonService) {
    this.persons = this.personService.getPersons();
  }
  ngOnInit() {
  }
} 
person.detail.component.html
<h3>Person Details</h3>
<div *ngIf="person">
    <p><b>Id:</b> {{person.personId}},  
    <b>Name:</b> {{person.name}} </p>
    <p><b>City:</b> {{person.city}} </p>
</div> 
person.detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { switchMap } from 'rxjs/operators';
import { PersonService } from '../../services/person.service';
import { Person } from '../../person';

@Component({
  templateUrl: './person.detail.component.html'
})
export class PersonDetailComponent implements OnInit {
  person = {} as Person | undefined;
  constructor(
    private personService: PersonService,
    private route: ActivatedRoute) { }

  ngOnInit() {
    this.route.params.pipe(
      switchMap((params: Params) => this.personService.getPerson(params['person-id']))
    ).subscribe(person => this.person = person);
  }
} 
Now find the code for other files used in the example.
page-not-found.component.ts
import { Component } from '@angular/core';
import { Location } from '@angular/common';

@Component({
  template: `<h2>Page Not Found.</h2>
             <div>
                <button (click)="goBack()">Go Back</button>
	     </div>
     `
})
export class PageNotFoundComponent {
    constructor(private location: Location) { }
	goBack(): void {
        this.location.back();
    }
} 
app.component.ts
import { Component } from '@angular/core';
@Component({
  selector: 'app-root',
  template: `
	   <nav ngClass = "parent-menu">
	     <ul>
		<li><a routerLink="/country/countryList" routerLinkActive="active">Country</a></li>
		<li><a routerLink="/person/personList" routerLinkActive="active">Person</a></li>
	     </ul> 
	   </nav>  
		
	   <router-outlet></router-outlet>	
  `
})
export class AppComponent { 
} 
app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PageNotFoundComponent } from './page-not-found.component';

const routes: Routes = [
	{
		path: '',
		redirectTo: '/country/countryList',
		pathMatch: 'full'
	},
	{
		path: 'country',
		loadChildren: () => import('./country/country.module').then(m => m.CountryModule)
	},
	{
		path: 'person',
		loadChildren: () => import('./person/person.module').then(m => m.PersonModule)
	},
	{
		path: '**',
		component: PageNotFoundComponent
	}
];

@NgModule({
	imports: [
		RouterModule.forRoot(routes)
	],
	exports: [
		RouterModule
	]
})
export class AppRoutingModule { } 
app.module.ts
import { NgModule }   from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }  from './app.component';
import { PageNotFoundComponent }  from './page-not-found.component';
import { AppRoutingModule }  from './app-routing.module';

@NgModule({
  imports: [     
    BrowserModule,
    AppRoutingModule,
  ],
  declarations: [
    AppComponent,
    PageNotFoundComponent
  ],
  providers: [ 
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { } 

Run Application

Download source code using download link given below on this page and run the application.
Angular resolve Guard Example
In our data store there is the detail available for the country India and China but not for UK. So when we click on detail button for India and China, we will go to country detail route and will see corresponding country detail. But when we click on detail button for UK, we remain on same route. This is because of Angular resolve guard.

References

Download Source Code

POSTED BY
ARVIND RAI
ARVIND RAI
LEARN MORE







©2024 concretepage.com | Privacy Policy | Contact Us