Angular: "Unsaved Changes Will be Lost" Route Confirmation
Tags: angular, forms, ngrx, router
Published: 2021-01-11 08:16:18

---
title: Angular: "Unsaved Changes Will be Lost" Route Confirmation
published: true
description: 
tags: Angular, Forms, NgRx, Router
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/9mucqsfhspwu86ax36ea.png
---

This article will walk through the use-case of creating an "Are you sure? Unsaved changes will be lost!" dialog in Angular that will keep a user on the current page, so they won't lose unsaved/unsubmitted changes to their forms by navigating away.

If you're looking for a quick fix for this use-case and source code is most helpful, here's a stackblitz of everything (with source code included!)

{% stackblitz ngrx-and-forms-and-can-deactivate %}

I'd encourage sticking around for the walkthrough though!

# The Use Case

We have a component with its own route to let a user change their name. We've noticed that if a user navigates away from the component without saving, they're sometimes a bit confused why their name has not updated.

To fix this, when the user attempts to route away from the component, we'll show them a dialog saying "Are you sure you want to leave this page? Unsaved changes will be lost!", and then give them the option to either stay on this page or navigate away.

# Our Tools

![Puzzle pieces](https://dev-to-uploads.s3.amazonaws.com/i/6ogn6rt17bbeqzlgod1r.png)

For this example, we'll be using:
+ Angular Reactive Forms as our forms library
+ `@ngrx/store` as our state management library
+ Angular Material for our dialog service
+ Angular Router for routing (especially the `canDeactivate()` feature)

Using other tools here is fine (and may be dictated by the constraints of your own use-case)! The basic ideas article should hold through. Afterall, we're essentially taking this set of tools and combining them together like puzzle pieces.

As long as we have comparable tools/APIs, you should be able to swap out any of these tools for another!

# Setting Up Our Global State!!
![NgRx lifecyle](https://ngrx.io/generated/images/guide/store/state-management-lifecycle.png)

For our global state, we'll be using `@ngrx/store` (we won't be using `@ngrx/effects`, but we will discuss how it could be used to handle making http requests for us - in case we needed to send the user's name to the server).

Let's follow the diagram to create our implementation.

## STORE

Looking at this problem, the user's name is state that belongs at the  global level. The user's name is shared across the app, and show we'd like a single point of truth for the user's name - so that when we change it, that change fiction-less-ly propagates across our app.

So we'll set up some initial assets/typing to reflect this:

`app.state.ts`:
```ts
export const NAME_FEATURE_IDENTIFIER = "name";

export interface NameFeatureState {
  firstName: string;
  lastName: string;
}

export interface AppState {
  [NAME_FEATURE_IDENTIFIER]: NameFeatureState;
}
```

^ Here we declare the interfaces for a "name feature" of our store. `NAME_FEATURE_IDENTIFIER` is the property name for our Store's state object. We'll export this, so we can use it when importing our `StoreModule` in our `AppModule` towards the end of this section.

The `NameFeatureState` interface then defines the single point of truth we'll use for our storing our name.

## SELECTOR

Given we've defined our STORE, we can now build some selectors that will serve as 'queries' into the store's current contents.

If we think about how we'll use this data across our app:
+ We'll need to select the user's full name to tell them 'hello!' in our `hello.component`
+ We'll need to separately select the user's first and last name in order to pre-populate our `name-form.component` with the user's starting name.

So we'll add some selectors here to provide our app with the queries into these specific pieces of data:

`app.state.ts`:
```ts
export const nameFeatureSelector = createFeatureSelector<NameFeatureState>(
  NAME_FEATURE_IDENTIFIER
);
export const selectFirstName = createSelector(
  nameFeatureSelector,
  state => state.firstName
);
export const selectLastName = createSelector(
  nameFeatureSelector,
  state => state.lastName
);
export const selectFullName = createSelector(
  selectFirstName,
  selectLastName,
  (first, last) => `${first} ${last}`
);
```

## COMPONENT

Nothing to do here yet!!

When we get into both our `hello.component` and our `name-form.component` later though, we'll need to import our selectors to `select()` the pieces out of our state, and `dispatch()` actions when appropriate.

## ACTION

Thinking about the relevant events in our use-case, the events that could exist in our application that would affect our name state is limited to our user submitting a new name via our Name Form. That action will need a payload of the form's current contents as well, which we'll include as props:

`state.app.ts`:
```ts
export const submitNameChange = createAction(
  "[Name Form] User Submit New Name",
  props<{ firstName: string; lastName: string }>()
);
```

## REDUCER

Our reducer is a function that takes an initial state and an action and returns a new state. We'll use `@ngrx/store`'s `[createReducer()](https://ngrx.io/api/store/createReducer#usage-notes)` here to set our initial state (what our state will be when the app loads), and define a reducer function for a `submitNameChange()` action (essentially resetting the store contents to the submitted value).

`app.state.ts`
```ts
export const reducer = createReducer<NameFeatureState>(
  {
    firstName: "Zack",
    lastName: "DeRose"
  },
  on(submitNameChange, (_, newName) => newName)
);
```

Now that we've completed the lifecycle, we can import the `@ngrx/store`'s `StoreModule` to our `AppModule`:

`app.module.ts`:
```ts
@NgModule({
  imports: [
    /* ... */
    StoreModule.forRoot(
      { [NAME_FEATURE_IDENTIFIER]: reducer }
    )
  ],
  /* ... */
})
export class AppModule {}
```

# Managing State in our NameForm component!

Initially, let's set up to use our `NameFormComponent` without the "are you sure??" dialog just yet.

Thinking about the state here
+ We'll want to grab a snapshot of the current name state from our `Store` to populate our form
+ We'll want Angular Reactive Forms to manage the local state of our form (and touch it as little as possible).
+ We'll want to disabled the `Submit` button if there is no difference between the form's state and the `Store`'s state. (This is definitely optional, but it's something I like to do as it helps with UX. We'll leverage the derived state here too of whether the form has changes for our logic of whether to show the dialog or not.)
+ Clicking submit should update our `Store` and navigate back to hello.

***NOTE:*** We could definitely also use the built-in `@angular/forms` `dirty` property here to disable/enable our submit button, and take the `Store` completely out of the equation. I like the solution I describe better as it will keep in sync in case `Store` state changes while the user is on the page. Also, this solution will catch scenarios where the user types something in and then reverts back to the original input. `dirty` won't catch that, and the user could think they've updated their name, when in reality it's the same as when they started.


## Initializing Form
Let's start with the first bullet here. I'm going to implement this with `async/await` and the `ngOnInit()` Angular lifecycle hook:

`name-form.component.ts`:
```ts
export class NameFormComponent implements OnInit {
  form: FormGroup;

  constructor(private _store: Store) {}

  async ngOnInit() {
    const firstName = await this._store.pipe(
      select(selectFirstName),
      take(1)
    ).toPromise();
    const lastName = await this._store.pipe(
      select(selectLastName),
      take(1)
    ).toPromise();
    this.form = new FormGroup({
      firstName: new FormControl(firstName),
      lastName: new FormControl(lastName)
    });
  }
}
```

Notice that using the `select()` operator with the `take()` operator, we're able to convert it to a promise. When selecting from the `Store`, the `Store` emits with a `Behavior` strategy - meaning it will immediately emit as soon as subscribed to. By calling `toPromise()` on the observable, we are effectively subscribing to it, cause it to immediately emit. The `take(1)` operator then causes the observable to complete, which causes the newly wrapped promise to resolve with that first emission.

Our form is now all set and we can let Angular Reactive Forms manage that state for us without touching anything else.

## Derived State: `formHasChanges$`

Using `Store` and our Reactive form, we now have an observable of both our 'global' state according to our `Store`, and our local state according to our form. These each are managing their respective state's source of truth.

We'll want to derive some state from these exposed observable streams to determine the derived state `formHasChanges$`.

To do this, we'll declare a public property on our class: `formHasChanges: Observable<boolean>;`

And to define this derived state, we'll reach for `RxJS`:

`name-form.component.ts`
```ts
  this.formHasChanges = combineLatest([
    this.form.valueChanges.pipe(startWith(this.form.value)),
    this._store.pipe(select(selectFirstName)),
    this._store.pipe(select(selectLastName))
  ]).pipe(
    map(([formValue, storeFirstName, storeLastName]) => {
      return formValue.firstName !== storeFirstName || formValue.lastName !== storeLastName
    })
  );
```

Using `combineLatest()` from RxJS, we'll start listening immediately to the value of our form, as well as the value in our store, and whenever either changes, we'll compare the values and determine if the form has changes when compared to the store.

I find this is especially helpful (over `formControl.dirty`) for making your forms feel more reactive/smart/well built, as you can toggle your submit button disabled (and any other feedback to the end-user that they have [OR don't have!!] a change on their hands). The reactive nature of this code also means that if we submit some change to a backend, we can react to the store changing as well.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8qj6k0am2znefr4ay3n2.gif)

For now we'll use this `Observable` to disable/enable the Submit button, but we'll also tap into this same stream in our `CanDeactivate` guard.

## `CanDeactivate` Guards

For the Angular Router piece of this puzzle, Angular has a built-in mechanism for preventing certain routing events - if you've spent some time in Angular, you're likely familiar with the concept of a guard.

Most of the time, these guards are in the context of preventing a user from accessing certain routes (for instance if a regular user attempts to route to a component that only an admin user should have access to). By setting the `canActivate` property of a given route to a [`CanActivate`](https://angular.io/api/router/CanActivate) guard, we can define the logic for whether or not a user may access that route.

`canDeactivate` is very much the same thing, but in reverse. Rather than defining the logic for whether a user can get to a component/route, this flavor of guard defines logic for whether or not a user can leave a certain component/route!

Before creating our actual guard, let's actually take care of most of the logic within our class (as we'll need some of the component state to inform our decision):

```ts
  async canDeactivate(): Promise<boolean> {
    if (this._cancelClicked || this._submitClicked) {
      return true;
    }
    const formHasChanges = this.formHasChanges.pipe(take(1)).toPromise();
    if (!formHasChanges) {
      return true;
    }
    const dialogRef = this.dialog.open<
      ConfirmDialogComponent,
      undefined,
      { response: "leave page" | "stay on page" }
    >(ConfirmDialogComponent);
    const { response } = await dialogRef.afterClosed().toPromise();
    return response === "leave page";
  }
```

Walking through this logic, we'll start with a check to see if the user is routing because they clicked 'cancel' or 'submit'. If so, we'll return true immediate to say 'Yes the user may leave.'

If we've made it past this check, we know that our user is trying to route away (maybe via clicking a navigation link for example). We'll want to check our `formHasChanges` Observable next to see if the user has left their form in a state where their form state doesn't match the store state. If there are no differences between the form and the store, there's no need to stop the user, so we'll return true at this point to again, let the user through!

If we've made to this point - we'll go ahead and open a dialog to inform our user that they have changes, and let them determine how to proceed.

![Asking the user how to proceed](https://dev-to-uploads.s3.amazonaws.com/i/s7tz618rytug4urdzfp7.png)

To proceed, we'll `await` the user's response, and if the user decides to `leave page`, we'll let them leave. (Note we're using the Angular Material Dialog API here, but it's likely most other dialog/modal Angular API's will have very similar APIs). Otherwise, we'll cancel the route event and return them to their form view.

That takes care of our logic, next we need to appropriately attach this logic to Angular's router.

To do this, we'll create a `name-form.can-deactivate.guard` that is pretty trivial - it simply references this logic we created in our component:

```ts
@Injectable()
export class NameFormCanDeactivateGuard
  implements CanDeactivate<NameFormComponent> {
  canDeactivate(component) {
    return component.canDeactivate();
  }
}
```

And finally in our `RouterModule` import of the `AppModule`, we'll set in the `canDeactivate` value for our route:

```ts
@NgModule({
  imports: [
    /* ... */
    RouterModule.forRoot([
      { path: "", component: HelloComponent },
      {
        path: "form",
        component: NameFormComponent,
        canDeactivate: [NameFormCanDeactivateGuard]
      }
    ]),
    /* ... */
  ],
  providers: [NameFormCanDeactivateGuard],
  /* ... */
})
export class AppModule {}
```

And voila!! We've created a proper `Are you sure you want to leave?` dialog!!

# More Content By Zack

[Blogs](https://zackderose.dev/blogs)
[YouTube](https://www.youtube.com/channel/UCIEY8ETWdW3EX-3nWE-T0dQ)
[Twitch](https://www.twitch.tv/zackderose)
[Twitter](https://twitter.com/zackderose)
[All Video Content Combined](https://zackderose.dev/videos)