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
You want a reusable way to prompt users before navigating away from a component containing a dirty form.
Requirements:
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.
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.
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