Where were we?
In the last post, I introduced saNavigationGuard, an AngularJS factory that provided a mechanism for different components to guard against the user navigating away from the page when it was not desirable. This used the $window.onbeforeunload event and therefore, only works when navigating outside of the AngularJS application. For in-app navigation, we need to monitor a different event that Angular provides called $locationChangeStart.
$locationChangeStart
In the following code I have expanded saNavigationGuard to include support for $locationChangeStart.
angular.module('somewhatabstract').factory('saNavigationGuard', ['$window', '$rootScope', function($window, $rootScope) { var guardians = []; var onBeforeUnloadHandler = function($event) { var message = getGuardMessage(); if (message) { ($event || $window.event).returnValue = message; return message; } else { return undefined; } }; var locationChangeStartHandler = function($event) { var message = getGuardMessage(); if (message && !$window.confirm(message)) { if ($event.stopPropagation) $event.stopPropagation(); if ($event.preventDefault) $event.preventDefault(); $event.cancelBubble = true; $event.returnValue = false; } }; var getGuardMessage = function () { var message = undefined; _.any(guardians, function(guardian) { return !!(message = guardian()); }); return message; }; var registerGuardian = function(guardianCallback) { guardians.unshift(guardianCallback); return function() { var index = guardians.indexOf(guardianCallback); if (index >= 0) { guardians.splice(index, 1); } }; }; if ($window.addEventListener) { $window.addEventListener('beforeunload', onBeforeUnloadHandler); } else { $window.onbeforeunload = onBeforeUnloadHandler; } $rootScope.on('$locationChangeStart', locationChangeStartHandler); return { registerGuardian: registerGuardian }; }]);
In this new version of saNavigationGuard, the getGuardMessage method has been introduced that is responsible for querying the guardians for a reason not to allow navigation. In addition, an event handler has been added on $rootScope to handle $locationChangeStart. This handler is then used to query the guardians and display a confirmation message.
To display the confirmation message, I settled on using $window.confirm. Originally, I had thought about allowing a custom experience instead, but I felt that, due to any alternative experience being non-modal, the state management to allow a navigation to continue would be overly complicated.
Finally…
I was expecting the modifications to support $locationChangeStart to be more difficult than it turned out, though there are certainly some improvements that can be made1.
In the next post, I am going to show how we can track pending web requests and prevent navigation accordingly. In the meantime, please comment and let me know if you have other uses for saNavigationGuard or even alternative implementations.
- For example, the old and new URLs that are given as arguments to the $locationChangeStart handler could be passed to the guardians in order for them to make more informed decisions. [↩]
This is just brilliant. Cheerios!
Thank you!
How to integrate a "$scope.form.$dirty" check before prompting the user?
In your registered handler, you could easily add a check for that and then decide if you wanted to prompt the user or not. If you don't want to, just return an empty string or other falsy value.
How to identify back button click during a navigation and navigate after user confirms on the popup, so that history is handled properly.
I am not sure and I haven't worked in Angular for a long time now. I recommend hitting up StackOverflow.com – folks there will be able to help.