Angular resolve Guard Example
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.
Contents
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;
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 useResolveFn
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; } })); } }
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)) ); }
providers
metadata of @NgModule
decorator as following.
@NgModule({ providers: [ CountryDetailResolver ], ------ }) export class CountryRoutingModule { }
ResolveFn
.
const countryResolver: ResolveFn<Country | null> = ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot, ) => inject(CountryDetailResolver).resolve(route, state);
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 useresolve
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; } }); } }
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)); }
providers
metadata of @NgModule
decorator as following.
@NgModule({ providers: [ PersonDetailResolver ], ------ }) export class PersonRoutingModule { }
ResolveFn
.
const personResolver: ResolveFn<Person | null> = ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot, ) => inject(PersonDetailResolver).resolve(route, state);
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.tsimport { 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; } })); } }
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)) ); } }
export interface Country { countryId: string; name: string; capital: string; currency: string; }
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 { }
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 { }
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 { }
<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>
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() { } }
<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>
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); } }
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; } }); } }
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)); } }
export interface Person { personId: string; name: string; city: string; }
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 { }
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 { }
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 { }
<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>
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() { } }
<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>
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); } }
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(); } }
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 { }
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 { }
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 { }