angularangular-materialobservablesubscribecandeactivate

How to subscribe asynchronously to matdialog service for candeactivate guard?


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


Solution

  • Your problem

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

    Requirements:

    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