I'm using a CanDeactivate
guard in Angular to prompt the user before navigating away from a form with unsaved changes. It works fine for normal route changes (e.g., clicking a link), but it breaks when the user presses the browser back button.
Let's say my routing flow is:
/home → /user → /edit
/edit
with unsaved changes.CanDeactivate
./home
, skipping /user
.I implemented a CanDeactivate
guard like this:
export class YourFormComponent implements CanComponentDeactivate, OnInit, AfterViewInit {
hasUnsavedChanges = false;
// Implement the canDeactivate method
canDeactivate(): Observable<boolean> | boolean {
if (!this.hasUnsavedChanges) {
return true;
}
const confirmLeave = window.confirm('You have unsaved changes. Leave anyway?');
return confirmLeave;
}
}
route.ts
import { Routes } from '@angular/router';
import { YourFormComponent } from './your-form.component';
import { ConfirmLeaveGuard } from './confirm-leave.guard';
const routes: Routes = [
{
path: 'form',
component: YourFormComponent,
canDeactivate: [ConfirmLeaveGuard]
}
];
confrim-leave.guard.ts
import { inject } from '@angular/core';
import { CanDeactivateFn } from '@angular/router';
import { Observable } from 'rxjs';
import { Location } from '@angular/common';
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
export const ConfirmLeaveGuard: CanDeactivateFn<CanComponentDeactivate> = (component: any, currentRoute, currentState, nextState) => {
const location = inject(Location);
const result = component.canDeactivate();
// If it's a boolean value
if (typeof result === 'boolean') {
if (!result) {
location.replaceState(window.location.pathname); // Restore URL
}
return result;
}
// If it's an Observable or Promise
if (result instanceof Observable || result instanceof Promise) {
return new Promise(resolve => {
Promise.resolve(result).then(confirmed => {
if (!confirmed) {
location.replaceState(window.location.pathname); // Restore URL
}
resolve(confirmed);
});
});
}
return true;
};
I also tried using Location.replaceState() or Location.go() inside the guard to restore the history stack, but it still misbehaves when using the back button.
Question How can I correctly handle the browser back button with CanDeactivate to prevent double navigation or skipped routes?
Any advice or examples would be appreciated.
Working image - https://s6.imgcdn.dev/YjXe3M.gif
Using your stackblitz I may have a solution. Try replacing your canDeactivateGuard
with this version. Angular’s CanDeactivate
guard does not directly prevent the back navigation in the way we want. The guard only cancels the navigation to the new route, but it doesn't stop the back navigation. So it seems that the browser is "holding onto" the previous back, and then when you cancel back again it adds another back onto it, which is why its going back two steps.
This solution is probably not perfect, but hopefully might help you get to the right place.
import { inject } from '@angular/core';
import { CanDeactivateFn } from '@angular/router';
import { Observable, of } from 'rxjs';
import { Location } from '@angular/common';
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
export const canDeactivateGuard: CanDeactivateFn<CanComponentDeactivate> = (
component: CanComponentDeactivate,
currentRoute,
currentState,
nextState
) => {
const location = inject(Location);
const result = component.canDeactivate();
if (typeof result === 'boolean') {
if (!result) {
// Restore the current URL if the user cancels
window.history.pushState(null, '', currentState.url);
}
return of(result); // Wrap the result in an Observable
}
// If it's an Observable or Promise, handle it properly
if (result instanceof Observable) {
return new Observable<boolean>((observer) => {
result.subscribe((confirmed) => {
if (!confirmed) {
// Restore the current URL if the user cancels
window.history.pushState(null, '', currentState.url);
}
observer.next(confirmed);
observer.complete();
});
});
}
if (result instanceof Promise) {
return new Observable<boolean>((observer) => {
result.then((confirmed) => {
if (!confirmed) {
// Restore the current URL if the user cancels
window.history.pushState(null, '', currentState.url);
}
observer.next(confirmed);
observer.complete();
});
});
}
return of(true); // Default case if no action needed
};
Working stackblitz