A deep dive into Backbone.View events

Backbone Views have a handy declarative events API. That lets you abstract your DOM event bindings into a declarative hash on your views prototype. For example

1
2
3
4
5
6
Backbone.View.extend({
template: _.template("<p>click me </p>"),
events: {
"click p": () => {console.log("clicked")}
}
})

After your view is created and rendered, anytime a user clicks on the p tag there will be a nice alert message in the console.

How does this work though? Let’s dive in and investigate.

Before we go into the exact details, let’s step through the path of setting up event handlers on DOM nodes to round out our perspective.

In the early days of html (V3.2) or so w3-spec
there was a call to allow users to harness a set of “intrinsic events that are generated by objects associated with HTML elements”, onClick happens to be one of those. With that said, why don’t we turn back the clock and set up an “intrinsic” event?

1
<p onclick="alert('wow such inline')">click me</p>

Most people can agree nowadays that we want some degree of separation between our markup language and event bindings*, allowing a level of declarative control at a scripting level.

In comes the mythical JavaScript beast.

1
2
3
4
5
<p> im just a p tag sitting in a body </p>
<script type="text/javascript">
document.querySelector('p').addEventListener('click', ()=> alert('hi mom'), false)
</script>

addEventListener gives you the ability to register arbitrary event listeners on the event target, opening the door for dynamic event registration and essentially giving you the power to set up a reactive DOM tree depending on a user’s actions.

From there we waddle up the evolutionary tree into jQuery, and into the beauty of $.on. At this point we can get back to the question initially proposed. How does the Backbone event hash really work?

Looking at the source for Backbone’s View class we see a call to delegateEvents within our view constructor

Let’s step through this method line by line to really understand every single thing that it is doing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
delegateEvents: function(events) {
// If we have no events NOOP and exit the method.
// (this is sometimes called a GuardClause or Precondition*)
if (!(events || (events = _.result(this, 'events')))) return this;
// unset any event handlers that might
// already be in place
this.undelegateEvents();
// Now we are going to loop over every key value pair
// in the events hash. For this case lets assume the events hash
// contains a single key value pair
// of {"click p": function(){alert('hi')}}
for (var key in events) {
// Since we only have the key value of the
// hash ("click p") in this case
// we need to get the value out of the events hash.
var method = events[key];
// Backbone allows you to pass in a string based value
// that it will look up
// on the view instance. In our case we do not have to work
// about this line
if (!_.isFunction(method)) method = this[events[key]];
// If there is no value, or the value evaluated to a
// falsey value kick out of this
// iteration of the loop. (This is a micro optimization)
if (!method) continue;
// The match here is going to take care of splitting the
// DOM node selector from the
// event that we will be registering on the DOM node.
var match = key.match(delegateEventSplitter);
// In our case eventName will be click and selector will be 'p'
var eventName = match[1], selector = match[2];
// The next step is to ensure that the `this`
// context of the event handler will be
// our view as compared to the default `this` context
// of a event registration which would be the
// DOM node that event was registered on
// (the p tag in our case).
method = _.bind(method, this);
// This line can be a bit confusing initially,
// however it is a very powerful one. This is taking your event
// name that you want to register for
// (click in our case) and setting up
// a jquery event namespace*, significantly
// simplifying how to unregister
// all events on this given view instance.
eventName += '.delegateEvents' + this.cid;
// At this point eventName will be something
// like "click.delegateEventsc1"
if (selector === '') {
// If we do not have a selector we set the event listener on
// the root `el` node of out view.
this.$el.on(eventName, method);
} else {
// If there is a selector however we use the
// 3 argument signature of jquery's `on`.
this.$el.on(eventName, selector, method);
}
}
return this;
},

After that look into how delegateEvents works you may be thinking OK great now I know how it works. However I will challenge your understanding with a follow up. If you noticed that we call delegateEvents in the view’s constructor, before we have ever called render.
So … We just registered a click handler on a p tag that does not exist. addEventListener would not work, so how does on work before we have even rendered? Good question right?

Taking a look at the jquery docs reveals the answer to our puzzling question. In comes Direct and in our case delegated events.
Docs

Delegated events have the advantage that they can process events from descendant elements that are added to the document at a later time. By picking an element that is guaranteed to be present at the time the delegated event handler is attached, you can use delegated events to avoid the need to frequently attach and remove event handlers

Meaning, since we know we have our root el from Backbone that will eventually contain our template, we can use this feature of jQuery to take care of the deferred event registration for us.

In summary, the Backbone event hash does quite a bit more than simple registration of key value pairs. It abstracts a fairly complex set of concerns into a easy-to-decipher declarative API.

I hope you learned something, I know I did.
Thanks to Seebiscuit for asking this great question
in the Marionette Chatroom.

Sam Saccone
@samccone


[1] - Angular has ng-click, and React has onClick. There is a nice argument around coupling the event bindings to your template, but for this article that is not a battle I want to fight. :)
[2] - http://c2.com/cgi/wiki?GuardClause
[3] - http://api.jquery.com/on/#event-names http://api.jquery.com/event.namespace/

comments powered by Disqus