Backbone.js Basics: Bringing an App to Life with Events
In a previous tutorial, we plunged into the workings of Backbone.js, an MV* JavaScript framework for building applications. Building a small surf shop app (to help us keep track of surf boards in stock), we looked at creating models, grouping model instances into collections, and iterating over collections to create views. We also took it a step further by rendering each of our model instances into its own view, and grouping them together in a parent view. This is where we left off.
In this tutorial, we’re going to see the importance of that last step, and introduce some controller-type logic into the picture. I touched on controller-type logic and where it belongs in the last article, but let’s just drive the point home before we continue.
Separation of Concerns
In an MV* framework, views are typically responsible for the following:
- Rendering the UI, and displaying the data retrieved from the database and/or collections to the end user.
- Handling user input via events, and somehow communicating those events to the model.
The “somehow” part is where the controller logic comes in. In Backbone.js, we’re able to bind events to a view during its instantiation. If we wanted to add or remove stock, or delete a model completely, we’re able to do so. We’ll go through how to do this step by step, but for now, just imagine that we have a button in our view that allowed us to remove one item from stock. We can register that event in the view, which is essentially controller logic. When that button is clicked, we communicate with the respective model, and make the necessary updates to it. Changes in models can also trigger events that the view can handle, ultimately re-rendering the view and displaying the correct data.
So in a nutshell:
- A view renders model data, and registers events.
- When an event is fired via user input, we can run some kind of callback or function which communicates with the model.
- Inside the model, the logic gets performed. In a real world scenario, you’ll probably also be updating a database here.
- The model change fires off an event.
- The view picks up on that event, and acts accordingly, possibly re-rendering itself.
With all that in mind, we now need to think about how we’re going to register these events in the first place. Let’s move on to that.
Using Backbone Events
According to the Backbone events documentation:
Events is a module that can be mixed in to any object, giving the object the ability to bind and trigger custom named events.
There are many ways to handle events in a Backbone application, but we’ll be leveraging two ways:
- We’ll use the catalog of built in events in tandem with the
listenTo
method, which tells an object to listen to a particular event on another object. - We’ll also delegate events using the events hash directly inside the view.
Let’s first take a look at leveraging the events hash inside the view instances.
Leveraging the Events Hash
Here’s a recap of the current code of the SurfboardView
from last time:
var SurfboardView = Backbone.View.extend({
tagName: 'tr',
template: _.template($('#surfboard-template').html()),
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
}
});
You’ll notice that each view instance gets wrapped in its own tr
element, which is populated from the template we made before. Let’s edit that template a little bit to include a couple of buttons that will allow us to add and remove stock. We’ll also need to update the markup for the table:
<table class="table">
<thead>
<tr>
<th>Manufacturer</th>
<th>Model</th>
<th>Stock</th>
<th>Add/Remove</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
<script type="text/template" id="surfboard-template">
<td><%= manufacturer %></td>
<td><%= model %></td>
<td><%= stock %></td>
<td>
<button class="add-one">+1</button>
<button class="minus-one">-1</button>
</td>
</script>
Each of our view instances should now have two buttons, but they do nothing at the moment. Let’s take a look at the events hash now, and see how we can register clicks on those two buttons. An events hash in Backbone generally looks like this:
events: {
'event target': 'callback'
}
The event
is the actual event, such as click
, dblclick
, mouseover
, etc. The target
is the target element, which is found via DOM traversal based on the view’s element. In other words, if we specified a class name, an ID, or a tag, it would look for the first of that type that the lookup matches. This is why I added class names to the buttons in the template. Finally, the callback
is the function that gets called when the event is fired.
So in our case, we can add the following to our SurfboardView
view:
events: {
'click .add-one': 'addOne',
'click .minus-one': 'minusOne'
},
addOne: function(e) {
e.preventDefault();
// code to add one to stock here...
},
minusOne: function(e) {
e.preventDefault();
// code to minus one from stock here...
}
The only thing missing now is the script to actually update the model.
Updating the Model
If you put an alert or console statement inside either the addOne
or minusOne
functions in the view, and fire the events by clicking the buttons, you’ll see the events in action quickly. That doesn’t really help us just yet though, as we have to communicate this user action back to the model. So far, I’ve just prevented default browser button behaviour. Next up, we need to call a model function (which we still have to write). These two model functions will reside inside the model itself, staying true to our “separation of concerns” ideology. Here’s the updated SurfboardView
:
var SurfboardView = Backbone.View.extend({
tagName: 'tr',
events: {
'click .add-one': 'addOne',
'click .minus-one': 'minusOne'
},
template: _.template($('#surfboard-template').html()),
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
},
addOne: function(e) {
e.preventDefault();
this.model.addOne();
},
minusOne: function(e) {
e.preventDefault();
this.model.minusOne();
}
});
Remember, each view instance contains a reference to its corresponding model instance. This is why we’re able to access model functions using this.model.FUNCTION
. Back in our Surfboard
model though, we need to account for these functions.
The logic here is simple, as we just need to grab the existing stock number, either add or subtract one from it, and update that model’s stock number. We already looked at how to get
an attribute value from a model. Likewise, we can set
it. Here’s the updated model code:
var Surfboard = Backbone.Model.extend({
defaults: {
manufacturer: '',
model: '',
stock: 0
},
addOne: function() {
this.set({
stock: this.get('stock') + 1
});
// probably update a database
},
minusOne: function() {
this.set({
stock: this.get('stock') - 1
});
// probably update a database
}
});
In a real world app, you’ll probably want to update a database here too, but that is beyond the scope of this tutorial.
Now if you run this, you’ll notice that the events are being fired when we click the buttons, and the corresponding attribute values in the model instances are being updated. Nothing is happening on screen though. Why is that? Well, we’re not re-rendering the associated view yet, so let’s take a look at that.
Listening for Events on View Instantiation
Here’s a quick scenario. Imagine that we had a view that had been rendered to the screen already. A user then interacted with that view, which triggered a change in the model. That model then fires off a change
event (which we know can get fired from the catalog of events). When that model is changed, we want to re-render the view. How do we do that?
For starters, let’s remember that we have an initializer function available in our view instance. We can use that to add the listener, like this:
initialize: function() {
this.listenTo(this.model, "change", this.render);
}
Our view object is now listening for changes on the model object, and when it does indeed change, we’re calling the render function again, which updates the view.
Guess what? We’re pretty much there, because of the code we’ve written so far. By clicking those buttons inside each of the view instances, we are not only running the associate callback function, but we’re firing off a change event. That means that by inserting this little line of code in our view, we’re able to do whatever we want when that model’s change event fires. Here’s a look at our updated SurfboardView
code:
var SurfboardView = Backbone.View.extend({
tagName: 'tr',
events: {
'click .add-one': 'addOne',
'click .minus-one': 'minusOne'
},
template: _.template($('#surfboard-template').html()),
initialize: function() {
this.listenTo(this.model, "change", this.render);
},
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
},
addOne: function(e) {
e.preventDefault();
this.model.addOne();
},
minusOne: function(e) {
e.preventDefault();
this.model.minusOne();
}
});
Now, everything should be working, and you should be able to add and minus stock as you with! Here’s the demo and codebase to go along with it:
See the Pen Backbone.js Basics – Part 4 by SitePoint (@SitePoint) on CodePen.
Wrap Up
And that’s a wrap, folks! In this tutorial, we made some great progress with Backbone, and looked into the importance of events and how to use them. We also explored how the communication lines between views and models work, and how we can keep our code organized to make it more future proof and scalable. There’s a whole lot more to Backbone than this though, and the documentation is full of interesting events, methods, and possibilities. If you have any questions or comments, I’d be glad to hear them in the discussion below. Keep exploring and building, and thanks for reading.
Want more on Backbone?
SitePoint Premium has a new course on Backbone.js. Join Premium to access it, and the entire SitePoint library of resources!