Handling Unsaved Changes: Preventing Users from Exiting Angular Pages
Introduction
Have you ever wondered how to prevent someone from exiting an Angular screen, potentially losing unsaved content? In this article, I will demonstrate how to handle such a case by displaying a dialog that asks the user if they are sure they want to exit the screen.
There are many ways to achieve this task. The differences between each implementation lie in best practices and how manageable the implementation is. I won't discuss all the approaches I have in mind because, in my opinion, they are all ineffective except for one, which I consider a good practice (subjectively speaking).
In this article, I will be using concepts like CanDeactivate, Angular Material Dialogs, and Reactive Forms.
Implementation
For those who don't know, a guard is a way to prevent a user from accessing or loading a screen before the component lifecycle is triggered. It adds a layer of security to our Angular application and makes routing management easier.
CanDeactivate is a guard that is called before the ngOnDestroy method of the screen component. If canDeactivate returns false, it will cancel the destruction of the component, thereby preventing the screen from being destroyed.
Below is the code for the guard. Following the code, you'll find a brief explanation of its functionality.
领英推荐
import { inject, Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanDeactivate, CanDeactivateFn, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { FormComponent } from './form/form.component';
import { map, takeUntil, tap } from 'rxjs/operators';
import { of } from 'rxjs';
export const canExitFormGuard: CanDeactivateFn<FormComponent> = (component, currentRoute, currentState, nextState) => {
const router = inject(Router);
const dialog = inject(MatDialog)
if(component.form.dirty) {
return dialog.open(component.confirmLossDialog).afterClosed().pipe(
takeUntil(component.destroyed$),
map(data => {
dialog.closeAll()
if(data === true) {
component.form.markAsPristine()
}
else {
// Programmatically navigate back to prevent overwriting the current route
router.navigateByUrl(currentState.url);
return false;
}
})
)
}
else {
component.form.markAsPristine()
return of(true)
}
};
As you can see in the code above, Angular injects the instance of the component into the function as a parameter. This allows you to write code that has access to the context of the component.
If the component is dirty, it will display a dialog asking if the user is sure they want to exit the page and will wait for the user's response before making a decision. By the way, if you're wondering how I created a property that references the dialog, I did so by using a combination of the ng-template tag and ViewChild, as shown in the code blocks below. As you can see, I passed a boolean value to [mat-dialog-close]="true". This value is handled in the dialog subscription, as demonstrated in the code above. In this case, data will be a boolean.
One more thing to note is the use of router.navigateByUrl(currentState.url);. The reason for using this is that the back history is being mutated even if the navigation hasn't finished or has been canceled.
<ng-template #confirmUserLossDialog>
<div>
<p mat-dialog-title class="confirm-delete" [ngStyle]="{ 'text-align': titleCentered ? 'center' : 'start'}">{{title}}</p>
<mat-dialog-content align="start">
<div class="dialog-extra">
<ng-content select="[extra]"></ng-content>
</div>
<p class="center-align font-large dialog-message" [ngClass]="classes">{{ message }}</p>
</mat-dialog-content>
<div class="yesNoBtnContainer yesNobtnPadding">
<button class="yesNoWidth newCancelButton" (click)="close()" [mat-dialog-close]="false">{{ noText }}</button>
<button class="btnNewStyle yesNoWidth" (click)="delete()" [mat-dialog-close]="true" *ngIf="yestText">{{ yestText }}</button>
</div>
</div>
</ng-template>
@ViewChild('confirmUserLossDialog') confirmUserLossDialog!: TemplateRef<any>;
Finally, all you have to do is to bind the canDeactivate function to the route.
const routes: Routes = [
{
path: "",
component: FormComponent,
canDeactivate: [canExitFormGuard]
}
];
Senior .NET Developer Experienced backend developer with 6 years of expertise in .NET Core (C#), specializing in designing and optimizing web APIs and SQL database solutions.
1 个月Very helpful