The other day, Ward Bell and I were discussing both the future and existing routing features in AngularJS. In that conversation, we talked about what was and wasn't an appropriate responsibility for a routing module. In my opinion, the router itself shouldn't know when it's safe to navigate away from the current location; to me, that logic belongs in the Controller(s) that both understand and manage the current view-model. The controllers can hook into the $location (or $route) events and then examine the view-model in order to determine if the user should be prompted for location-change approval.
In this blog post, I'm using the $location service. But, the same functionality could be achieved using the $route service as well. It's important to understand that the $route service is nothing more than a thin layer built on top of the core $location service. As such, it provides no extra value in this particular context.
The key to prompting a user for a location-change confirmation is in understanding the $location lifecycle. There are two events triggered by the $location service:
- $locationChangeStart - fires before the location view-model and browser URL are synchronized.
- $locationChangeSuccess - fires after the location view-model and the browser URL are synchronized.
The $locationChangeStart event gives our application a hook into pre-synchronization validation, which is exactly what we want. In the $locationChangeStart event handler, we can call event.preventDefault(), which will cancel the location change entirely. Our controller can then prompt the user for change-confirmation; and, if the user agrees, we can re-initiate the location-change event.
In the following demo, I have a few links that will change the URL. Each time the user goes to change the URL, we're going to ask them if they really want to allow the navigation.
Because I am doing this in the root Controller, there's an edge-case that we have to work-around. Since the AppController and the $location service are being initialized in the same cycle, we run into an edge-case in which our AppController will pick up the $locationChangeStart event triggered during the $location service initialization. To get around this, we are putting our event-binding inside of a $timeout(). While this is a valid edge-case, it's less likely to be one that you encounter in your real-world application (since most Controllers are instantiated as a result of the location change, not before the location change).
If you look past the comments, there's really only a few lines of code here. When the Controller picks up the $locationChangeStart event, it cancels the location change and stores the target location data. Then, if the user confirms the location change, the Controller re-initiates the target location change.
Since most real-world scenarios would result in the destruction of the given controller (otherwise, why bother prompting the user), it would generally be sufficient to simply deregister the event bindings. But, in our case, since the Controller is never actually being destroyed, we have to deregister and then reregister the $locationChangeStart event binding so that the demo may continue to work properly.
Beyond the controller-based logic, the real secret sauce to this workflow is being able to prompt the user for data and then return that data in the form of a promise. In this case, I'm wrapping the core window.confirm() method in a promise; but, you could also create a simple modal window system in AngularJS that runs on promises.
Want to use code from this post? Check out the license.
Nice dive, as always! There's a module I found awhile back that does something very similar, but it is centered squarely around form submission and integrates well with formController. Been using it in production for awhile now. Here's the repo for anyone interested:
Cool directive. The whole Form ecosystem is really something that I need to learn more about. I've only scratched the surface, looking at the ngModelController. It seems like there's so much more to forms and the form lifecycle that I know nothing about. So much to learn, so few hours :D
Very good article, help me so much. Thank you.
Does this work for when the user writes the destination url in the searchbar? What about front and back button clicks? I could not get it to work for this.