How to use a custom dialogue with the canDeactivate route guard in Angular
Sometimes, when you have your users in the middle of a workflow or form completion, you want to be helpful and offer a warning if they try to navigate away from what they’re in the middle of. Using Angular you accomplish this with the concept of Route Guards, specifically the canDeactivate guard. The tutorial in the Angular documentation is good, but I feel that it leaves out some useful specifics that I will try to cover in this post.
The route guard foundation
The first step is to set up a guard service. This follows closely the first step in the Angular docs, the only differences being a different name for the interface (makes more sense to me), and a slightly different implementation of the same. This way of writing an interface is more in line with the TypeScript specification and allows your IDE to automatically generate the correct code for implementing it.
So now we have a CanDeactivateGuard class that implements the built-in interface CanDeactivate (don’t forget to provide the class in the appropriate angular module). We can now apply this class to any route we wish in our router:
What happens now is that whenever a user tries to navigate away from our route the route guard is going to kick in. Its canDeactivate function is going to run, and, as you can see, this function receives a component as parameter. This is the component that is associated with the path. Now the canDeactivate function is going to look at the component and see if the component has a function called canDeactivate. If it does (which it should since it will be implementing our interface) that function will be executed and the result will be returned back to the guard, and then returned from the guards’ function.
The value that is returned here can either be a boolean or an Observable/Promise that emits/resolves to a boolean. If it’s a boolean, then the navigation is going to be executed or aborted, straight away. If it’s an Observable then nothing is going to happen until the observable emits a value. Then the navigation will either execute or abort.
Now, let’s implement the interface in our component:
For now we can let the function return true all the time and navigation will be no different from how it was before, without the guard.
Next, let’s ask the user what they want to do, and return a value depending on their answer…
Enabling a custom dialog
I’m not going to go into details on how to set up a universal modal system in your application, but I’m using an approach that is very close to this tutorial by Brian Childress. It involves a modal service that is used to open and close modals across the application. It also allows for putting any custom html and components inside the generic modal. This custom content is what we’ll be focusing on.
Prepare a modal for opening, and then open it when the user tries to navigate away. Note that I put the modal in the root of the application — in the app.component template. This is because the modal service can activate a modal from anywhere in the app, regardless of where it has been created. This will also allow any route you decide to guard to utilize the same modal, instead of each guarded component having its own. (But feel free to put the modal anywhere you want, as long as it exists at the time you want to open it.
Tying it all together
Instead of returning true in the components canDeactivate method, we’ll now return an Observable<boolean>. This observable can come from anywhere really, I decided to declare mine in the ModalService class, just to keep things that has to do with modals in one place.
As you can see it’s not an ordinary observable, but a Subject, which is an observable that is also an observer. The subject also has a handy function we want to use that is not present on the regular observer: next() which causes the subject to emit a new value on command.
Create a new component for the content of your question modal. Put a couple of buttons in the template for the user to click, and tie them to a function in the component that triggers an emission of the observable we returned from the canDeactivate-method.
And that’s it! Now, when a user tries to navigate away from our route the following will happen:
- The guard will look for a canDeactivate-method on the corresponding component.
- The method is found and executed, which does two things:
- A modal is opened
- An observable is returned to the guard
- The user clicks one of the buttons in the modal
- The same observable that was just returned to the guard emits a value, depending on the users choice, and the appropriate navigation action is triggered.
In its current state the modal will be displayed any time the user tries to navigate away from our route. If you want to be less annoying and only prompt the user in cases where there has been changes made to the page (or whatever it is you might want to check for) add the logic in the components canDeactivate method, and if there is no need for a modal, return a plain ol’ true before it even opens.
I think this is a great example of how Angular makes use of observables to handle asynchronous events, in this case user input. It allows for an elegant solution and good architechture and I’m sure similar behavior could be implemented within other frameworks and it could be something to try out.
Hit me up on the Twitter for more blog posts like this one.