Navigation guard using AngularJS: $locationChangeStart

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.

  1. 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. []

Navigation guard using AngularJS: onbeforeunload

onbeforeunload

I have been working on an add-on for one of our products that includes some data entry components. As part of this, I wanted to make sure that any changes to the data were saved before the user lost interest and went searching the Internet for cat pictures. I quickly turned to the window.onbeforeunload event.

The onbeforeunload  event is the last chance for a website to get the user's attention before they have navigated away to something more shiny or sparkly. In AngularJS, since a real navigation is not going to occur in a single-page application, two events are provided, $locationChangeStart  and $locationChangeSuccess, that can be used to similarly intercept in-app navigations.

Through onbeforeunload and the subsequent browser-based dialog, or the AngularJS events and a subsequent app-based dialog, the user is given the opportunity to cancel the navigation and address something important like unsaved data or a cute cat photo that they almost missed.

So, with events available to intercept navigation, I started hooking them up in all the places that needed them. A directive here, a factory there; before long I was duplicating almost the same code in nearly 20 places. There had to be a better way, so I created the saNavigationGuard service1.

saNavigationGuard

The saNavigationGuard service provides consumers with the ability to register callbacks, known as guardians, that will be called if a navigation event is detected. If a guardian wishes to prevent that navigation, it returns a message explaining why. The saNavigationGuard then signs up to the relevant events and when they occur, asks each guardian if it should go ahead. Whichever guardian returns a string first, wins, and the guardian acts accordingly to prevent the navigation.

In the case of onbeforeunload , the browser continues the event handling by offering the user a choice to continue or cancel the navigation. In the case of the $locationChangeStart  event, this part must be handled by the application. We will come back to how in another post; for right now, let's just look at the onbeforeunload scenario.

Here is the saNavigationGuard factory declaration:

angular.module('somewhatabstract').factory('saNavigationGuard', ['$window', function($window) {
	var guardians = [];

	var onBeforeUnloadHandler = function(event) {
		var message;
		if (_.any(guardians, function(guardian) { return !!(message = guardian()); })) {
			(event || $window.event).returnValue = message;
			return message;
		} else {
			return undefined;
		}
	}

	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;
	}

	return {
		registerGuardian: registerGuardian
	};
}]);

First thing to note is on line 6 where I have used the any method from underscore (or lodash). This calls a predicate for each element in an array until the predicate returns true. In this case, we are passing our array of guardians and the predicate is calling that guardian to see if it currently wants to stop navigation. If it does, it will return a message.

If one of the guardians returns a message, the message it returns is captured and the predicate returns true. The message is then passed to the event so that the browser will show its dialog. If no guardian returns a message, then the browser is allowed to continue with the navigation unhindered (at least by this code).

Because of how factories are instantiated in Angular, the initialization code that actually signs-up to the onbeforeunload event only occurs once per Angular application. So, injecting this factory into your directives, factories, etc. means the event handling will be initialized just once. This gives a central point to control interception of navigation while allowing individual components to prevent navigation for their own reasons.

Usage

Here is a simple example of how this might be used:

angular.module('somewhatabstract').controller('saEditorThingyCtrl', ['$scope', 'saNavigationGuard', function($scope, saNavigationGuard) {
    var editing = false;
    var navigationGuardian = function() {
        return editing ? "Edit in progress" : undefined;
    };
    
    saNavigationGuard.registerGuardian(navigationGuardian);

    $scope.startEdit = function(){
        if (editing) return;
        editing = true;
        // Stuff happens
    };
    $scope.endEdit = function() {
        if (!editing) return;
        editing = false; 
    };
}]);

Of course, this is a contrived example but it illustrates how easy it is to put the saNavigationGuard to work. In fact, if you didn't want your guardian to be called all the time because you knew it usually would not block navigation, you could only have it registered when needed.

    ...
    var unregisterGuardian;
    
    $scope.startEdit = function(){
        ...
        unregisterGuardian = saNavigationGuard.registerGuardian(navigationGuardian);
        ...
    };
    $scope.endEdit = function() {
        ...
        unregisterGuardian();
        unregisterGuardian = undefined;
        ...
    };
    ...

This ability to register and unregister guardians brings me to my second noteworthy aspect of saNavigationGuard. When registering a new guardian, it is added to the start of the array of guardians rather than the end. This is because, in my use case, the more recently registered guardians are often the most urgent. I suspect this may be common for most uses.

Finally…

This post is the first in a series covering some useful things I have created in my experiences with AngularJS. In the posts to follow, I will be covering how we can expand saNavigationGuard to cover in-app navigation and how we can intercept web requests to provide both navigation guards and busy indication.

