English |  Español |  Français |  Italiano |  Português |  Русский |  Shqip

Developing a Backbone.js Edge

Chapter 3: Backbone Events

Now that we have covered how to test our code, let's get started by looking at Backbone events. With functions as first class members, JavaScript is a great language for taking full advantage of events. If you've ever responded to a user click or keyboard event with some JavaScript code that was waiting for it, you've already been using events. An event is simply a way of letting another part of the program know that something has happened. Lucky for us, Backbone provides a sleek, fast API for binding callbacks to events. Even better, this API has been extended onto all Model, Collection, View, and Router instances, as well as the Backbone object itself.

This section will go over the Events API and then give some specific examples of how Hubbub uses events.

API

The Backbone.Events object comes equipped with seven methods that allow you to invoke a specific function when an event occurs, trigger an event, or stop waiting for an event to occur. We will go over the signature and purpose of each method along with an example of when it's most appropriately used.

on(name, callback[, context])

The on method is used to bind a callback to an event. In other words, you're saying you want a function (callback, with an optional context) to be invoked when (on) an event (name) is triggered. on is best used when you want the context of your callback to be the object triggering the event.

Here are the arguments for on:

  • name - The name of the event to listen to.
  • callback - The function to invoke when the name event is triggered.
  • context (optional) - The context the callback should be called with. In other words, context refers to the object that will be represented by this inside the function. If not supplied, the default context will be the object that triggered the event.

As an example, if you're using localStorage for persistance like we do in Hubbub, you may wish to save a model every time it changes. Without Events, you'll have to remember to call model.save() after every model.set(attributes). That's not fun, and Backbone is all about fun!

Instead, open index.html in your browser and try this in your Console: 

var model = new Backbone.Model();
model.on('change', function () {
console.log("time to save the model!");
});
model.set('name', 'Gunner'); // "time to save the model!"

See how well it reads? When model changes, save it.

The name 'all' is a special-cased event. When you bind a callback to 'all' it will be invoked on all of that object's triggers with the name of the event fired as the first argument. For example:

model.on('all', function (name, a, b, c) {
// On the first trigger, name is 'event', a === 1, b === 2, c === 3
// On the second trigger, name is 'event2', a === 4, b === 5, c === 6
console.log(arguments);
});
model.trigger('event', 1, 2, 3);
model.trigger('event2', 4, 5, 6);

The 'all' event is used internally in Collections to proxy Model events.

You could use on() for all of your event bindings, but you might get bit by memory leaks, which we'll explain soon. As a general rule, on() is best used when you want the context of callback to be the object you're binding to and triggering from.

once(name, callback[, context])

once works identically to on, except that the event will be automatically unbound from the object as soon as it is called. It gives you an easy way to bind to an event for just one occurrence, and then forget about it. A custom event like a modal view 'close' is a good use case for once, because generally a modal view will be removed once it is closed, so further listening would be pointless.

model.once('resolve', function () {
alert('Tears of joy, our issue has been resolved!');
}); model.trigger('resolve'); // alerted!
model.trigger('resolve'); // no alert.

off([name][, callback][, context])

What do you do when you need a callback to stop firing? Easy, just turn it off. It's important to turn off callbacks you aren't going to be using anymore, since lingering callbacks (zombie events) can lead to memory leaks. Here's an example of a memory leak using on:

model.on('eventName', callback, view);
view.remove();

Now it's clear that we're done with view since we've removed it. However, model knows nothing about view's removal and will continue firing callback with view's context every time 'event' is triggered. How can we fix this problem? Use off to reverse the effect of on:

model.off('eventName', callback, view);

Now the callback has been removed and the potential memory leak has been thwarted. off can take any combination (or none) of the same arguments you feed to on. If no arguments are passed, every event listener is destroyed.

Turning off the special case 'all' event works the same way. To turn off a callback listening to all events, simply:

obj.off('all', callback);

listenTo(obj, name, callback)

listenTo is what can be referred to as an inversion-of-control method. It takes on's job and puts it in the hands of the listening object, rather than the triggering one. The difference between on and listenTo is best seen in an example.

These two methods of binding an object to another object's event are functionally the same:

objA.on('eventName', callback, objB);
objB.listenTo(objA, 'eventName', callback);

