Javascript
BACKBONE.JS: INTRODUCING THE BACKBONE STORE!
Note: This tutorial is out of date! There are now two updates: A Javascript, HTML, and CSS Version, and a Coffeescript, HAML, and Stylus Version.
Introduction
I've been playing with Backbone.js, a small but nifty Javascript library that provides a small Model-View-Controller framework where Models can generate events that trigger View changes, and vice versa, along with a Collections models so groups of models can cause view-level events, and a Sync library that provides a basic REST architecture for propagating client-made changes back to the server.
There are a number of good tutorials for Backbone, such as: Meta Cloud, &Yet's Tutorial, Backbone Mobile (which is written in Coffee), and Backbone and Django. However, a couple of months ago I was attempting to learn Sammy.js, a library very similar to Backbone, and they had a nifty tutorial called The JsonStore.
In the spirit of The JSON Store, I present The Backbone Store.
Literate Program
A note: this article was written with the Literate Programming toolkit Noweb. Where you see something that looks like
The Store
The store has three features: A list of products, a product detail page, and a ``shopping cart'' that does nothing but tally up the number of products total that you might wish to order. The main viewport flips between a list of products and a product detail; the shopping cart quantity tally is always visible.
Let's start by showing you the HTML that we're going to be exploiting. As you can see, the shopping cart's primary display is already present, with zero items showing. DOM ID ``main'' is empty. We'll fill it with templated data later.
HTML
<a name="NW4SMJMZ-3cYSIo-1"></a><a href="#NWD4SMJMZ-1"><dfn><index.html>=</dfn></a>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>The Backbone Store</title>
<link rel="stylesheet" href="jsonstore.css" type="text/css" media="screen" charset="utf-8" />
<a name="NW4SMJMZ-3cYSIo-1-u1"></a><a href="#NWD4SMJMZ-5"><product list template></a>
<a name="NW4SMJMZ-3cYSIo-1-u2"></a><a href="#NWD4SMJMZ-B"><product template></a>
</head>
<body>
<div id="container">
<div id="header">
<h1>
The Backbone Store
</h1>
<div class="cart-info">
My Cart (<span class="cart-items">0</span> items)
</div>
</div>
<div id="main">
</div>
</div>
<script src="jquery-1.4.4.min.js" type="text/javascript"></script>
<script src="jquery.tmpl.min.js" type="text/javascript"></script>
<script src="underscore.js" type="text/javascript"></script>
<script src="backbone.js" type="text/javascript"></script>
<script src="store.js" type="text/javascript"></script>
</body>
</html>
This is taken, more or less, straight from The JSON Store. I've included one extra thing, aside from jQuery and Backbone, and that's the jQuery Templates kit. There is also a simplified JSON file that comes in the download; it contains six record albums that the store sells. (Unlike the JSON store, these albums don't exist; the covers were generated during a round of The Album Cover Game.)
The Program
And here's the skeleton of the program we're going to be writing:
<a name="NW4SMJMZ-3plm23-1"></a><a href="#NWD4SMJMZ-2"><dfn><store.js>=</dfn></a>
<a name="NW4SMJMZ-3plm23-1-u1"></a><a href="#NWD4SMJMZ-3"><product models></a>
<a name="NW4SMJMZ-3plm23-1-u2"></a><a href="#NWD4SMJMZ-4"><product list view></a>
<a name="NW4SMJMZ-3plm23-1-u3"></a><a href="#NWD4SMJMZ-6"><shopping cart models></a>
<a name="NW4SMJMZ-3plm23-1-u4"></a><a href="#NWD4SMJMZ-7"><shopping cart view></a>
<a name="NW4SMJMZ-3plm23-1-u5"></a><a href="#NWD4SMJMZ-8"><product view></a>
<a name="NW4SMJMZ-3plm23-1-u6"></a><a href="#NWD4SMJMZ-E"><application></a>
Products and Product List View
To start, I have a list of products. The basic product is just a model, with nothing to show for it; the list of products is a Backbone.Collection
, with one feature, the comparator
, which sorts the albums in order by album title.
<a name="NW4SMJMZ-46kOnK-1"></a><a href="#NWD4SMJMZ-3"><dfn><product models>=</dfn></a> (<a href="#NWD4SMJMZ-2"><-U</a>)
var Product = Backbone.Model.extend({});
var ProductCollection = Backbone.Collection.extend({
model: Product,
comparator: function(item) {
return item.get('title');
}
});
The ProductCollection
is what we want to show when the user isn't looking at a specific product. I create a Backbone.View
object class ProductListView
:
<a name="NW4SMJMZ-14ADMh-1"></a><a href="#NWD4SMJMZ-4"><dfn><product list view>=</dfn></a> (<a href="#NWD4SMJMZ-2"><-U</a>)
var ProductListView = Backbone.View.extend({
el: $('#main'),
indexTemplate: $("#indexTmpl").template(),
render: function() {
var sg = this;
this.el.fadeOut('fast', function() {
sg.el.empty();
$.tmpl(sg.indexTemplate, sg.model.toArray()).appendTo(sg.el);
sg.el.fadeIn('fast');
});
return this;
}
});
Here, we've told this view that it's principle element is the DOM ID main'', allocated an indexTemplate using the jQuery template complire, and created a `render` function that fades out
main'', replaces its content with a rendered template, and fades it back in.
The template looks like this:
<a name="NW4SMJMZ-1uC8r-1"></a><a href="#NWD4SMJMZ-5"><dfn><product list template>=</dfn></a> (<a href="#NWD4SMJMZ-1"><-U</a>)
<script id="indexTmpl" type="text/x-jquery-tmpl">
<div class="item">
<div class="item-image">
<a href="#item/${cid}"><img src="${attributes.image}" alt="${attributes.title}" /></a>
</div>
<div class="item-artist">${attributes.artist}</div>
<div class="item-title">${attributes.title}</div>
<div class="item-price">$${attributes.price}</div>
</div>
</script>
There's some Demeter violations going on here, in that I have to know about the attributes
of a Backbone model, something that's normally hidden within the class. But this is good enough for our purposes. The above is a jQuery template, and the \$\{\}
syntax is what's used to dereference variables within a template.
(As an aside, I think that the set
and get
methods of Backbone.Model
are a poor access mechanism. I understand why they're there, and I can only hope that someday Javascript Getter and Setters become so well-established as to make set
and get
irrelevant.)
The Shopping Cart
Before I move on to the product view, I want to go over the shopping cart.
A little rocket science here: A Cart
contains CartItems
. Each item'' represents a quantity of a `Product`. (I know, that always struck me as odd, but that's how most online stores do it.) `CartItem` has an update method that allows you to add more (but not remove any-- hey, the Sammy store wasn't any smarter, and this is For Demonstration Purposes Only), and we use the `set` method to make sure that a
change'' event is triggered.
The Cart
, in turn, has a method, getByPid (``Product ID''), which is meant to assist other objects in finding the CartItem
associated with a specific product. Here, I'm just using the Backbone default client id. Once we've found that item, we can just call update()
on it.
<a name="NW4SMJMZ-4WYyeR-1"></a><a href="#NWD4SMJMZ-6"><dfn><shopping cart models>=</dfn></a> (<a href="#NWD4SMJMZ-2"><-U</a>)
var CartItem = Backbone.Model.extend({
update: function(amount) {
this.set({'quantity': this.get('quantity') + amount});
}
});
var Cart = Backbone.Collection.extend({
model: CartItem,
getByPid: function(pid) {
return this.detect(function(obj) { return (obj.get('product').cid == pid); });
},
});
The cart is represented by a little tag in the upper right-hand corner of the view; it never goes away, and its count is always the total number of Products
(not CartItem
s) ordered. So the CartView
needs to update whenever a CartItem
is added or updated. And we want a nifty little animation to go with it:
<a name="NW4SMJMZ-3HOP7O-1"></a><a href="#NWD4SMJMZ-7"><dfn><shopping cart view>=</dfn></a> (<a href="#NWD4SMJMZ-2"><-U</a>)
var CartView = Backbone.View.extend({
el: $('.cart-info'),
initialize: function() {
this.model.bind('change', _.bind(this.render, this));
},
render: function() {
var sum = this.model.reduce(function(m, n) { return m + n.get('quantity'); }, 0);
this.el
.find('.cart-items').text(sum).end()
.animate({paddingTop: '30px'})
.animate({paddingTop: '10px'});
}
});
A couple of things here: the render is rebound to this
to make sure it renders in the context of the view. I found that that was not always happening. Note the use of reduce
, a nifty method from underscore.js
that allows you to build a result out an array using an anonymous function. This reduce, obviously, sums up the total quantity of items in the cart. Also, jQuery enthusiasts could learn (I certainly did!) from the .find()
and .end()
methods, which push a child object onto the stack to be modified, and then pop it off after the operation has been applied.
One of the big things this illustrates is that a Backbone.View
is not a full-page event; it's a mini-application for drawing its own little universe, that may be part of a larger universe. It's entirely possible to have lots of Views on a page.
Also, this cart does not have a template associated with it: we're changing a single textual item on the page and animating another one that is always present.
The Product Detail View
So now we're down to the ProductView
. This is slightly more complicated. First, let me show you a skeleton of the view, similar to the one we saw for the ProductListView
:
<a name="NW4SMJMZ-4IaRoN-1"></a><a href="#NWD4SMJMZ-8"><dfn><product view>=</dfn></a> (<a href="#NWD4SMJMZ-2"><-U</a>)
var ProductView = Backbone.View.extend({
el: $('#main'),
itemTemplate: $("#itemTmpl").template(),
<a name="NW4SMJMZ-4IaRoN-1-u1"></a><a href="#NWD4SMJMZ-D"><product events></a>
<a name="NW4SMJMZ-4IaRoN-1-u2"></a><a href="#NWD4SMJMZ-9"><product view initialization></a>
<a name="NW4SMJMZ-4IaRoN-1-u3"></a><a href="#NWD4SMJMZ-C"><update product></a>
<a name="NW4SMJMZ-4IaRoN-1-u4"></a><a href="#NWD4SMJMZ-A"><render product></a>
});
The reason the ProductView is complex is because it's going to interact with the shopping cart. We need to keep track of the cart. There are two ways of dealing with this: Have the ProductView track down its cart item every time, or keep a reference to an individual track item having found it once. I'm going with the first option.
<a name="NW4SMJMZ-smsmK-1"></a><a href="#NWD4SMJMZ-9"><dfn><product view initialization>=</dfn></a> (<a href="#NWD4SMJMZ-8"><-U</a>)
initialize: function(options) {
this.cart = options.cart;
},
Rendering is exactly the same as that for the ProductListView. In fact, it's so similar, I'm thinking maybe I should have made this an abstract function and mixed it in afteward:
<a name="NW4SMJMZ-2AOn81-1"></a><a href="#NWD4SMJMZ-A"><dfn><render product>=</dfn></a> (<a href="#NWD4SMJMZ-8"><-U</a>)
render: function() {
var sg = this;
this.el.fadeOut('fast', function() {
sg.el.empty();
$.tmpl(sg.itemTemplate, sg.model).appendTo(sg.el);
sg.el.fadeIn('fast');
});
return this;
}
The template for a ProductView, however, has some interesting qualities:
<a name="NW4SMJMZ-1qK35A-1"></a><a href="#NWD4SMJMZ-B"><dfn><product template>=</dfn></a> (<a href="#NWD4SMJMZ-1"><-U</a>)
<script id="itemTmpl" type="text/x-jquery-tmpl">
<div class="item-detail">
<div class="item-image"><img src="${attributes.large_image}" alt="${attributes.title}" /></div>
<div class="item-info">
<div class="item-artist">${attributes.artist}</div>
<div class="item-title">${attributes.title}</div>
<div class="item-price">$${attributes.price}</div>
<div class="item-form">
<form action="#/cart" method="post">
<input type="hidden" name="item_id" value="${cid}" />
<p>
<label>Quantity:</label>
<input type="text" size="2" name="quantity" value="1" class="uqf" />
</p>
<p><input type="submit" value="Add to Cart" class="uq" /></p>
</form>
</div>
<div class="item-link"><a href="${attributes.url}">Buy this item on Amazon</a></div>
<div class="back-link"><a href="#">« Back to Items</a></div>
</div>
</div>
</script>
Note the octothorpe used as the target link for Home''. I kept thinking an empty link or just
/'' would be appropriate, but no, it's an octothorpe.
Also note that it has a form. (Again, note the Demeter violations.) What we want is to update the shopping cart whenever the user enters a number into the input box and either presses ``Add To Cart'' or the ENTER button. That gives us our methods: We're in a view for a specific product; we must see if the customer has a CartItem
for that product in the Cart
, and add or update it as needed. Like so:
<a name="NW4SMJMZ-4IWYOC-1"></a><a href="#NWD4SMJMZ-C"><dfn><update product>=</dfn></a> (<a href="#NWD4SMJMZ-8"><-U</a>)
update: function(e) {
e.preventDefault();
var cart_item = this.cart.getByPid(this.model.cid);
if (_.isUndefined(cart_item)) {
cart_item = new CartItem({product: this.model, quantity: 0});
this.cart.add(cart_item, {silent: true});
}
cart_item.update(parseInt($('.uqf').val()));
},
updateOnEnter: function(e) {
if (e.keyCode == 13) {
return this.update(e);
}
},
But how to do these events get triggered? Go back to the ProductView skeleton above; there's a placeholder for ``product events'', which looks like this:
<a name="NW4SMJMZ-3y7xDR-1"></a><a href="#NWD4SMJMZ-D"><dfn><product events>=</dfn></a> (<a href="#NWD4SMJMZ-8"><-U</a>)
events: {
"keypress .uqf" : "updateOnEnter",
"click .uq" : "update",
},
Backbone uses a curious definition of an event with an event selector'', followed by a target method of the View class. Backbone is also limited about what events can be used here, as the following events cannot be wrapped by jQuery's delegate method and do not work:
focus'', blur'',
change'', submit'', and
reset''.
We preventDefault
to keep the traditional meaning of the submit button from triggering. When the CartItem
is updated, it triggers a change'' event, and the `CartView` will update itself automatically. I added the
silent'' option to keep the ``change'' event from triggering twice when adding a new CartItem
to the Cart
.
The Router
The router is a fairly straightforward component. It's purpose is to pay attention to the #hash'' portion of your URL and, when it changes, do something. Anything, really. `Backbone.History` is the event listener for the hash, so it has to be activated after the application. In many ways, a Backbone
Controller'' is just a big View with authority over the entire Viewport.
Here's the skeleton of our router, along with its instantiation:
<a name="NW4SMJMZ-1qRGnC-1"></a><a href="#NWD4SMJMZ-E"><dfn><application>=</dfn></a> (<a href="#NWD4SMJMZ-2"><-U</a>)
var Workspace = Backbone.Controller.extend({
<a name="NW4SMJMZ-1qRGnC-1-u1"></a><a href="#NWD4SMJMZ-G"><application variables></a>
<a name="NW4SMJMZ-1qRGnC-1-u2"></a><a href="#NWD4SMJMZ-F"><routes></a>
<a name="NW4SMJMZ-1qRGnC-1-u3"></a><a href="#NWD4SMJMZ-I"><initialization></a>
<a name="NW4SMJMZ-1qRGnC-1-u4"></a><a href="#NWD4SMJMZ-H"><index render call></a>
<a name="NW4SMJMZ-1qRGnC-1-u5"></a><a href="#NWD4SMJMZ-J"><product render call></a>
});
workspace = new Workspace();
Backbone.history.start();
There are two routes that we want to present: the index (our list of products) and the item (a product detail). So, using Backbone.Controller
, we're going to route the following:
<a name="NW4SMJMZ-3REFC9-1"></a><a href="#NWD4SMJMZ-F"><dfn><routes>=</dfn></a> (<a href="#NWD4SMJMZ-E"><-U</a>)
routes: {
"": "index",
"item/:id": "item",
},
There are a few things I want to track: the index view, the individual product views, and the shopping cart.
<a name="NW4SMJMZ-1XiTYP-1"></a><a href="#NWD4SMJMZ-G"><dfn><application variables>=</dfn></a> (<a href="#NWD4SMJMZ-E"><-U</a>)
_index: null,
_products: null,
_cart :null,
Now, we can render the index view:
<a name="NW4SMJMZ-274TIc-1"></a><a href="#NWD4SMJMZ-H"><dfn><index render call>=</dfn></a> (<a href="#NWD4SMJMZ-E"><-U</a>)
index: function() {
this._index.render();
},
There are two things left in our workspace, that we haven't defined. The intialization, and the product render.
Initialization consists of getting our product list, creating a shopping cart to hold ``desired'' products (and in quantity!), and creating the product list view.
<a name="NW4SMJMZ-3GGy0P-1"></a><a href="#NWD4SMJMZ-I"><dfn><initialization>=</dfn></a> (<a href="#NWD4SMJMZ-E"><-U</a>)
initialize: function() {
var ws = this;
if (this._index === null) {
$.ajax({
url: 'data/items.json',
dataType: 'json',
data: {},
success: function(data) {
ws._cart = new Cart();
new CartView({model: ws._cart});
ws._products = new ProductCollection(data);
ws._index = new ProductListView({model: ws._products});
Backbone.history.loadUrl();
}
});
return this;
}
return this;
},
Here, I load the data, and upon success create a new ProductCollection
from the data, a new shopping cart, and a new ProductListView
for the product collection. I then call Backbone.history.loadUrl()
, which then routes us to the correct view.
Thanks to this, users can bookmark places in your site other than the home page. Yes, the bookmark will be funny and have at least one octothorpe in it, but it will work.
And now I'm down to one last thing. I haven't defined that product render call in the application controller. The one thing I don't want to do is have ProductViews
for every product, if I don't need them. So I want to build them as-needed, but keep them, and associate them with the local Product
, so they can be recalled whenever we want. The underscore function isUndefined
is excellent for this.
<a name="NW4SMJMZ-10RBlh-1"></a><a href="#NWD4SMJMZ-J"><dfn><product render call>=</dfn></a> (<a href="#NWD4SMJMZ-E"><-U</a>)
item: function(id) {
if (_.isUndefined(this._products.getByCid(id)._view)) {
this._products.getByCid(id)._view = new ProductView({model: this._products.getByCid(id),
cart: this._cart});
}
this._products.getByCid(id)._view.render();
}
And that's it. Put it all together, and you've got yourself a working Backbone Store.
This code is available at my github at The Backbone Store.
Index of code references:
* _<application>_: U1, D2
* _<application variables>_: U1, D2
* _<index render call>_: U1, D2
* _<index.html>_: D1
* _<initialization>_: U1, D2
* _<product events>_: U1, D2
* _<product list template>_: U1, D2
* _<product list view>_: U1, D2
* _<product models>_: U1, D2
* _<product render call>_: U1, D2
* _<product template>_: U1, D2
* _<product view>_: U1, D2
* _<product view initialization>_: U1, D2
* _<render product>_: U1, D2
* _<routes>_: U1, D2
* _<shopping cart models>_: U1, D2
* _<shopping cart view>_: U1, D2
* _<store.js>_: D1
* _<update product>_: U1, D2
Postscript: Someone pointed out to me that using the toJSON() method that Backbone provides for models and collections would provide better results than the Demeter violations I complained about. The problem with that is that I'm using the CID as my primary key, and toJSON doesn't include the CID in the JSON object. I suppose I could override the toJSON method and add it, or write an inline to decorate the JSON product with the CID, but after all that it doesn't seem worth the effort. For more canonical work, though, it's something to keep in mind.