I hope you find the saNavigationGuard factory as useful as I have on my current project where I have guardians for active edits and when there are pending POST, PUT or DELETE requests.

If you use this or anything else that I have written about in your project or want to suggest some tweaks or alternatives, please don't forget to leave a comment.

  1. It's really a factory in the AngularJS world, but it's providing a service…oh, I'm not getting into this discussion again, just look up service vs. factory and read about why it doesn't matter as long as you choose. []

CodeMash 2.0.1.4

Adventure

It is almost nine years since I first set foot in the US. It was through that experience that I rediscovered the joy in challenging myself and embracing change, something I had not so strongly felt since I first started singing in a band. So, while I had faced challenges before as a result of my own decisions, none had been bigger. Even though the opportunity had been provided by someone else, it had been my choice to take it and to see it through1.

It took me a while to settle in to my new home (or even to acknowledge it as home), but I eventually joined the developer community in Ann Arbor and the wider mid-west region. The interaction with other developers has continued to provide challenging opportunities and encourage positive change within my career, as well other aspects of my life. It was through the basic act of attending one local Ann Arbor .NET Developers Group meeting and the people I met there that I learned about CodeMash.

CodeMash

CodeMash v2.0.1.4 logo
 
The CodeMash conference – a community-organized event held annually in Sandusky, Ohio – never fails to provide unique experiences or challenges. My first CodeMash, CodeMash v2.0.1.2 was unique because I had never attended a developer conference before (or any other conference), and CodeMash v2.0.1.3 provided a completely new experience when, after attending a fantastic workshop on public speaking, I went on to win the PechaKucha contest.

This year, I was guaranteed yet another unique experience when I was accepted to be a speaker. I am extremely grateful to friends, mentors and others for their support and encouragement leading up to speaking at CodeMash v2.0.14. It was a wonderful honor that I thoroughly enjoyed, and while it changed my CodeMash experience with the added anxiety of speaking and subsequent release when my session ended, I would definitely do it again if the chance arose.

To those that attended my talk on AngularJS for XAML developers, thank you. I  hope that you found it valuable. If you were there or if you have an interest, you can find my slide deck and code on GitHub (Deck|Code).

I am very grateful to the volunteers that organize and run CodeMash each year, as well as the many friends and mentors that have guided my own CodeMash experiences and the many other experiences within the developer community. Without these people, I would not have had such amazing opportunities, nor would I have learned how important it is to challenge myself and strive for new experiences. It is always uncomfortable to embrace change, but the rewards of doing so are often worth the pain.

To close, I encourage you to challenge yourself this year. Make sure to let me know in the comments below how you will challenge yourself and perhaps we can follow-up at the end of the year.

  1. Of course, there were many times in the weeks between being offered the position and setting foot in the US when I considered changing my mind, including just after the plane doors closed []

Creating and using your own AngularJS filters

I have been working on the client-side portion of a rather complex feature and I found myself needing to trim certain things off a string when binding it in my AngularJS code. This sounded like a perfect job for a filter. For those familiar with XAML development on .NET-related platforms like WPF, Silverlight and WinRT, a filter in Angular is similar to a ValueConverter. The set of built in filters for Angular is pretty limited and did not support my desired functionality, so I decided to write new filter of my own called trim. I even wrote some simple testing for it, just to make sure it works.

Testing

For the sake of argument, let's presume I followed TDD or BDD principles and wrote my test spec up front. I used jasmine to describe each of the behaviours I wanted1.

describe('trim filter tests', function () {
	beforeEach(module('awesome'));

	it('should trim whitespace', inject(function (trimFilter) {
		expect(trimFilter(' string with whitespace ')).toBe('string with whitespace');
	}));
		
	it('should trim given token', inject(function (trimFilter) {
		expect(trimFilter('stringtoken', 'token')).toBe('string');
	}));
		
	it('should trim token and remaining whitespace', inject(function (trimFilter) {
		expect(trimFilter(' string token ', 'token')).toBe('string');
	}));
});

An important point to note here is that for your filter to be injected, you have to append the word Filter onto the end. So if your filter is called bob, your test should have bobFilter as its injected parameter.

Implementing the Filter

With the test spec written, I could implement the filter. Like many things in Angular that aren't directives, filters are pretty easy to write. They are a specialization of a factory, returning a function that takes an input and some arbitrary parameters, and returning the filter output.

You add a filter to a module using the filter method. Below is the skeleton for my filter, trim.

var myModule = angular.module('awesome');

myModule.filter( 'trim', function() {
    return function (input, tokenToTrim) {
        var output = input;
        // Do stuff and return the result
        return output;
    };
});

