NgRx / Angular Material - Dirty Form Checking Tutorial

Whoa! Hold on, are you sure you want to leave?

It's a common message warning users before exiting a page for any number of reasons. Maybe it entices the user to complete a purchase, or for less nefarious marketing reasons, warn for data loss on a partially filled form.

My product owners requested the prevention of data loss with partially filled forms.

I stumbled through how to accomplish this normal user experience task. Initially finding that Deactivating route guards can prevent navigation to the next page, I found many unsatisfactory methods of achieving this request. Many of the tutorials focused on using the route's component to deactivate, but if forms nested deep into the component hierarchy, how do I crawl the component tree easily? That didn't seem feasible.

Tracking forms dirty state could be accomplished through tracking value changes of a FormGroup and dispatching events to NgRx.

So in tutorial, I'll show how to prevent users from navigating away if there's dirty data in a form using NgRx, Angular Material's Dialog, Route Guards, and the FormGroup.

Goals

  1. Create a NgRx Store for handling checks for dirty forms
  2. Create a BaseFormComponent class to handle form value changes
  3. Create the RouteGuard implementing CanDeactivate and Material Dialog to confirm the user wants to leave.
  4. Update the Routes

1. Create the Store

The store contains a key / value map of { [id: string]: boolean }. This provides flexibility to id the form and check if the form is dirty.

Three actions will UPSERT, REMOVE, and CLEAR all the form references in our state.

The reducer updates the form IDed with the boolean value of dirty property of the FormGroup.

The selector chooses the state and whether there exists dirty forms.

2. Create a BaseFormComponent class

The responsibility of the BaseFormComponent class will be notifying our DirtyFormStore if the form changed values, with a check if the form is dirty.

First, create an abstract property public abstract formGroup: FormGroup. The consuming component will be responsible for initializing the formGroup.

Add another public property, destroy$: Subject<void>. This will be used to complete the stream on valueChanges of the formGroup property.

In the constructor, inject the Store service and set an id to be used to identify the form in our DirtyFormStore.

Implement the Init and OnDestroy interfaces on the BaseFormComponent.

The ngOnInit creates a stream to watch for value changes of the formGroup property. As the value of the form changes, actions dispatch to notify the form changes as dirty or not.

In the ngOnDestroy method, an action dispatches to remove the referenced form id from the store when the component is destroyed.

Step 3 - Create a Deactivate Guard*

The deactivate guard's responsibility ensures the user chose to leave the route with dirty data in the form.

Utilizing the DirtyFormSelector, check if there exist any dirty forms. Should there exist any dirty forms, open a Material Dialog component.

The ConfirmationModalComponent supplies whether the user chose to leave the page or stay.

*As a note of caution when using this deactivate guard, I found that route parameters change from my NgRx router serializer as certain router events get dispatched and change the route parameters. Selectors consumed these route parameters and caused dependent data and removed the relying DOM elements. After clicking cancel, things rendered normally.

Step 4 - Update Route(s)

Depending which routes and component forms exist, the DirtyFormGuard can be added to the top level of your routes. Add the DirtyFormGuard to the deactivate property on the route.

Summary

canDeactivate prevents navigation to the next route. We can use the data within NgRx's store to check for conditions of dirty forms. Using Angular Material, we display a dialog making sure the user chooses to orphan the data. The BaseFormComponent class helps add and remove the form from the store given changes to the formGroup and destroying the component.