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.
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.
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 trigger
ed. 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
change
s, 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 trigger
s 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 Collection
s 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
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 remove
d 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.
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 remove
d 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
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.
As you would expect, listenToOnce
is to listenTo
as once
is to on
.
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 remove
d, so normally in the case of view
s 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.
Now that you have all your callback
s 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
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.
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.
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 name
s to callback
s. 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 callback
s 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.
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 name
s as the keys and corresponding callback
s as the values.
this.listenTo(this.repos, {
We've given board
an issues
Collection
that we'd like to store all issue
s in. It's easier to filter the issue
s 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 add
ed 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 remove
d 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 remove
d 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.
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 trigger
ing 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.