Monday, August 31, 2020

CanDeactivate Guard in Angular With Example

In this post we’ll see an example of CanDeactivate guard in Angular which helps in handling unsaved changes.

CanDeactivate interface in Angular

Any class implementing CanDeactivate interface can act as a guard which decides if a route can be deactivated. If you configure this class to be a canDeactivate guard for any route then if you try to navigate away from that route you can show a dialog box to confirm whether user really want to move away.

CanDeactivate interface has a single method canDectivate and the interface is defined as given below-

interface CanDeactivate<T> {
  canDeactivate(component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree
}

Angular CanDeactivate guard example

It is easy to show the required configuration and implementation using an example so let’s go through an example of using CanDeactivate guard in Angular.

In the example we’ll have a Users route that shows a list of users and two nested routes add user and edit user.

What we want to implement is that if you click anywhere else while you are in the middle of adding or editing a user you should get a confirmation dialog box asking whether you really want to navigate away. If you click cancel, you won’t be navigated away, if you click ok then you’ll be navigated away. This handling of unsaved changes is done through implementing a CanDeactivate guard.

Dialog Service

DialogService has a confirm() method to prompt the user to confirm their intent. The window.confirm is a blocking action that displays a modal dialog and waits for user interaction.

import { Observable, of } from 'rxjs';
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class DialogService {
  confirm(message?: string): Observable<boolean> {
    const confirmation = window.confirm(message || 'Is it OK?');
    return of(confirmation);
  }
}

CanDeactivate Guard implementation

Here is an implmentation of canDeactivate guard that checks for the presence of a canDeactivate() method in any component so it can be used as a reusable guard. First thing is to create an interface with a canDeactivate method and then create a Service that implements CanDeactivate interface passing your interface (CanComponentDeactivate) as its generic parameter.

import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';

export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
   
@Injectable({
 providedIn: 'root',
})
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate) {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

In the canDeactivate() method there is a method call component.canDeactivate(). This generic imlementation can detect if a component has the canDeactivate() method and call it. Any component where a canDeactivate guard is needed can define canDeactivate() method, that method will be invoked by this component.canDeactivate() call otherwise true is returned.

A component where canDeactivate guard is needed can just have a canDeactivate() method and Angular will detect it. If you want to be more correct in your code then your component can also implement the interface created by you. For example if I have a AddUserComponent where I want to have canDeactivate guard then the class can be written as given below.

export class AddUserComponent implements CanComponentDeactivate{
 ....
 ....
}

component-specific CanDeactivate guard

You could also make a component-specific CanDeactivate guard. In our example let’s assume that we want a component specific CanDeactivate guard for EditUserComponent. Guard class implements the CanDeactivate interface but its parameter now is the component for which this guard is going to be used.

import { EditUserComponent } from '../users/edituser/edituser.component';
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { DialogService } from './dialog.service';

@Injectable({
    providedIn: 'root',
})
export class UserEditCanDeactivateGuard implements CanDeactivate<EditUserComponent> {
  constructor(private dialogService: DialogService) { }
  canDeactivate(component: EditUserComponent, currentRoute: ActivatedRouteSnapshot, 
      currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): 
      boolean | Observable<boolean> | Promise<boolean> {
    if(!component.isUserEdited && component.userForm.dirty){
      return this.dialogService.confirm('Discard changes?');
    }
    return true;
  }
}

User Model (user.model.ts)

There is a User model class with the required fields for the User object.

export class User {
  id: number;
  name: string;
  age: number;
  joinDate: Date;
  constructor(id: number, name: string, age : number, joinDate : Date) {
    this.id = id;
    this.name = name;
    this.age = age;
    this.joinDate  = joinDate;
  }
}

UserService (user.service.ts)

This is a service class with methods to add or update a user, getting all the users or user by id. Also creates an array of User objects that is displayed through UsersComponent.

import { User } from '../users/user.model';
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  constructor(){}
  // User detail array
  users = [new User(1,'Jack', 62, new Date('2005-03-25')),
  new User(2, 'Olivia', 26, new Date('2014-05-09')),
  new User(3, 'Manish', 34, new Date('2010-10-21'))] ;
  // for adding new user
  addUser(user: User){
    let maxIndex = this.users.length + 1;
    user.id = maxIndex;
    this.users.push(user);
    return user;
  }
  // for updating existing user
  editUser(user: User){
    let obj = this.users.find(e => e.id === user.id)
    obj = user;
  }
  // get the users list
  getUsers(){
    return this.users;
  }
  // User by Id
  getUser(id: Number){
    return this.users.find(user => user.id === id)
  }
}

Components and templates

We have our CanDeactivate guards, dialog service and UserService ready now let’s go through the components.

HomeComponent (home.component.ts)

This component doesn’t do much just shows a welcome message.

import { OnInit, Component } from '@angular/core';

@Component({
    selector: 'app-home',
    templateUrl: './home.component.html'
})
export class HomeComponent implements OnInit {
  message: string;
  constructor() {
    this.message = 'Welcome to CanDeactivate guard example';
  }
  ngOnInit() {
  }
}

