NgRx / Angular Material - Dirty Form Checking Tutorial
Dirty data in your forms? Walk through this tutorial using NgRx and Route Guards to check if the user wants to leave the page.
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
- Create a NgRx Store for handling checks for dirty forms
- Create a
BaseFormComponent
class to handle form value changes - Create the
RouteGuard
implementingCanDeactivate
and Material Dialog to confirm the user wants to leave. - 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.