Handling Unsaved Changes: Preventing Users from Exiting Angular Pages

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]
  }
];        


Jad Azar

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

回复

要查看或添加评论,请登录

Jean-Paul Abi-Ghosn的更多文章

  • The One Line of Code That Silenced All Compile-Time Errors

    The One Line of Code That Silenced All Compile-Time Errors

    Introduction In my first months as an Angular developer, I discovered one of my modules was crashing with the error…

    1 条评论
  • Conditional Content Projection in Angular

    Conditional Content Projection in Angular

    Introduction In this article, I will discuss rendering certain elements inside a component based on those injected by…

  • Request cancellations in Angular

    Request cancellations in Angular

    I've noticed that many Angular developers often don't handle their subscriptions correctly when tasks are triggered by…

    5 条评论
  • Reusable custom form control components in Angular

    Reusable custom form control components in Angular

    Introduction While developing in angular, it is always imperative to follow the principle of "Do not repeat yourself"…

    6 条评论

社区洞察

其他会员也浏览了