home.component.html

<h4>{{ message }}</h4>

UsersComponent (users.component.ts)

This component is used to show users in a tabular form with a edit button in each row to edit any user. There is also an option to add a new user.

import { Component, OnInit, Input } from '@angular/core';
import { User } from './user.model';
import { UserService } from '../services/user.service';
import { Router, ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html',
  styleUrls: ['./users.component.css']
})
export class UsersComponent implements OnInit {
  users : User[];
  constructor(private userService: UserService, 
              private router: Router, 
              private route: ActivatedRoute){}
              
  ngOnInit(): void {
    // get all users
    this.users = this.userService.getUsers();
  }

  onClickNewUser(){
    this.router.navigate(['add'], {relativeTo:this.route});
  }

  editUser(id: number){
    this.router.navigate(['edit', id], {relativeTo:this.route});
  }
}

users.component.html

<div class="row">
  <div class="col-sm-6, col-md-6">
    <h2>User Details</h2>
    <table class="table table-sm table-bordered m-t-4">
      <tr>
        <th></th>
        <th>Name</th>
        <th>Age</th>
        <th>Joining Date</th>
        <th></th>
      </tr>
      <tr *ngFor="let user of users; index as i;">
        <td>{{i+1}}</td>
        <td>{{user.name}}</td>
        <td>{{user.age}}</td>
        <td>{{user.joinDate | date:'dd/MM/yyyy'}}</td>
        <td>
          <button class="btn btn-primary btn-sm" (click)="editUser(user.id)">
            Edit
          </button>
        </td>
      </tr>
    </table>
    <button class="btn btn-primary" (click)="onClickNewUser()">
      New User
    </button>
  </div>
  <div class="col-sm-5, col-md-5">
    <router-outlet></router-outlet>
  </div>
</div>

AddUserComponent (adduser.commponent.ts)

This is the child component of the UsersComponent and it has the functionality to add new user. In this component canDeactivate() method is added which is called by the CanDeactivateGuard.

Notice that CanComponentDeactivate interface is not explicitly implemented here but it is a good practice to do that. So you can have class as this also- export class AddUserComponent implements CanComponentDeactivate{ .. }

import { Component } from '@angular/core';
import { UserService } from '../../services/user.service';
import { FormBuilder } from '@angular/forms';
import { User } from '../user.model';
import { Router, ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { DialogService } from 'src/app/services/dialog.service';

@Component({
  selector: 'app-adduser',
  templateUrl: './adduser.component.html',
  styleUrls: ['./adduser.component.css'],
  //providers: [UserService]
})
export class AddUserComponent {
  isUserAdded = false;    
  constructor(private userService: UserService, 
    private dialogService: DialogService,
    private formBuilder: FormBuilder,
    private router: Router,
    private route: ActivatedRoute){}
  
  // Create a Formgroup instance
  userForm = this.formBuilder.group({
    name: '',
    age: '',
    joindate: ''
  });
  onFormSubmit() {
    // Get values from form
    let name = this.userForm.get('name').value;
    let age = this.userForm.get('age').value;
    let joindate = this.userForm.get('joindate').value;
    let user = new User(null, name, age, new Date(joindate));
    // call adduser to add the new user
    this.userService.addUser(user); 
    //set property to true
    this.isUserAdded = true;
    // navigate to parent route 
    this.router.navigate([ '../' ], { relativeTo: this.route })
  }
  // CanDeactivate guard 
  canDeactivate(): Observable<boolean> | boolean {
    if (!this.isUserAdded && this.userForm.dirty) {
      return this.dialogService.confirm('Discard changes?');
    }
    return true;
  }
}

adduser.component.html

<h3>Add User</h3>
<form [formGroup]="userForm" (ngSubmit)="onFormSubmit()">
  <div class="form-group">
    <label for="name">Name:</label>
    <input class="form-control" placeholder="Enter name" formControlName="name">
  </div>
  <div class="form-group">
    <label for="age">Age:</label>            
    <input class="form-control" placeholder="Enter age" formControlName="age">
  </div> 
  <div class="form-group">
    <label for="joindate">Joining Date:</label>    
    <input class="form-control" placeholder="yyyy-MM-dd" formControlName="joindate">
  </div>
  <p> 
    <button class="btn btn-primary">Add User</button> 
  </p>
</form> 

EditUserComponent (edituser.commponent.ts)

This is the child component of the UsersComponent and it has the functionality to edit a user. This component has a component specific UserEditCanDeactivateGuard already implemented.

import { Component, OnInit, LOCALE_ID, Inject } from '@angular/core';
import { UserService } from 'src/app/services/user.service';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { User } from '../user.model';
import { FormGroup, FormBuilder } from '@angular/forms';
import { formatDate } from '@angular/common';

@Component({
  templateUrl: './edituser.component.html'
})
export class EditUserComponent implements OnInit{
  user: User;
  userForm: FormGroup;
  isUserEdited = false; 
  constructor(private userService: UserService, 
          private router: Router, 
          private route: ActivatedRoute, 
          private formBuilder: FormBuilder,
          @Inject(LOCALE_ID) private locale: string) {}
    
  ngOnInit() {
    this.route.params.subscribe((params: Params)=> {
      // get the id parameter
      let userId = +params['userid'];
      //console.log('In edit user-- ' + userId);
      this.user = this.userService.getUser(userId);
      //console.log('In edit user-- ' + this.user.name);
      this.createEditForm(this.user);
    });
  }

  createEditForm(user: User){
    this.userForm = this.formBuilder.group({
      id: user.id,
      name: user.name,
      age: user.age,
      joindate: formatDate(user.joinDate, 'yyyy-MM-dd',this.locale)            
    });
  }
  onFormSubmit() {        
    this.user.name = this.userForm.get('name').value;
    this.user.age = this.userForm.get('age').value;
    this.user.joinDate = this.userForm.get('joindate').value;
    // call edit user method
    this.userService.editUser(this.user);
    //set property to true
    this.isUserEdited = true;
    this.router.navigate([ '../../' ], { relativeTo: this.route })
  }
}

edituser.component.html

<h3>Edit User</h3>
<form [formGroup]="userForm" (ngSubmit)="onFormSubmit()">
  <label for="id">User Id:</label> {{user.id}}
  <div class="form-group">
    <label for="name">Name:</label>
    <input class="form-control" formControlName="name">
  </div>
  <div class="form-group">
    <label for="age">Age:</label>            
    <input class="form-control" formControlName="age">
  </div> 
  <div class="form-group">
    <label for="joindate">Joining Date:</label>    
    <input class="form-control" formControlName="joindate">
  </div>
  <p> <button class="btn btn-primary">Update User</button> </p>
</form> 

AppRoutingModule (app-routing.module.ts)

Now comes the configuration part where we’ll configure the routes and the associated CanDeactivate guards.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home.component';
import { UsersComponent } from './users/users.component';
import { AddUserComponent } from './users/adduser/adduser.component';
import { EditUserComponent } from './users/edituser/edituser.component';
import { CanDeactivateGuard } from './services/can-deactivate.guard';
import { UserEditCanDeactivateGuard } from './services/useredit-can-deactivate.guard';

const routes: Routes = [
                      {path: 'home', component: HomeComponent},  
                      {path: 'user', component: UsersComponent, children: [
                        {path: 'add', component: AddUserComponent, canDeactivate: [CanDeactivateGuard]}, 
                        {path: 'edit/:userid', component: EditUserComponent, canDeactivate: [UserEditCanDeactivateGuard]}
                      ]},                                        
                      {path: '', redirectTo:'/home', pathMatch: 'full'}             
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule], 
  providers: [CanDeactivateGuard, UserEditCanDeactivateGuard]
})
export class AppRoutingModule { }

