To also cover guards against browser refreshes, closing the window, etc. (see @ChristopheVidal's comment to Günter's answer for details on the issue), I have found it helpful to add the @HostListener
decorator to your class's canDeactivate
implementation to listen for the beforeunload
window
event. When configured correctly, this will guard against both in-app and external navigation at the same time.
For example:
Component:
import { ComponentCanDeactivate } from './pending-changes.guard';
import { HostListener } from '@angular/core';
import { Observable } from 'rxjs/Observable';
export class MyComponent implements ComponentCanDeactivate {
// @HostListener allows us to also guard against browser refresh, close, etc.
@HostListener('window:beforeunload')
canDeactivate(): Observable<boolean> | boolean {
// insert logic to check if there are pending changes here;
// returning true will navigate without confirmation
// returning false will show a confirm dialog before navigating away
}
}
Guard:
import { CanDeactivate } from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
export interface ComponentCanDeactivate {
canDeactivate: () => boolean | Observable<boolean>;
}
@Injectable()
export class PendingChangesGuard implements CanDeactivate<ComponentCanDeactivate> {
canDeactivate(component: ComponentCanDeactivate): boolean | Observable<boolean> {
// if there are no pending changes, just allow deactivation; else confirm first
return component.canDeactivate() ?
true :
// NOTE: this warning message will only be shown when navigating elsewhere within your angular app;
// when navigating away from your angular app, the browser will show a generic warning message
// see http://stackoverflow.com/a/42207299/7307355
confirm('WARNING: You have unsaved changes. Press Cancel to go back and save these changes, or OK to lose these changes.');
}
}
Routes:
import { PendingChangesGuard } from './pending-changes.guard';
import { MyComponent } from './my.component';
import { Routes } from '@angular/router';
export const MY_ROUTES: Routes = [
{ path: '', component: MyComponent, canDeactivate: [PendingChangesGuard] },
];
Module:
import { PendingChangesGuard } from './pending-changes.guard';
import { NgModule } from '@angular/core';
@NgModule({
// ...
providers: [PendingChangesGuard],
// ...
})
export class AppModule {}
NOTE: As @JasperRisseeuw pointed out, IE and Edge handle the beforeunload
event differently from other browsers and will include the word false
in the confirm dialog when the beforeunload
event activates (e.g., browser refreshes, closing the window, etc.). Navigating away within the Angular app is unaffected and will properly show your designated confirmation warning message. Those who need to support IE/Edge and don't want false
to show/want a more detailed message in the confirm dialog when the beforeunload
event activates may also want to see @JasperRisseeuw's answer for a workaround.
The router provides a lifecycle callback CanDeactivate
for more details see the guards tutorial
class UserToken {} class Permissions { canActivate(user: UserToken, id: string): boolean { return true; } } @Injectable() class CanActivateTeam implements CanActivate { constructor(private permissions: Permissions, private currentUser: UserToken) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean>|Promise<boolean>|boolean { return this.permissions.canActivate(this.currentUser, route.params.id); } } @NgModule({ imports: [ RouterModule.forRoot([ { path: 'team/:id', component: TeamCmp, canActivate: [CanActivateTeam] } ]) ], providers: [CanActivateTeam, UserToken, Permissions] }) class AppModule {}
original (RC.x router)
class CanActivateTeam implements CanActivate { constructor(private permissions: Permissions, private currentUser: UserToken) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<boolean> { return this.permissions.canActivate(this.currentUser, this.route.params.id); } } bootstrap(AppComponent, [ CanActivateTeam, provideRouter([{ path: 'team/:id', component: Team, canActivate: [CanActivateTeam] }]) );
The example with the @Hostlistener from stewdebaker works really well, but I made one more change to it because IE and Edge display the "false" that is returned by the canDeactivate() method on the MyComponent class to the end user.
Component:
import {ComponentCanDeactivate} from "./pending-changes.guard";
import { Observable } from 'rxjs'; // add this line
export class MyComponent implements ComponentCanDeactivate {
canDeactivate(): Observable<boolean> | boolean {
// insert logic to check if there are pending changes here;
// returning true will navigate without confirmation
// returning false will show a confirm alert before navigating away
}
// @HostListener allows us to also guard against browser refresh, close, etc.
@HostListener('window:beforeunload', ['$event'])
unloadNotification($event: any) {
if (!this.canDeactivate()) {
$event.returnValue = "This message is displayed to the user in IE and Edge when they navigate without using Angular routing (type another URL/close the browser/etc)";
}
}
}
June 2020 answer:
Note that all solutions proposed up until this point do not deal with a significant known flaw with Angular's canDeactivate
guards:
This has been discussed here, here, and at length here
Please see my solution to the problem demonstrated here which safely works around this issue*. This has been tested on Chrome, Firefox, and Edge.
* IMPORTANT CAVEAT: At this stage, the above will clear the forward history when the back button is clicked, but preserve the back history. This solution will not be appropriate if preserving your forward history is vital. In my case, I typically use a master-detail routing strategy when it comes to forms, so maintaining forward history is not important.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With