Angular canDeactivate Guard Example
February 28, 2024
This page will walk through Angular canDeactivate
route guard example. The canDeactivate
is a property of Route
interface that is assigned with an array of CanDeactivateFn
instances. CanDeactivateFn
is a signature of function that performs as canDeactivate
route guard. We can create an injectable service containing a method named as canDeactivate
that will be injected during the navigation process. While deactivating route using canDeactivate
guard, we need to open a confirmation dialog box to take user confirmation if user wants to stay on the page or leave the page. To shorten the code for creating canDeactivate
guard, we can use Angular mapToCanDeactivate
function.
The
canDeactivate
guard can be used in the scenario where a user is changing form data and before saving user tries to navigate away. In this scenario we can use canDeactivate
guard to deactivate the route and open a Dialog Box to take user confirmation.
Here on this page we will create
canDeactivate
guard that can be used with any component and we will also create component specific canDeactivate
guard. Now let us discuss complete example step-by-step.
Contents
1. Technologies Used
Find the technologies being used in our example.1. Angular 17.0.0
2. Node.js 20.10.0
3. NPM 10.2.3
2. CanDeactivateFn
CanDeactivateFn
is a signature of function to create route guard that decides if a route can be deactivated. Find the signature of CanDeactivateFn
from Angular doc.
type CanDeactivateFn<T> = ( component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot ) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree }
a. component : Component for which route is to be deactivated.
b. currentRoute : Instance of
ActivatedRouteSnapshot
for current route.
c. currentState : Instance of
RouterStateSnapshot
for current state.
d. nextState : Instance of
RouterStateSnapshot
for next state.
Returns :
CanDeactivateFn
function will return Observable<boolean>
or Promise<boolean>
or boolean
. If it returns true, route can be deactivated otherwise not.
3. Steps to Create canDeactivate Guard for any Component
To usecanDeactivate
guard in our application, create an injectable service and define canDeactivate()
method. If a component needs canDeactivate
route guard then that component has also to create a method named as canDeactivate()
.
Find the sample output of our application for
CanDeactivate
guard.
canDeactivate
in our application.
3.1 Create Service for CanDeactivate Guard
First we will create an interface that will declarecanDeactivate
method and using this interface we will create a service that will act as canDeactivate
guard. This service will define canDeactivate
method.
can-deactivate-guard.service.ts
export interface CanComponentDeactivate { canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean; } @Injectable() export class CanDeactivateGuard { canDeactivate(component: CanComponentDeactivate, route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { let url: string = state.url; console.log('Url: '+ url); return component.canDeactivate ? component.canDeactivate() : true; } }
canDeactivate
method whose return type is Observable<boolean>
or Promise<boolean>
or boolean
. In the service code, we need to call canDeactivate
method using component instance. For any component which has defined canDeactivate
method, that method will be called by component instnace otherwise true will be returned by the above guard. We can access current route and current state, too. Our CanDeactivateGuard
service can be used for any component.
3.2 Configure CanDeactivate Guard Service in Application Routing Module
We will configure ourCanDeactivateGuard
service in application routing module using providers
attribute of @NgModule
decorator. We need to add it in main application routing module so that Router
can inject it during the navigation process.
import { CanDeactivateGuard } from './can-deactivate-guard.service'; ------ @NgModule({ ------ providers: [ CanDeactivateGuard ] }) export class AppRoutingModule { }
3.3 Create Service for Confirmation Dialog Box
Create a service for Confirmation Dialog Box.dialog.service.ts
@Injectable() export class DialogService { confirm(message?: string): Observable<boolean> { const confirmation = window.confirm(message || 'Are you sure?'); return Observable.of(confirmation); }; }
Observable
. We need to configure the above service in application module using providers
attribute of @NgModule
decorator.
3.4 Create canDeactivate() method within Component
We have created a service forcanDeactivate
guard .i.e. CanDeactivateGuard
and it can be used for any component. Suppose we have a component as PersonEditComponent
that will perform update operation for person details. If we want to use CanDeactivate
guard for this component then we need to create canDeactivate()
method within this component.
@Component({ templateUrl: './person.edit.component.html' }) export class PersonEditComponent implements OnInit { ------ constructor( public dialogService: DialogService) { } canDeactivate(): Observable<boolean> | boolean { if (!this.isUpdating && this.personForm.dirty) { return this.dialogService.confirm('Discard changes for Person?'); } return true; } }
canDeactivate()
of this component will be called by CanDeactivateGuard
service. The canDeactivate()
method of the above component will open a Dialog Box to ask if we want to stay on the route or navigate.
3.5 Add canDeactivate Guard to Component Route
We need to add ourCanDeactivate
guard .i.e. CanDeactivateGuard
to component route in routing module using canDeactivate
attribute.
const personRoutes: Routes = [ { path: 'person', component: PersonComponent, children: [ { path: '', component: PersonListComponent, children: [ { path: ':id', component: PersonEditComponent, canDeactivate: [ ( component: CanComponentDeactivate, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot ) => inject(CanDeactivateGuard).canDeactivate(component, currentRoute, currentState) ] } ] } ] } ];
CanDeactivate
guard in our application for any component.
3.6 Using mapToCanDeactivate
mapToCanDeactivate
maps an array of injectable classes containing method named as canDeactivate to an array of equivalent CanDeactivateFn
. The mapToCanDeactivate
is helpful to shorten the code writing to create canDeactivate route guard.
Let us use
CanDeactivateGuard
service with mapToCanDeactivate
function.
path: '', component: PersonListComponent, children: [ { path: ':id', component: PersonEditComponent, canDeactivate: mapToCanDeactivate([CanDeactivateGuard]) } ]
mapToCanDeactivate
forces us to create a method named as canDeactivate
in our injectable CanDeactivateGuard
service otherwise code will not compile.
4. Component-Specific canDeactivate Guard
We can create component specificcanDeactivate
guard. Suppose we have a component CountryEditComponent
and we want to create canDeactivate
guard for this component.
country-edit-can-deactivate-guard.service.ts
@Injectable() export class CountryEditCanDeactivateGuard { constructor(private dialogService: DialogService) { } canDeactivate( component: CountryEditComponent, route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean> | boolean { const url: string = state.url; console.log('Url: '+ url); if (!component.isUpdating && component.countryForm.dirty) { component.isUpdating = false; return this.dialogService.confirm('Discard changes for Country?'); } return true; } }
canDeactivate()
method, we need to open our Confirmation Dialog Box.
We will configure
CountryEditCanDeactivateGuard
in main application routing module so that Router
can inject it during the navigation process.
import { CanDeactivateGuard } from './can-deactivate-guard.service'; import { CountryEditCanDeactivateGuard } from './country-edit-can-deactivate-guard.service'; ------ @NgModule({ ------ providers: [ CanDeactivateGuard, CountryEditCanDeactivateGuard ] }) export class AppRoutingModule { }
CountryEditComponent
route in routing module using canDeactivate
attribute.
path: 'list', component: CountryListComponent, children: [ { path: 'edit/:country-id', component: CountryEditComponent, canDeactivate: mapToCanDeactivate([CountryEditCanDeactivateGuard]) } ]
CanDeactivate
guard in our application for a specific component.
5. Complete Example
can-deactivate-guard.service.tsimport { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; export interface CanComponentDeactivate { canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean; } @Injectable() export class CanDeactivateGuard { canDeactivate( component: CanComponentDeactivate, route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { const url: string = state.url; console.log('Url: ' + url); return component.canDeactivate ? component.canDeactivate() : true; } }
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { CountryEditComponent } from './country/country-list/edit/country.edit.component'; import { DialogService } from './dialog.service'; @Injectable() export class CountryEditCanDeactivateGuard { constructor(private dialogService: DialogService) { } canDeactivate( component: CountryEditComponent, route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean> | boolean { const url: string = state.url; console.log('Url: ' + url); if (!component.isUpdating && component.countryForm.dirty) { component.isUpdating = false; return this.dialogService.confirm('Discard changes for Country?'); } return true; } }
import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; @Injectable() export class DialogService { confirm(message?: string): Observable<boolean> { const confirmation = window.confirm(message || 'Are you sure?'); return of(confirmation); }; }
import { Component } from '@angular/core'; @Component({ template: `<h2>Welcome to Country Home</h2> <nav [ngClass] = "'child-menu'"> <ul> <li><a [routerLink]="['add']" routerLinkActive="active">Add Country</a></li> <li><a [routerLink]="['list']" routerLinkActive="active">Country List</a></li> </ul> </nav> <div [ngClass] = "'child-container'"> <router-outlet></router-outlet> </div> ` }) export class CountryComponent { }
export class Country { constructor(public countryId:number, public name:string, public capital:string, public currency:string) { } }
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { CountryComponent } from './country.component'; import { AddCountryComponent } from './add-country/add-country.component'; import { CountryListComponent } from './country-list/country.list.component'; import { CountryEditComponent } from './country-list/edit/country.edit.component'; import { CountryService } from './services/country.service'; import { CountryRoutingModule } from './country-routing.module'; @NgModule({ imports: [ CommonModule, ReactiveFormsModule, CountryRoutingModule ], declarations: [ CountryComponent, AddCountryComponent, CountryListComponent, CountryEditComponent ], providers: [ CountryService ] }) export class CountryModule { }
import { NgModule, inject } from '@angular/core'; import { ActivatedRouteSnapshot, RouterModule, RouterStateSnapshot, Routes, mapToCanDeactivate } from '@angular/router'; import { CountryComponent } from './country.component'; import { CountryListComponent } from './country-list/country.list.component'; import { AddCountryComponent } from './add-country/add-country.component'; import { CountryEditComponent } from './country-list/edit/country.edit.component'; import { CanComponentDeactivate, CanDeactivateGuard } from '../can-deactivate-guard.service'; import { CountryEditCanDeactivateGuard } from '../country-edit-can-deactivate-guard.service'; const countryRoutes: Routes = [ { path: 'country', component: CountryComponent, children: [ { path: 'add', component: AddCountryComponent, canDeactivate: [ ( component: CanComponentDeactivate, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot ) => inject(CanDeactivateGuard).canDeactivate(component, currentRoute, currentState) ] }, { path: 'list', component: CountryListComponent, children: [ { path: 'edit/:country-id', component: CountryEditComponent, canDeactivate: mapToCanDeactivate([CountryEditCanDeactivateGuard]) } ] } ] } ]; @NgModule({ imports: [RouterModule.forChild(countryRoutes)], exports: [RouterModule] }) export class CountryRoutingModule { }
<h3>Add Country</h3> <form [formGroup]="countryForm" (ngSubmit)="onFormSubmit()"> <p> Name: <input formControlName="name"> </p> <p> Capital: <input formControlName="capital"> </p> <p> Currency: <input formControlName="currency"> </p> <p> <button>Add</button> </p> </form>
import { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { CountryService } from '../services/country.service'; import { Country } from '../country'; import { DialogService } from '../../dialog.service'; import { Observable } from 'rxjs'; @Component({ templateUrl: './add-country.component.html' }) export class AddCountryComponent { isAdding = false; constructor( private countryService: CountryService, private route: ActivatedRoute, private router: Router, private formBuilder: FormBuilder, private dialogService: DialogService) { } countryForm = this.formBuilder.group({ name: '', capital: '', currency: '' }); onFormSubmit() { this.isAdding = true; const name = this.countryForm.get('name')?.value ?? ''; const capital = this.countryForm.get('capital')?.value ?? ''; const currency = this.countryForm.get('currency')?.value ?? ''; const country = new Country(0, name, capital, currency); this.countryService.addCountry(country) .subscribe(() => this.router.navigate(['../list'], { relativeTo: this.route }) ); } canDeactivate(): Observable<boolean> | boolean { if (!this.isAdding && this.countryForm.dirty) { return this.dialogService.confirm('Discard unsaved Country?'); } return true; } }
<h3>Country List</h3> <div *ngFor="let country of countries | async" [ngClass]= "'sub-child-menu'"> <p> {{country.countryId}} - {{country.name}} - {{country.capital}} - {{country.currency}} | <a [routerLink]="['edit', country.countryId]" routerLinkActive="active">Edit</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>Edit Country</h3> <p *ngIf="country"><b>Country Id: {{country.countryId }} </b></p> <form [formGroup]="countryForm" (ngSubmit)="onFormSubmit()"> <p> Name: <input formControlName="name"> </p> <p> Capital: <input formControlName="capital"> </p> <p> Currency: <input formControlName="currency"> </p> <p> <button>Update</button> </p> </form>
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router, Params } from '@angular/router'; import { FormGroup, FormBuilder } from '@angular/forms'; import { switchMap } from 'rxjs/operators'; import { CountryService } from '../../services/country.service'; import { Country } from '../../country'; @Component({ templateUrl: './country.edit.component.html' }) export class CountryEditComponent implements OnInit { country = {} as Country; countryForm = {} as FormGroup; isUpdating = false; constructor( private countryService: CountryService, private route: ActivatedRoute, private router: Router, private formBuilder: FormBuilder) { } ngOnInit() { this.route.params.pipe( switchMap((params: Params) => this.countryService.getCountry(+params['country-id'])) ).subscribe(country => { this.country = country ?? {} as Country; this.createForm(country); }); } createForm(country: Country | undefined) { this.countryForm = this.formBuilder.group({ name: country?.name, capital: country?.capital, currency: country?.currency }); } onFormSubmit() { this.isUpdating = true; this.country.name = this.countryForm.get('name')?.value; this.country.capital = this.countryForm.get('capital')?.value; this.country.currency = this.countryForm.get('currency')?.value; this.countryService.updateCountry(this.country) .subscribe(() => this.router.navigate(['../../'], { relativeTo: this.route }) ); } }
import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { Country } from '../country'; const COUNTRIES = [ new Country(1, 'India', 'New Delhi', 'INR'), new Country(2, 'China', 'Beijing', 'RMB') ]; let countriesObservable = of(COUNTRIES); @Injectable() export class CountryService { getCountries() { return countriesObservable; } getCountry(id: number) { return this.getCountries().pipe( map(countries => countries.find(country => country.countryId === id)) ); } updateCountry(country: Country) { return this.getCountries().pipe( map(countries => { let countryObj = countries.find(ob => ob.countryId === country.countryId); countryObj = country; return countryObj; })); } addCountry(country: Country) { return this.getCountries().pipe( map(countries => { let maxIndex = countries.length - 1; let countryWithMaxIndex = countries[maxIndex]; country.countryId = countryWithMaxIndex.countryId + 1; countries.push(country); return country; })); } }
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 { }
export class Person { constructor(public personId:number, public name:string, public city:string) { } }
import { NgModule, inject } from '@angular/core'; import { ActivatedRouteSnapshot, CanDeactivateFn, RouterModule, RouterStateSnapshot, Routes, mapToCanDeactivate } from '@angular/router'; import { PersonComponent } from './person.component'; import { PersonListComponent } from './person-list/person.list.component'; import { PersonEditComponent } from './person-list/edit/person.edit.component'; import { CanDeactivateGuard } from '../can-deactivate-guard.service'; const personRoutes: Routes = [ { path: 'person', component: PersonComponent, children: [ { path: '', component: PersonListComponent, children: [ { path: ':id', component: PersonEditComponent, canDeactivate: mapToCanDeactivate([CanDeactivateGuard]) } ] } ] } ]; @NgModule({ imports: [RouterModule.forChild(personRoutes)], exports: [RouterModule] }) export class PersonRoutingModule { }
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { PersonComponent } from './person.component'; import { PersonListComponent } from './person-list/person.list.component'; import { PersonEditComponent } from './person-list/edit/person.edit.component'; import { PersonService } from './services/person.service'; import { PersonRoutingModule } from './person-routing.module'; @NgModule({ imports: [ CommonModule, ReactiveFormsModule, PersonRoutingModule ], declarations: [ PersonComponent, PersonListComponent, PersonEditComponent ], providers: [ PersonService ] }) export class PersonModule { }
<h3>Person List</h3> <div *ngFor="let person of persons | async" [ngClass]= "'sub-child-menu'"> <p>{{person.personId}}. {{person.name}}, {{person.city}} <button type="button" (click)="goToEdit(person)">Edit</button> </p> </div> <div [ngClass]= "'sub-child-container'"> <router-outlet></router-outlet> </div>
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs'; import { PersonService } from '../services/person.service'; import { Person } from '../person'; @Component({ templateUrl: './person.list.component.html' }) export class PersonListComponent implements OnInit { persons: Observable<Person[]>; constructor( private personService: PersonService, private route: ActivatedRoute, private router: Router) { this.persons = this.personService.getPersons(); } ngOnInit() { } goToEdit(person: Person) { this.router.navigate([person.personId], { relativeTo: this.route }); } }
<h3>Edit Person</h3> <p *ngIf="person"><b>Person Id: {{person.personId }} </b></p> <form [formGroup]="personForm" (ngSubmit)="onFormSubmit()"> <p> Name: <input formControlName="name"> </p> <p> City: <input formControlName="city"> </p> <p> <button>Update</button> </p> </form>
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router, Params } from '@angular/router'; import { FormGroup, FormBuilder } from '@angular/forms'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { PersonService } from '../../services/person.service'; import { Person } from '../../person'; import { DialogService } from '../../../dialog.service'; @Component({ templateUrl: './person.edit.component.html' }) export class PersonEditComponent implements OnInit { person = {} as Person; personForm = {} as FormGroup; isUpdating = false; constructor( private personService: PersonService, private route: ActivatedRoute, private router: Router, private formBuilder: FormBuilder, private dialogService: DialogService) { } ngOnInit() { this.route.params.pipe( switchMap((params: Params) => this.personService.getPerson(+params['id'])) ).subscribe(person => { this.person = person ?? {} as Person; this.createForm(person); }); } createForm(person: Person | undefined) { this.personForm = this.formBuilder.group({ name: person?.name, city: person?.city }); } onFormSubmit() { this.isUpdating = true; this.person.name = this.personForm.get('name')?.value; this.person.city = this.personForm.get('city')?.value; this.personService.updatePerson(this.person) .subscribe(() => this.router.navigate(['../'], { relativeTo: this.route }) ); } canDeactivate(): Observable<boolean> | boolean { if (!this.isUpdating && this.personForm.dirty) { this.isUpdating = false; return this.dialogService.confirm('Discard changes for Person?'); } return true; } }
import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { Person } from '../person'; const PERSONS = [ new Person(1, 'Mahesh', 'Varanasi'), new Person(2, 'Ram', 'Ayodhya'), new Person(3, 'Krishn', 'Mathura') ]; let personsObservable = of(PERSONS); @Injectable() export class PersonService { getPersons(): Observable<Person[]> { return personsObservable; } getPerson(id: number) { return this.getPersons().pipe( map(persons => persons.find(person => person.personId === id)) ); } updatePerson(person: Person) { return this.getPersons().pipe( map(persons => { let personObj = persons.find(ob => ob.personId === person.personId); personObj = person; return personObj; })); } }
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" routerLinkActive="active">Country</a></li> <li><a routerLink="/person" 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'; import { CanDeactivateGuard } from './can-deactivate-guard.service'; import { CountryEditCanDeactivateGuard } from './country-edit-can-deactivate-guard.service'; const routes: Routes = [ { path: '', redirectTo: '/country', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [ RouterModule ], providers: [ CanDeactivateGuard, CountryEditCanDeactivateGuard, ], }) 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 { CountryModule } from './country/country.module'; import { PersonModule } from './person/person.module'; import { AppRoutingModule } from './app-routing.module'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ BrowserModule, CountryModule, PersonModule, AppRoutingModule, ], declarations: [ AppComponent, PageNotFoundComponent ], providers: [ DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { }
.parent-menu ul { list-style-type: none; margin: 0; padding: 0; overflow: hidden; background-color: #333; } .parent-menu li { float: left; } .parent-menu li a { display: block; color: white; text-align: center; padding: 15px 15px; text-decoration: none; } .parent-menu li a:hover:not(.active) { background-color: #111; } .parent-menu .active{ background-color: #4CAF50; } .child-container { padding-left: 10px; } .sub-child-container { padding-left: 10px; } .child-menu { padding-left: 25px; } .child-menu .active{ color: #4CAF50; } .sub-child-menu { background-color: #f1f1f1; width: 275px; list-style-type: none; margin: 0; padding: 0; } .sub-child-menu .active{ color: #4CAF50; } button { background-color: #008CBA; color: white; }
6. Run Application
Download source code using download link given below on this page and run the application.Click on the country and go to add country. Fill data and try to navigate away. We will get Confirmation Dialog Box to ask if we want to discard unsaved data.