LanguagesJavaScriptPreventing Data Loss In Angular Applications Using a CanDeactivate Route Guard

Preventing Data Loss In Angular Applications Using a CanDeactivate Route Guard

It’s always helpful when an application warns you when you’re about to leave a page with unsaved changes. In some cases, it’s expected. I recently had the occasion to implement that very functionality in an app for managing investment themes such as 5G, Green Energy, etc. As I came to discover, Angular 11 offers a highly versatile mechanism for preventing navigation from occurring under certain conditions. It’s called a Route Guard. In today’s tutorial, we’ll learn how Route Guards work by using one to protect our MatIcons survey from the previous couple of articles.

About the CanDeactivate Interface

The secret to route guards is the CanDeactivate interface. If a guard returns false, navigation is cancelled. The CanDeactivate interface implements a single method called canDeactivate(). It accepts several arguments and may return a variety of types for maximum versatility:

interface CanDeactivate<T> { 
    canDeactivate(component: T, 
                  currentRoute: ActivatedRouteSnapshot,
                  currentState: RouterStateSnapshot,
                  nextState?: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean
} 

Based on the result of canDeactivate(), the route will either be deactivated or allowed to continue. A result of false cancels the current navigation while a result of true allow navigation to continue.

Implementing the CanDeactivate Interface

A component can implement the CanDeactivate Interface directly, but a better approach is to employ a service to do so. We’ll generate one called DeactivateGuardService.

ng g s shared/services/deactivate-guard

These changes should be made to the Keeping Track of Form Changes In Angular demo. If you haven’t created that project yet, fear not; you can download it from codesandbox.io by choosing File -< Export to ZIP from the main menu.

Inside the deactivate-guard.service.ts file, we’ll create our own interface called “IDeactivateComponent”. That will allow us to pass any component that implements it to CanDeactivate. Our canExit() method is just a simplified version of canDeactivate that will determine whether or not it’s OK to proceed with navigation:

import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs';

export interface IDeactivateComponent {
  canExit: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable({
  providedIn: 'root'
})
export class DeactivateGuardService implements CanDeactivate<IDeactivateComponent>
{
  public canDeactivate(
    component: IDeactivateComponent
  ): Observable<boolean> | Promise<boolean> | boolean {
    return component.canExit ? component.canExit() : true;
  }
}

Referencing the Form from canExit()

If we now add implements IDeactivateComponent to the SurveyComponent class declaration, it must now include the canExit() method. That’s where we’ll give the user the opportunity to cancel navigation if the survey form contains changes. We can easily check for changes using the NgForm dirty property. The only problem is that we don’t currently have a reference to the form other than the one that is passed to onSubmit(). That’s easily remedied by adding a @ViewChild reference at the top of the class as follows:

@ViewChild('f') private ngFormRef!: NgForm;

Note the use of the ! non-null assertion operator. It tells the compiler that the variable won’t be null or undefined when it comes time to reference it, so don’t complain about the possibility of it being null or undefined. For the record, variables that reference elements via @ViewChild are instantiated by the AfterViewInit event.

With that taken care of, we can check for form dirtiness and bring up a prompt dialog if there are changes:

public canExit(): boolean {
  return this.ngFormRef.dirty
    ? window.confirm('You have unsaved changes.  Are you sure you want to leave the page?')
    : true;
};

Adding the Route Guard

All of the code that we’ve added thus far will do very little until we provide our service to the router. That is done in the AppRoutingModule where we declared the routes array:

const routes: Routes = [
  { path: "", component: HomeComponent },
  { 
    path: "survey", 
    component: SurveyComponent,
    canDeactivate: [DeactivateGuardService] 
  }
];

The canDeactivate attribute accepts an array of route guards. We’re only using one, so we’ll put it in the array.

Putting It All Together

All that’s left to do now is test out the route guard.

  1. Issue the ng serve –open command to open our app in your default browser.
  2. Open the Survey page and try clicking on one of the sentiment icons and/or entering some text in the comments textarea.
  3. Finally, click on the Home link. You should get a confirm dialog like the following:

    angular data protection

  4. Clicking OK will bring you to the Home page, while choosing Cancel will keep you on the Survey page with all your changes intact.

Conclusion

Angular 11 Route Guards provide a highly versatile means for preventing navigation from occurring under certain conditions. In today’s tutorial, we learned how Route Guards work by using one to protect our MatIcons survey from the previous couple of articles.

You can play with the demo of today’s project on Codesandbox.io.

Latest Posts

Related Stories