Here I have created a module called awesome and then added a new filter called trim. My filter takes the input and a token that is to be trimmed from the input. However, currently, the filter does nothing with that token; it just returns the input. We can use this filter in an Angular binding as below.

<p style'font-style:italic'>Add More {{someValue | trim:'Awesome'}} Awesome</p>

You can see that I am applying the trim filter and passing the token, "Awesome". If someValue was "Awesome", this would output:

Add More Awesome Awesome

You can see that "Awesome" was not trimmed because we didn't actually implement the filter yet. Here is the implementation.

myModule.filter('trim', function () {
	return function (input, token) {
		var output = input.trim();

		if (token && output.substr(output.length - token.length) === token) {
			output = output.substr(0, output.length - token.length).trim();
		}
		return output;
	};
});

This takes the input and removes any extra spaces from the start and end. If we have a token and the trimmed input value ends with the token value, we take the token off the end, trim and trailing space and return that value. Our binding now gives us:

Add More Awesome

Perfect.

  1. Try not to get hung up on the quality of my tests, I know you are in awe []

jQuery validation of select elements where multiple="multiple"

I was recently working on a simple feature in our web client at work. Part of the feature required that the user be able to select one to many items from a multiple select list. My first stab at this worked great; I added the required class to the select element and voila, the user was limited to at least one item. However, I thought it would be nice to give a custom error message with a little more context than just "this field is required" and perhaps provide the ability to limit the selection to a variable minimum and maximum length.

I quickly discovered that I don't understand the jQuery Validation plug-in and the documentation seems to be written by someone who thinks I might know way more than I actually do1. My next step was to find examples but they were all related to comparing element values on more standard input elements, which just served to confuse me further, so I embarked on a quest to find out how to solve my problem and write a blog about it.

Now, I'm sure that what I am about to document will seem like child's play to the jQuery ninjas out there, but for every ninja equipped with the skills to silently dispatch every henchman in the room, there's someone like me who just wants to get out of the room alive. So, please bear that in mind as you read on and whether a ninja or just a survivor, please comment.

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>My jQuery real good fun time yeah page</title>
    <script src="http://code.jquery.com/jquery-1.9.1.js" type="text/javascript"></script>
    <script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.11.1/jquery.validate.js" type="text/javascript"></script>
    <script type="text/javascript">
        /* Placeholder for my amazing coding skills. */
    </script>
</head>
<body>
    <form id="mySuperCoolForm">
        <fieldset>
            <legend>Select some stuff</legend>
            <select name="things" multiple="multiple">
                <option>thing 1</option>
                <option>thing 2</option>
                <option>thing 3</option>
                <option>thing 4</option>
                <option>thing 5</option>
            </select>
            <input type="submit" />
        </fieldset>
    </form>
</body>
</html>

This is the simple HTML page that I'm going to work with. There's nothing complex here, just a form with a select and a submit input. I have also added some script imports for jQuery and the jQuery validate plug-in and an empty script element for our validation definitions to live.

To this, we want to add some validation to make sure that at least one element in the list is selected when the form is submitted. First, just to check things are wired up correctly, we will just specify the select named "things" as being required. Rather than do this by just adding the required class, let's use some scripting, that way we can manipulate the script as we go.

$(function () {
    $('#mySuperCoolForm').validate({
        rules:{
            things:{
                required:true
            }
        }
    });
});

This code is not doing much. It is telling the jQuery validate plug-in to attach its validation to our form, mySuperCoolForm with the rule that our field, things is required. If you try this out and click the submit button without selecting anything, you'll get a message stating, "This field is required." Not a very descriptive message, so let's fix that by adding a message for our "things are required" rule.

$(function () {
    $('#mySuperCoolForm').validate({
        rules: {
            things: {
                required: true
            }
        },
        messages: {
            things: {
                required: 'Please select at least one thing.'
            }
        }
    });
});

Again, not a very complex change. I have added a messages field to the JSON object being passed to the validate method. This field provides messages that should be displayed when validation rules fail. In this case, we are stating we want at least one thing to be selected whenever our required rule fails. You should note the correlation here between the name of our field, things and the rules and messages that are attached to it. This is important to understanding how to manipulate the jQuery validation. When a rule fails to validate for a named field, the message under the same named field with the matching name to that failed rule will be displayed.

That works nicely so it's job done, right? Not quite. You see, while this works for the cases where one to many items need selecting, it feels a little hacky. I'd rather say explicitly "I want x items selected" rather than rely on the required rule working the way it does for multiple select scenarios. I like being explicit. So, let's get explicit2.