In the module two things to notice are-

1. Configuration of guards in child routes using canDeactivate property.

{path: 'add', component: AddUserComponent, canDeactivate: [CanDeactivateGuard]}, 
                        {path: 'edit/:userid', component: EditUserComponent, canDeactivate: [UserEditCanDeactivateGuard]}

2. Providers array with guard implementations.

providers: [CanDeactivateGuard, UserEditCanDeactivateGuard]

AppModule (app.module.ts)

In the AppModule you need to provide the components in the declarations array. In the providers array you need to add DialogService and UserService.

providers: [UserService, DialogService]

Since this example used Forms so also add FormsModule and ReactiveFormsModule in imports array.

  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    AppRoutingModule
  ]

app.component.html

Code for creating a menu and Route links is in this template.

<nav class="navbar navbar-expand-md bg-dark navbar-dark">
  <div class="container-fluid">
    <div class="collapse navbar-collapse" id="collapsibleNavbar">
      <ul class="nav navbar-nav">
        <li class="nav-item" routerLinkActive="active">
          <a class="nav-link" routerLink="/home">Home</a>
        </li>
        <li class="nav-item" routerLinkActive="active">
          <a class="nav-link" routerLink="/user">Users</a>
        </li>
      </ul>
    </div>
  </div>
</nav>
<div class="container">
  <div class="row"><p></p></div>
  <div class="row">
    <div class="col-sm-12, col-md-12">
      <router-outlet></router-outlet>
    </div>
  </div>
</div>

Add User (Navigating Away)

CanDeactivate Guard confirmation box

Edit User (Navigating Away)

CanDeactivate Guard example angular

That's all for this topic CanDeactivate Guard in Angular With Example. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Angular Tutorial Page


Related Topics

  1. Angular Access Control CanActivate Route Guard Example
  2. Angular CanActivateChild Guard to protect Child Routes
  3. Setting Wild Card Route in Angular
  4. Nested Route (Child Route) in Angular
  5. Highlight Currently Selected Menu Item Angular Routing Example

You may also like-

  1. Angular @Input and @Output Example
  2. Angular - Call One Service From Another
  3. Angular Custom Event Binding Using @Output Decorator
  4. Angular Class Binding With Examples
  5. Serialization and Deserialization in Java
  6. Fibonacci Series Program in Java
  7. What is SafeMode in Hadoop
  8. Name Mangling in Python

No comments:

Post a Comment