The difference is when it comes time to turn off these callbacks. More on that in the stopListening section. As a general rule, use listenTo when the context of your callback should be the object you're listeningTo with. This is often the case for views where you want the view to listen to a model or collection, but stop listening when the view is destroyed to prevent memory leaks.

listenToOnce(obj, name, callback)

As you would expect, listenToOnce is to listenTo as once is to on.

stopListening([obj][, name][, callback])

stopListening is what makes listenTo shine. Internally, listenTo keeps track of the objects the listening object has been listening to. This comes in handy when it comes time to unbind events. Here is a common case with views:

modelA.on('change', view.renderA, view);
modelB.on('change', view.renderB, view);
modelC.on('change', view.renderC, view);

Now the only way to turn off these views would be like so:

modelA.off('change', view.renderA, view);
modelB.off('change', view.renderB, view);
modelC.off('change', view.renderC, view);

Ugh...tedious. Let's try the same thing with listenTo and stopListening:

view.listenTo(modelA, 'change', view.render);
view.listenTo(modelB, 'change', view.render);
view.listenTo(modelC, 'change', view.render);

Now, since listenTo has been keeping track of objects being listened to (in this case modelA, modelB, and modelC), all bindings can be undone with a single stopListening:

view.stopListening();

stopListening is automatically called by a view when it is removed, so normally in the case of views you don't have to worry about doing this yourself.

stopListening can also be passed any combination (or none) of the same arguments listenTo (or listenToOnce) receives to turn off callbacks accordingly.

trigger(name[, arguments...])

Now that you have all your callbacks ready to listen, trigger gives you the power to be heard. Internally, Backbone triggers about a dozen built-in events on instances of Models, Collections, and Routers. You saw one of them in the first on example, 'change'. If you look through the Backbone.js source you'll notice trigger pops up a lot.

The arguments trigger takes look like this:

  • name - The name of the event to trigger.
  • arguments... - Any additional arguments will be passed as arguments to the listening callback functions.

What's great about the Events API, though, is that you are free to trigger whatever kind of event you want to listen to. Maybe you want to do something before a model is saved. You could do something like this:

collection.trigger('sort:before', collection, 'hello');
collection.sort();

Now any object that was listening for model's 'sort:before' event will have its callback invoked with the collection and 'hello' arguments.

A more advanced way of doing this would be to override the sort method to trigger 'before-sort'.

// Create a closure so we can use a local variable to store the original
// `sort` method.
(function () {
var sort = Backbone.Collection.prototype.sort;
Backbone.Collection.prototype.sort = function (options) {
if (!options.silent) this.trigger('sort:before',
this, options);
return sort.apply(this, arguments);
};
})();

Now that you have that set up, you can forget worrying about triggering 'sort:before' before you sort, and just do it.

collection.on('sort:before', beforeCallback);
collection.on('sort', afterCallback);
collection.sort(); // `beforeCallback` invoked then
`afterCallback` invoked

More Examples

If you'd like to see more examples of on, off, trigger, listenTo and the other event methods check out the Backbone events test file. There are plenty of assertions there that will help you understand events better and also may give you some ideas to help your testing.

Extensibility

The Backbone.Events object was designed to be extendable onto any object. For example, using _.extend() you can do something like this:

var objA = _.extend({
name: 'Gunner',
cheer: function () {
alert(this.name + " cheers on `objB`'s dancing!");
}
}, Backbone.Events);
var objB = _.extend({
dance: function () {
this.trigger('dance');
}
}, Backbone.Events);
objA.listenTo(objB, 'dance', objA.cheer);
objB.dance();

You can even extract just the Events code from backbone.js for your own project if you don't need the other features Backbone provides.

Bonus Features

Backbone's Events API is sprinkled with some nice syntactic sugar to make your life easier. The first one we'll go over is method chaining. This allows you to bind or trigger an event and continue calling methods on the binding object in that same expression. We'll use our earlier on example as, well, an example:

model.on('change', function () {
this.save();
}); model.set('name', 'Gunner'); // `model` is saved!

You have a few options for binding names to callbacks. So far we've only used the standard single name to single callback style, but there are a couple other ways you can use any of the Event methods to be more terse.

Sometimes you want a certain callback to be fired when either of two events are triggered:

obj.on('eventA eventB', callback);
obj.trigger('eventA'); // `callback` fired!
obj.trigger('eventB'); // `callback` fired!

