Backbone.js declarative is-key events

You want to hide a dom element when some key triggers “keyup”. This is a fairly simple scenario where your code just might end up looking like this.

var DialoadView = Backbone.View.extend({
	el: '#dialog',

	events: {
		'keyup': 'closeDialog'
	},

	closeDialog: function (e) {
		if (e.which === 27)
			this.$el.addClass('hidden');
	}
});

So what is the problem? The problem is that closeDialog() has two very clear and very different responsibilities.
1. Determine what key triggered the “keyup” event
2. Add class ‘hidden’ to the DOM element

These two actions should really not be mixed. Yet we don’t want to add closeDialogIfKeyIsEscape() and have this method call two other methods determining key and manipulating DOM, respectively. Instead, we would like to make use of common “is-key” logic that could be used by any function/eventhandler of the View.

Let’s explore a few options.

First try

We could simply wrap the callback of Backbones delegate events. This would allow us to do the following.

var DialoadView = Backbone.View.extend({
	el: '#dialog',

	events: {
		'keyup': ifIsKey(27, 'closeDialog')
	},

	closeDialog: function () {
		this.$el.addClass('hidden');
	}
});

Which would require an implementation similar to the following.

function ifKeyIs(keyCode, method) {
	var slice = Array.prototype.slice;

	return function (e) {
		if (!_.isFunction(method))
			method = this[method];

		if (e.which === keyCode)
			method.apply(this, slice.apply(arguments));
	};
}

Problem

After having implemented this, we soon realize it was an incomplete solution. As long as we only need to listen for a single type of key within a View, we don’t run into trouble. But if different keys need to call different methods then we are out of luck. Properties on objects are unique making it impossible to have multiple declarations as shown by the following.

events: {
	'keyup': ifIsKey(27, 'closeDialog'),
	'keyup': ifIsKey(13, 'submitDialog') // <- Overrides first property
}

Second try

By extending the logic of the property key, instead of the value, we can guarantee uniqueness. We would like to implement functionality that would let us do the following.

var DialoadView = Backbone.View.extend({
	el: '#dialog',

	events: {
		'keyup:iskey(27)': 'closeDialog',
 		'keyup:iskey(13)': 'submitDialog'
	},

	closeDialog: function () {
		this.$el.addClass('hidden');
	},

	submitDialog: function () {},
});

This is much better. We are able to define an event handler for each unique “keyup” event. It is completely reusable by any View and closeDialog() now only has the responsibility of manipulating the DOM element.

Let’s look at an implementation. We are going to intercept the delegateEvents method of the Backbone View prototype. We decorate this with extra behavior before calling the original base function. The Backbone architecture allows us to do this without any hassle. The following would be an implementation of this.

;(function () {
	var slice = Array.prototype.slice;

	function ifKeyIs(keyCode, method) {
		return function (e) {
			if (e.which === keyCode)
				method.apply(this, slice.apply(arguments));
		};
	}

	var delegateEvents = Backbone.View.prototype.delegateEvents,
		eventKeyNameSplitter = /^(\S+):iskey\((\d+)\)\s?(.*)$/;

	Backbone.View.prototype.delegateEvents = function (events) {
		if (!(events || (events = _.result(this, 'events'))))
			return this;

		for (var key in events) {
			var method = events[key],
				match = key.match(eventKeyNameSplitter);
			
			// Does key follow :iskey(...) schema?
			if (!match)
				continue;
			
			// Delete original key
			delete events[key];

			// Map method-key to function if not already a function
			if (!_.isFunction(method))
				method = this[method];

			if (!method)
				continue;

			// Add sanitized key
			var keyCode = parseInt(match[2], 10);
			events[match[1] + ' ' + match[3]] = ifKeyIs(keyCode, method);
		}

		// Run everything as normal
		delegateEvents.apply(this, slice.apply(arguments));

		return this;
	};
}());

That’s it. This code must be loaded after backbone.js but before any View is initialized. Any View will be able to declare :iskey(…) when attaching an events handler through the backbone “events” object. A simple declarative implementation of crosscutting functionality.

Dotless dynamic server-side values

Using the dotless HttpHandler, we can easily start transforming our less files into rendered CSS. But if we want to manipulate variable values before it is processed, we have to do a bit of manual work. Let's take a look at how this can be … [Continue reading]

MSBuild Task dll locked by Visual Studio

Through MSBuild we are able to control how we wish to process and build our software. A nice feature of this is the ability to create custom Tasks. A Task is a unit of executable code used by MSBuild to perform atomic build operations. … [Continue reading]

Do Git file renaming by itself

Often it would seem that you can throw anything at Git and it will automatically know exactly what you mean. Sometimes, though, Git simply does not have a fighting chance! Git recognizes file content and not names meaning it uses an algorithm to … [Continue reading]

JavaScript function memoization

I have previously written about JavaScript functions having properties. I provided a fairly simple example of how this works. Another way this can be utilized is when doing function memoization. Memoization optimizes speed of function execution by … [Continue reading]