Validating with minlength

It just so happens that the jQuery validation plug-in has another rule, minlength that appears to be exactly what we want. Thinking this, I naively wrote some code to require at least two items. I also included some string formatting for the error message so that the number of things in the message matched the number required by the rule.

$(function () {
    $('#mySuperCoolForm').validate({
        rules: {
            things: {
                minlength: 2
            }
        },
        messages: {
            things: {
                minlength: $.format('Please select at least {0} things.')
            }
        }
    });
});

Now, if we select two things, we can submit just fine, but if we select just one thing, we get told we need at least two. So far so good. However, if we select nothing, the form passes validation and submits fine! What? We stated a minimum of two and yet zero passes. That makes no sense. It made even less sense when I set my minlength to 1 and it didn't appear to do any validation at all. Everything was feeling so intuitive, what happened? By looking at the code, it becomes clearer.

The minlength rule looks like this:

minlength: function( value, element, param ) {
			var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element);
			return this.optional(element) || length >= param;
		}

If you put a breakpoint here, you'll discover that when nothing is selected, value is null, but that's okay because length becomes `0` in that circumstance anyway. The problem is on the second line of the function, which I have highlighted. It checks to see if the element is optional or not and if it is optional, the element passes validation regardless. A quick look at the optional function shows that it calls the required rule and returns the inverse of its result. When we have no items selected, required returns false which optional turns to true which incorrectly says our data is valid. However, when only one item is selected, required returns true which means the `length >= param` check occurs and our validation fails as we would like. While this behaviour is probably intuitive for say, string input fields where no value and values of a certain minimum length make sense, this is confusing when using minlength with select fields.

To get our validation working as we would like, we have to apply both the required and minlength rules as follows:

$(function () {
    $('#mySuperCoolForm').validate({
        rules: {
            things: {
                required: true,
                minlength: 2
            }
        },
        messages: {
            things: {
                required: 'Please select at least 2 things.'
                minlength: $.format('Please select at least {0} things.')
            }
        }
    });
});

Unfortunately, now we've lost the nice dynamic nature of our error messages. If we change the minlength rule to 3, we have to remember to edit the message for required. It seems we have gone from "feels hacky" to "feels hackier".

What I really want is for required to behave differently when I have the minlength rule applied to a select element. When using minlength on a select element, I want required to default to true but my error message to come from the minlength message format. This would feel far more intuitive for this use case than the current behaviour does. So, given that the plug-in does not do this, what can we do to fix it?

Implementing an intuitive minlength

The jQuery Validation plug-in provides the ability to add custom validation methods. Not only does this allow the addition of new methods, but it also allows for the replacement of existing methods. This gives us two options:

  1. Create a new minlengthofselection or similar that follows the rules we want.
  2. Override the existing minlength that does things the way we want.

While the first option is probably safest for those already familiar with the behaviour as it stands, option two is more fun. Guess which one I did3.

$(function () {
    $.validator.addMethod('minlength', function (value, element, param) {
        var length = $.isArray(value) ? value.length : this.getLength($.trim(value), element);

        if (element.nodeName.toLowerCase() === 'select' && this.settings.rules[$(element).attr('name')].required !== false) {
            // could be an array for select-multiple or a string, both are fine this way
            return length >= param;
        }

        return this.optional(element) || length >= param;
    }, $.format('Please select at least {0} things.'));

    $('#mySuperCoolForm').validate({
        rules: {
            things: {
                minlength: 2
            }
        },
        messages: {
            things: {
                minlength: $.format('Please select at least {0} things.')
            }
        }
    });
});

In this final update to the code, I provided a new implementation to the validator for minlength. This new version is almost identical to the default except that when checking a select element, it looks to see if the field is explicitly not required, thereby assuming that we want something to be selected. Finally, we have a minlength validation check that works intuitively for our scenario as well as the more common scenarios surround string lengths and we don't have to hard code numbers into the error message.

Conclusion

In conclusion, I ran into what is probably a corner case when it comes to using jQuery Validation where things were not as intuitive as I had hoped. In doing so, I was able to learn more about the plug-in and eventually, resolve my specific issue.

  • Have you had a similar problem?
  • Do you have improvements to suggest?
  • Should I submit this minor change for inclusion in the jQuery Validation plug-in?

Please, comment and let me know.

Acknowledgements

I would like to thank, alen, the author of this tutorial which gave me a foundation to work from when investigating and resolving this issue.

  1. Turns out the real reason was my expectations of what should work and the reality of what did work did not match, making the documentation hard to interpret. []
  2. Not in that way! []
  3. Hint: It's not the first one. []