3

I have implemented candeactivate guard using angular forms validation. If user clicks on an ngForm Field. and tries to navigate to different Tab, user will get a custom confirmation Popup, which will say "Discard Changes ? " and returns true or false.

This is my form guard

import { NgForm } from "@angular/forms";
import { ComponentCanDeactivate } from './component-can-deactivate';

export abstract class FormCanDeactivate extends ComponentCanDeactivate {

abstract get form(): NgForm;

canDeactivate(): boolean {
    return this.form.submitted || !this.form.dirty;
}
}

Component Guard

import { HostListener } from "@angular/core";

export abstract class ComponentCanDeactivate {

abstract canDeactivate(): boolean;

@HostListener('window:beforeunload', ['$event'])
unloadNotification($event: any) {
    if (!this.canDeactivate()) {
        $event.returnValue = true;
    }
}
}

Now here is my code for confirmation popup. My problem here is if I use default confirm() method (commented line in below code), it gives windows popup,and asks for YES or NO, which works perfect. But if I use Custom Material Popup here, I have to subscribe to afterclosed() method, which performs asynchronously, whereas I have to wait till this method executes before proceeding. How can I achieve this ?

import { Injectable } from '@angular/core';
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { MatMenuTrigger, MatDialog } from '@angular/material';
import { Observable } from 'rxjs/Observable';
import { ComponentCanDeactivate } from './component-can-deactivate';
import { ConfirmationComponent } from 'src/app/core/modals/confirmation/confirmation.component';


@Injectable()
export class CanDeactivateGuard implements CanDeactivate<ComponentCanDeactivate> {

    constructor(private modalService: MatDialog) {

    }

canDeactivate(component: ComponentCanDeactivate): boolean {

    if (!component.canDeactivate()) {
        // return confirm('You have unsaved changes! If you leave, your changes will be lost');

        const dialogRef = this.modalService.open(ConfirmationComponent, {});
        dialogRef.afterClosed().subscribe(res => {
            if (res == 'OK') {
                return true;
            } else {
                return false;
            }
        });
    }
        return true;
    }
}

And from the modal I am returning 'OK' like below

constructor(private dialogRef: MatDialogRef<ConfirmationComponent>) { }

btnOk() {
   this.dialogRef.close('OK');

}

Any help is appreciated.

Edit :

I have extended formdeactivate in my component

export class EditFormComponent extends FormCanDeactivate implements OnInit {

@ViewChild('form', { static: true }) form: NgForm;

constructor(){super();}
}

Stackblitz Link :https://angular-custom-popup-candeactivate.stackblitz.io

Chetan Birajdar
  • 461
  • 9
  • 22

1 Answers1

12

Your problem

You want a reusable way to prompt users before navigating away from a component containing a dirty form.

Requirements:

  • There is no prompt if the form is clean
  • If the user wants to exit, navigation will continue
  • If the user doesn't want exit, navigation will be cancelled

Your existing solution

Once I took a little time to understand your solution, I can see it is an elegant way of handling multiple components.

Your design is approximately this:

export abstract class ComponentCanDeactive {
  abstract canDeactivate(): boolean;
}

export abstract class FormCanDeactivate extends ComponentCanDeactivate {
  abstract get form(): NgForm;

  canDeactivate(): boolean {
    return this.form.submitted || !this.form.dirty;
  }
}

If you want to apply this to a component, you just extend the FormCanDeactivate class.

You implement it using the Angular CanDeactivate route guard.

export class CanDeactivateGuard implements CanDeactivate<ComponentCanDeactivate> {
  canDeactivate(component: ComponentCanDeactivate): boolean {
    return component.canDeactivate();
  }
}

You add this to the relevant routes in your routing. I assume that you understand how all of this works, since you provided the code and demo for it.

If you simply want to prevent route deactivation when a component has a dirty form, you have already solved the problem.

Using a dialog

You now want to give the user a choice before they navigate away from a dirty form. You implemented this with a synchronous javascript confirm, but you want to use the Angular Material dialog, which is asynchronous.

The solution

Firstly, since you are going to use this asynchronously, you need to return an asynchronous type from your guard. You can return either a Promise or Observable. The Angular Material dialog returns an Observable, so I'll use that.

It's now simply a case of setting up the dialog and returning the observable close function.

deactivate-guard.ts

constructor(private modalService: MatDialog) {}

canDeactivate(component: ComponentCanDeactivate):  Observable<boolean> {
  // component doesn't require a dialog - return observable true
  if (component.canDeactivate()) {
    return of(true);
  }

  // set up the dialog
  const dialogRef = this.modalService.open(YesNoComponent, {
    width: '600px',
    height: '250px', 
  });

  // return the observable from the dialog  
  return dialogRef.afterClosed().pipe(
    // map the dialog result to a true/false indicating whether
    // the route can deactivate
    map(result => result === true)
  );    
}

Where YesNoComponent is a custom dialog component you have created as a wrapper around the dialog.

export class YesNoComponent {

  constructor(private dialogRef: MatDialogRef<YesNoComponent>  ) { }

  Ok(){
    this.dialogRef.close(true);
  }

  No(){
    this.dialogRef.close(false);
  }
}

DEMO: https://stackblitz.com/edit/angular-custom-popup-candeactivate-mp1ndw

Kurt Hamilton
  • 12,490
  • 1
  • 24
  • 40
  • Just found that the another cause of the problem was angular ngx ui loader, which was loading infinitely, when I dirty a form field and route to different page, and not allowing me to enter Yes /no button. – Chetan Birajdar Feb 28 '20 at 12:08
  • Ah. It's never simple! – Kurt Hamilton Feb 28 '20 at 12:10
  • any idea where we can put our YesNo popup code on browser refresh or window close. We have window:beforeunload code to capture the window reload event in ComponentCanDeactivate class in above example. – Chetan Birajdar Mar 03 '20 at 06:57
  • 1
    That's a whole separate question! I think this answer is already complex enough without getting involved in the `window:beforeunload` behaviour. I would do some research and then post a new question if you are struggling. – Kurt Hamilton Mar 03 '20 at 07:28
  • 1
    Awesome answer. you have saved my day, thank you – Tushar Apr 16 '21 at 18:49
  • Thanks a lot, I was missing the map part – Kevin Oswaldo Jan 03 '22 at 17:27