How to use javascript event bindings to make callbacks cleaner
Support this website by purchasing prints of my photographs! Check them out here.I've been working a lot with Javascript lately, which is quite a bit different from my usual PHP development. For one thing, PHP is a traditional, class-based programming language, intended to be used on the server. It is stateless, and the application executes in under a second and is forever done. Javascript, however, is a functional language (everything is a function, assign functions to variables, functions in functions, OMG FUNCTIONS), the execution of code is often event based, and the code runs for a long time.
Anyway, when building an app in javascript, one often finds them self nesting functions very deep. This code is not only ugly, but gets un-maintainable, and it is hard to call the code deep within the nested functions from elsewhere in the application. Consider the following jQuery AJAX code:
var ResourceGrabber = function() {
getResources: function(selectedResource, selectedResource2) {
$.ajax({
url: '/api/resource1',
dataType: 'json',
success: function(result) {
var $container = $('select#container');
$container.empty().append("Select a Resource1...");
$.each(result, function(index, value) {
$container.append("<option>" + value.Resource.name + "</option>");
});
if (selectedResource) {
$container.val(selectedResource);
}
$.ajax({
url: '/api/resource2',
dataType: 'json',
success: function(result) {
var $container2 = $('select#container2');
$container2.empty().append("Select a Resource2...");
$.each(result, function(index, value) {
$container2.append("<option>" + value.Resource2.name + "</option>");
});
if (selectedResource2) {
$container.val(selectedResource2);
}
}
});
}
});
}
}
ResourceGrabber.getResources('alpha', 'beta');
A quick explanation of this code: we are requesting some data from the server (resource 1), building out our dropdown with different options for each item we get in our request, and selecting a default item. Once that is done, we then trigger another ajax request (perhaps the second request is dependent on data retrieved from the first request, although in this example it is not) so that we build out another dropdown list.
But, the code for this quickly gets unwieldily. Each time we want to add another function to our code, we have to paste the code within existing functionality. Before long, you've got a dozen level of indents and it's impossible to say run the Resource 2 grabbing code without first grabbing the Resource 1 code. This problem can be alleviated by doing what we call Event Binding and Event Triggering.
For this solution, we'll be using the Underscore and Backbone libraries, although jQuery has similar event binding code (on DOM elements) and you could always roll your own using plain Javascript.
The concept of event binding is pretty simple. Basically, our overall ‘class' function maintains a list of events. Each event has a function tied to it. When you run Object.bind(), you pass in two parameters. The first is the name of the event, a string. The second is the function we want to execute. When we run .bind(), it adds the event key/value to the list. When we run Object.trigger(), you pass in just the name of the event you want to trigger, and optionally some arguments to be passed along to the function. The functions passed in to .bind() are usually a reference to a function within the current ‘class'. When running .trigger(), the arguments you pass can be results of the current operation. This explanation is probably a little confusing, so here's a re-factored version of our above code which makes use of event binding.
var ResourceGrabber = function() {
var selectedResource1: '',
var selectedResource2: '',
getResources: function(selectedResource1, selectedResource2) {
this.selectedResource1 = selectedResource1;
this.selectedResource2 = selectedResource2;
this.trigger('initialized');
},
getResource1: function() {
var self = this;
$.ajax({
url: '/api/resource1',
dataType: 'json',
success: function(result) {
var $container = $('select#container');
$container.empty().append("Select a Resource1...");
$.each(result, function(index, value) {
$container.append("<option>" + value.Resource.name + "</option>");
});
if (self.selectedResource) {
$container.val(self.selectedResource);
}
this.trigger('retrievedResource1');
}
});
},
getResource2: function(selectedResource) {
var self = this;
$.ajax({
url: '/api/resource2',
dataType: 'json',
success: function(result) {
var $container2 = $('select#container2');
$container2.empty().append("Select a Resource2...");
$.each(result, function(index, value) {
$container2.append("<option>" + value.Resource2.name + "</option>");
});
if (self.selectedResource2) {
$container.val(self.selectedResource2);
}
this.trigger('retrievedResource2');
}
});
}
}
_.extend(ResourceGrabber, Backbone.Events);
ResourceGrabber.bind("initialized", function(arg) {
this.getResource1(arg);
});
ResourceGrabber.bind("retrievedResource1", function(arg) {
this.getResource2(arg);
});
ResourceGrabber.bind("retrievedResource2", function(arg) {
//Nothing For Now
});
ResourceGrabber.getResources('alpha', 'beta');
Notice how much cleaner our code is. We've no longer got nested callback functions and spaghetti code, not to mention we can call future events whenever we want by running our Object.trigger(‘eventName');. Event based programming is a different way of thinking about problems if you're used used to something like PHP, but for interactive GUI's, it's the best way to get things done.
Also, note that this example is overly simple, and there are better ways to run this code without the event code and pulling in the underscore and backbone libraries. The code length actually got longer as well. But, for larger applications, doing this will keep your code clean and organized, will allow for DRY (Don't Repeat Yourself) programming, and should keep the code even shorter.