It's also not uncommon to want to bind multiple events with multiple corresponding callbacks all at once. This is possible when you pass an event map as the first argument of the function:

obj.on({
eventA: callbackA,
eventB: callbackB
});

The same rule with space delimited events also applies to event maps, so you can really get creative:

obj.on({
'eventA eventB': callback1,
'eventA eventC': callback2
}); obj.trigger('eventA eventB eventC');
// This will invoke, in this order:
// `callback1`, `callback2`, `callback1`, `callback2`

And the granddaddy of Events API examples:

obj.on({
'eventA eventB': callback1,
'eventA eventC': callback2
}); obj.trigger({
eventA: 'hello',
'eventB eventC': null
});
// And the result is, in this order:
// callback1('hello'), callback2('hello'), callback1(null),
callback2(null)

This is a contrived example, but nevertheless, the features are there for your consumption.

Keep in mind this API is consistent throughout the events. Use a space delimited string to bind multiple events to a callback, or get even fancier and pass in an event map to listenTo an object with multiple events and multiple callbacks.

Events in Hubbub

If you browse the Hubbub source, you'll notice we use events extensively. Rather than point out each individual place events are used (which would take its own book), we'll focus on a distinct block of code in the Board model's initialize method.

The first event we hook onto is 'change'. You can probably already tell from reading it, but the board (this) will save on 'change's.

this.on('change', function () { this.save(); }); 

The next part is a bit trickier. We use listenTo here because we want the callback's context to be our board (this). Then we pass an event map of event names as the keys and corresponding callbacks as the values.

this.listenTo(this.repos, { 

We've given board an issues Collection that we'd like to store all issues in. It's easier to filter the issues on the kanban when we can pull them all from the same collection. To do this, we'll want to listen for new repos being added to the repos collection, and in turn add that repo's issues to our master issues collection. But that's not all. We also want to set up another hook for this new repo that ensures any new issues that are added to it, as we have added to our master issues collection. Finally, save the board so we can remember the newly added repo:

  add: function (repo) {
this.issues.add(repo.issues.models);
this.issues.listenTo(repo.issues, 'add', this.issues.add);
this.save();
},

Here is where stopListening comes into play to counter our listenTo in the add event. When a repo is removed, we need to tell the board's issues to stop waiting for the repo to add new issues. Obviously if the repo is being removed from our board, that case is no longer relevent. If the repo has been destroyed, failing to stopListening will result in a repo object that can never be de-referenced, even though it's no longer used (memory leak). And lastly, as with add, be sure to save the board so that the removed repo no longer shows on our kanban:

  remove: function (repo) {
this.issues.stopListening(repo.issues, 'add', this.issues.add);
this.save();
}
});

The last two callbacks we hook up are to automatically save (to localStorage) new repos and issues when they are added or changed. For example, when you change the state of an issue from "doing" to "done," you immediately save the issue's new state when that 'change' event is triggered.

this.repos.on('add change', function (repo) { repo.save(); });
this.issues.on('add change', function (issue) { issue.save(); });

As a credit to Backbone, we didn't find the need to trigger any custom events in Hubbub. This isn't the case for all applications, and larger applications can actually make great use out of this feature.

Conclusion

Backbone's event system is a powerful tool that you will come to know and love as you begin writing Backbone applications. Here are a few things to remember as you dive in with Events:

  • Use the right tool for the job.
    In general, on is best used when you need the context of the callback to be the object that is triggering the event. listenTo is best used when you need the context of the callback to be the object that is listening to the event.

  • Don't leak precious memory.
    When a browser page is refreshed, the JavaScript memory from the previous page is wiped. In a single-page Backbone application, you don't have this convenience so memory management becomes important. If you use on, once, listenTo, or listenToOnce, you have added a callback to an internal array that will stay there until (a) the object is dereferenced in JavaScript or (b) you call off or stopListening accordingly. Keep this information in mind and clean up your events. A memory leak may not be noticable at first, but too much wasted memory will eventually have noticable, detrimental effects to your application, especially if it's long-running.

  • Learn the events that Backbone triggers by default.
    As mentioned earlier, there are only about a dozen built-in events and they will be extremely helpful in establishing a logical flow for your application.

In the next chapter we will examine Views in Backbone, and how we used them in our Hubbub application.

There has been error in communication with Booktype server. Not sure right now where is the problem.

You should refresh this page.