562 lines
19 KiB
Plaintext
562 lines
19 KiB
Plaintext
% -*- Mode: noweb; noweb-code-mode: javascript-mode ; noweb-doc-mode: latex-mode -*-
|
|
\documentclass{article}
|
|
\usepackage{noweb}
|
|
\usepackage{hyperref}
|
|
\begin{document}
|
|
|
|
% Generate code and documentation with:
|
|
%
|
|
% noweave -filter l2h -delay -x -html backbonestore.nw | htmltoc > backbonestore.html
|
|
% notangle -Rstore.js backbonestore.nw > store.js
|
|
% notangle -Rindex.html backbonestore.nw > index.html
|
|
|
|
\section{Introduction}
|
|
|
|
I've been playing with
|
|
\nwanchorto{http://documentcloud.github.com/backbone/}{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 (See:
|
|
\nwanchorto{http://www.plexical.com/blog/2010/11/18/backbone-js-tutorial/}{Meta
|
|
Cloud},
|
|
\nwanchorto{http://andyet.net/blog/2010/oct/29/building-a-single-page-app-with-backbonejs-undersc/?utm_source=twitterfeed&utm_medium=twitter}{&Yet's
|
|
Tutorial},
|
|
\nwanchor{http://bennolan.com/2010/11/24/backbone-jquery-demo.html}{Backbone
|
|
Mobile} (which is written in
|
|
\nwanchorto{http://jashkenas.github.com/coffee-script/}{Coffee}), and
|
|
\nwanchor{http://joshbohde.com/2010/11/25/backbonejs-and-django/}{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
|
|
\nwanchorto{http://code.quirkey.com/sammy/tutorials/json_store_part1.html}{The
|
|
JsonStore}.
|
|
|
|
In the spirit of The JSON Store, I present The Backbone Store.
|
|
|
|
\subsection{Literate Program}
|
|
|
|
A note: this article was written with the
|
|
\nwanchorto{http://en.wikipedia.org/wiki/Literate_programming}{Literate
|
|
Programming} toolkit
|
|
\nwanchorto{http://www.cs.tufts.edu/~nr/noweb/}{Noweb}. Where you see
|
|
something that looks like \<\<this\>\>, it's a placeholder for code
|
|
described elsewhere in the document. Placeholders with an equal sign
|
|
at the end of them indicate the place where that code is defined. The
|
|
link (U->) indicates that the code you're seeing is used later in the
|
|
document, and (<-U) indicates it was used earlier but is being defined
|
|
here.
|
|
|
|
\subsection{Revision}
|
|
|
|
This is version 1.2 of \textit{The Backbone Store}. It includes
|
|
changes to the store based upon a better understanding of what
|
|
Backbone.js can do. This version uses jQuery 1.5 and Backbone 0.3.3.
|
|
|
|
\subsection{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 shoving. DOM ID ``main'' is empty.
|
|
We'll fill it with templated data later.
|
|
|
|
\subsection{HTML}
|
|
|
|
<<index.html>>=
|
|
<!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>
|
|
<title>
|
|
The Backbone Store
|
|
</title>
|
|
<link rel="stylesheet" href="jsonstore.css" type="text/css" media="screen" charset="utf-8" />
|
|
|
|
<<product list template>>
|
|
|
|
<<product template>>
|
|
|
|
</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.5.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 \nwanchorto{https://github.com/jquery/jquery-tmpl}{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 do not exist; the covers were
|
|
generated during a round of
|
|
\nwanchorto{http://elfs.livejournal.com/756709.html}{The Album Cover
|
|
Game}.)
|
|
|
|
\section{The Program}
|
|
|
|
And here's the skeleton of the program we're going to be writing:
|
|
|
|
<<store.js>>=
|
|
(function() {
|
|
|
|
<<product models>>
|
|
|
|
<<shopping cart models>>
|
|
|
|
<<shopping cart view>>
|
|
|
|
<<product list view>>
|
|
|
|
<<product view>>
|
|
|
|
<<application>>
|
|
|
|
<<initialization>>
|
|
|
|
}).call(this);
|
|
@
|
|
|
|
\section{Models and Collections for the Store}
|
|
|
|
Products are basically what we're selling. In Backbone, a product
|
|
maps to a Backbone Model. Backbone's Model class provides a full
|
|
suite of methods for setting and deleting attributes.
|
|
|
|
One of things I've found useful is to expose the CID (an internal and
|
|
locally unique ``client ID'' generated by Backbone) and use it to
|
|
decorate DOM id's and classes. So, here, I override the Models's
|
|
[[toJSON()]] and add the CID to the representation. In production,
|
|
I've typically created a parent class for all of my classes, overriden
|
|
[[toJSON()]], and extended that instead.
|
|
|
|
<<product models>>=
|
|
var Product = Backbone.Model.extend({
|
|
toJSON: function() {
|
|
return _.extend(_.clone(this.attributes), {cid: this.cid})
|
|
}
|
|
});
|
|
|
|
@
|
|
|
|
A store has a lot of products, so we use a Backbone Collection to keep
|
|
track of them. A Collection always has a Model object associated with
|
|
it; if you attempt to add an object to the Collection that is not an
|
|
instance of [[Backbone.Model]], the Collection will attempt to coerce
|
|
that object into it [[model]] type.
|
|
|
|
Both Models and Collections have a [[toJSON()]] method. The Model
|
|
creates a JSON representation of its attributes, and the collection
|
|
creates a JSON representation of an array, calling [[Model.toJSON()]]
|
|
for each model in contains.
|
|
|
|
|
|
The other novel thing here is the comparator; Backbone uses it define
|
|
the default ordering for the collection. If not defined, calling
|
|
[[sort()]] on the collection raises an exception.
|
|
|
|
<<product models>>=
|
|
var ProductCollection = Backbone.Collection.extend({
|
|
model: Product,
|
|
|
|
comparator: function(item) {
|
|
return item.get('title');
|
|
}
|
|
});
|
|
@
|
|
|
|
Shopping carts are a bit strange. The typical cart has items: each
|
|
item represents one product and the quantity the user wants to buy.
|
|
So a cart may have two items, but the user may be buying five things:
|
|
one of the first item, and four of the second, and so forth.
|
|
|
|
For our (simple) purpose, I'm just going to have an item that you can
|
|
add amounts to, that get stored as a 'quantity'.
|
|
|
|
<<shopping cart models>>=
|
|
var CartItem = Backbone.Model.extend({
|
|
update: function(amount) {
|
|
this.set({'quantity': this.get('quantity') + amount});
|
|
}
|
|
});
|
|
|
|
@
|
|
|
|
The other feature is that, for the collection, I will want to find the
|
|
CartItem not by its ID, but by the ID of the product it contains. So
|
|
I have added the method [[getByProductId]].
|
|
|
|
<<shopping cart models>>=
|
|
var Cart = Backbone.Collection.extend({
|
|
model: CartItem,
|
|
getByProductId: function(pid) {
|
|
return this.detect(function(obj) { return (obj.get('product').cid == pid); });
|
|
},
|
|
});
|
|
|
|
@
|
|
|
|
\section{Views}
|
|
|
|
Backbone Views are simple policy objects. They often have a root
|
|
element, the contents of which they manipulate, a model or collection
|
|
they represent within that root element, events that may occur within
|
|
that root element that they monitor and consequently act on. Views
|
|
are not rigid; it's just Javascript and the DOM, and you can hook
|
|
external events as needed. (This can be useful, for example, when
|
|
doing drag-and-drop with jQueryUI to highlight valid drop zones.)
|
|
More importantly, it is sensitive to events \textit{within its model
|
|
or collection}, and can respond to changes automatically, without
|
|
having to manually invoke the view.
|
|
|
|
There are three views here: the CartView, the ProductListView, and a
|
|
single ProductView.
|
|
|
|
The [[CartView]] lives in the upper-right-hand corner of our screen,
|
|
and just shows the quantity of items in our shopping cart. It has a
|
|
default [[el]], where it will draw its quantity. This view
|
|
illustrates the binding to its collection: whenever the collection is
|
|
updated in some way, the [[CartView]] automagically updates itself.
|
|
The programmer is now relieved of any responsibility of remembering to
|
|
update the view, which is a huge win. The [[\_.bind()]] method
|
|
associates the [[render]] with the instance of the [[CartView]].
|
|
|
|
The [[render()]] method is the conventional name for rendering the
|
|
elements. Nothing in Backbone calls [[render()]] directly; it's up to
|
|
the developer to decide how and when an object should should be
|
|
rendered.
|
|
|
|
This also illustrates the use of jQuery animations in Backbone.
|
|
|
|
<<shopping cart view>>=
|
|
var CartView = Backbone.View.extend({
|
|
el: $('.cart-info'),
|
|
|
|
initialize: function() {
|
|
this.collection.bind('change', _.bind(this.render, this));
|
|
},
|
|
|
|
render: function() {
|
|
var sum = this.collection.reduce(function(m, n) { return m + n.get('quantity'); }, 0);
|
|
this.el
|
|
.find('.cart-items').text(sum).end()
|
|
.animate({paddingTop: '30px'})
|
|
.animate({paddingTop: '10px'});
|
|
}
|
|
});
|
|
|
|
@
|
|
%$
|
|
|
|
The [[ProductListView]] again has a root element, this time the
|
|
[[#main]] DIV of our HTML, into which we're going to draw a jQuery
|
|
template list of our record albums.
|
|
|
|
The only tricks here are the compilation of the jQuery template when
|
|
the View is instantiated, and the use of an enclosured (is that a
|
|
word?) [[self]] variable to provide a hard context for the [[this]]
|
|
variable within inner jQuery calls.
|
|
|
|
<<product list view>>=
|
|
var ProductListView = Backbone.View.extend({
|
|
el: $('#main'),
|
|
indexTemplate: $("#indexTmpl").template(),
|
|
|
|
render: function() {
|
|
var self = this;
|
|
this.el.fadeOut('fast', function() {
|
|
self.el.html($.tmpl(self.indexTemplate, self.model.toJSON()));
|
|
self.el.fadeIn('fast');
|
|
});
|
|
return this;
|
|
}
|
|
});
|
|
|
|
@
|
|
|
|
The view uses a jQuery template. This is a simple, repeatable
|
|
template that jQuery.Template, upon encountering an array, repeats
|
|
until the array is exhausted. Note the presence of [[\${cid}]].
|
|
|
|
<<product list template>>=
|
|
<script id="indexTmpl" type="text/x-jquery-tmpl">
|
|
<div class="item">
|
|
<div class="item-image">
|
|
<a href="#item/${cid}"><img src="${image}" alt="${title}" /></a>
|
|
</div>
|
|
<div class="item-artist">${artist}</div>
|
|
<div class="item-title">${title}</div>
|
|
<div class="item-price">$${price}</div>
|
|
</div>
|
|
</script>
|
|
@
|
|
%$
|
|
|
|
The most complicated object in our ecosystem is the product view. It
|
|
actually does something! The prefix ought to be familiar, but note
|
|
that we're again using [[#main]] as our target; we'll be replacing
|
|
these contents over and over.
|
|
|
|
<<product view>>=
|
|
var ProductView = Backbone.View.extend({
|
|
el: $('#main'),
|
|
itemTemplate: $("#itemTmpl").template(),
|
|
|
|
initialize: function(options) {
|
|
this.cart = options.cart;
|
|
return this;
|
|
},
|
|
|
|
@
|
|
|
|
We want to update the cart as needed. Remember that when we update
|
|
the cart item, the CartView will be notified automagically. Later,
|
|
I'll show how when we initialize and route to a product view, we pass
|
|
in the model associated with it. This code ought to be fairly
|
|
readable: the only specialness is that it's receiving an event, and
|
|
we're ``silencing'' the call to [[cart.add()]], which means that the
|
|
cart collection will not publish any events. There are only events
|
|
when the item has more than zero, and that gets called on
|
|
[[cart_item.update()]].
|
|
|
|
<<product view>>=
|
|
update: function(e) {
|
|
e.preventDefault();
|
|
var cart_item = this.cart.getByProductId(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);
|
|
}
|
|
},
|
|
|
|
@
|
|
%$
|
|
|
|
These are the events in which we're interested: keypresses and clicks
|
|
on the update button and the quantity form. (Okay, ``UQ'' isn't the
|
|
best for ``update quantity''. I admit that.) Note the peculiar
|
|
syntax of ``EVENT SELECTOR'': ``methodByName'' for each event.
|
|
|
|
Backbone tells us that the only events it can track by itself are
|
|
those that jQuery's ``delegate'' understands. As of 1.5, that seems
|
|
to be just about all of them.
|
|
|
|
One thing that I was not aware of until recently: if you remove and
|
|
replace the [[el]] object during the lifespan of your view (including
|
|
in [[initialize()]]), you must then call [[delegateEvents()]] again on
|
|
the new object for these events to work.
|
|
|
|
<<product view>>=
|
|
events: {
|
|
"keypress .uqf" : "updateOnEnter",
|
|
"click .uq" : "update",
|
|
},
|
|
|
|
@
|
|
|
|
And finally the render. There is no rocket science here. You've seen
|
|
this before.
|
|
|
|
%'
|
|
<<product view>>=
|
|
render: function() {
|
|
var self = this;
|
|
this.el.fadeOut('fast', function() {
|
|
self.el.html($.tmpl(self.itemTemplate, self.model.toJSON()));
|
|
self.el.fadeIn('fast');
|
|
});
|
|
return this;
|
|
}
|
|
});
|
|
|
|
@
|
|
|
|
The template for a ProductView is straightforward. It contains the
|
|
form with the [[uq]] objects, the actions of which we intercept and
|
|
operate on internally. Backbone does this automatically using
|
|
jQuery's [[delegate]] method.
|
|
|
|
<<product template>>=
|
|
<script id="itemTmpl" type="text/x-jquery-tmpl">
|
|
<div class="item-detail">
|
|
<div class="item-image"><img src="${large_image}" alt="${title}" /></div>
|
|
<div class="item-info">
|
|
<div class="item-artist">${artist}</div>
|
|
<div class="item-title">${title}</div>
|
|
<div class="item-price">$${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="${url}">Buy this item on Amazon</a></div>
|
|
<div class="back-link"><a href="#">« Back to Items</a></div>
|
|
</div>
|
|
</div>
|
|
</script>
|
|
@
|
|
|
|
%'
|
|
\section{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.
|
|
|
|
To begin with, I'm going to keep track of the ``three'' views I care
|
|
about: the CartView, the ProductListView, and the ProductView. I'm
|
|
going to cheat by attaching the ProductViews to their individual
|
|
products, and invoke that view as necessary.
|
|
|
|
<<application>>=
|
|
var BackboneStore = Backbone.Controller.extend({
|
|
_index: null,
|
|
_products: null,
|
|
_cart :null,
|
|
@
|
|
%$
|
|
|
|
There are only two routes: home, and item:
|
|
|
|
<<application>>=
|
|
routes: {
|
|
"": "index",
|
|
"item/:id": "item",
|
|
},
|
|
|
|
@
|
|
|
|
Here's where things get interesting. There are two schools of thought
|
|
over the Controller; one, that the Controller ought to be able to get
|
|
all the data it needs, and two, that the Controller ought to begin
|
|
with enough data to do the job sensibly. I fall into the second camp.
|
|
I'm going to pass in to the [[initialize()]] method an array of
|
|
objects representing all the products in the system.
|
|
|
|
<<application>>=
|
|
initialize: function(data) {
|
|
this._cart = new Cart();
|
|
new CartView({collection: this._cart});
|
|
this._products = new ProductCollection(data);
|
|
this._index = new ProductListView({model: this._products});
|
|
return this;
|
|
},
|
|
|
|
@
|
|
|
|
When we're routed to the [[index]] method, all we need to do is render
|
|
the index:
|
|
|
|
<<application>>=
|
|
index: function() {
|
|
this._index.render();
|
|
},
|
|
|
|
@
|
|
|
|
When we are routed to a product, we need to find that product, get its
|
|
view if it has one or create one if it doesn't, then call render:
|
|
|
|
<<application>>=
|
|
item: function(id) {
|
|
var product = this._products.getByCid(id);
|
|
if (_.isUndefined(product._view)) {
|
|
product._view = new ProductView({model: product,
|
|
cart: this._cart});
|
|
}
|
|
product._view.render();
|
|
}
|
|
});
|
|
|
|
@
|
|
|
|
And that's the entirety of the application.
|
|
|
|
\section{Initialization}
|
|
|
|
Initialization for most single-page applications happens when the DOM
|
|
is ready. So I'll do exactly that.
|
|
|
|
This should be obvious, except what the Hell is that when/then
|
|
construct? That's a new feature of jQuery 1.5 called Deferreds (also
|
|
known as Promises or Futures). All jQuery 1.5 ajax calls are
|
|
Deferreds that return data when you dereference them; [[when()]] is an
|
|
instruction to wait until the ajax call is done, and [[then()]] is a
|
|
chained instruction on what to do next.
|
|
|
|
This is a trivial example, but when you have multiple streams of data
|
|
coming in (say, you're loading independent schemas, or you have
|
|
multiple, orthagonal data sets in your application, each with their
|
|
own URL as per the Richardson Maturity Model), you can pass the array
|
|
of ajax objects to [[when()]] and [[then()]] won't fire until they're
|
|
all done. Automagic synchronization is a miracle.
|
|
|
|
<<initialization>>=
|
|
$(document).ready(function() {
|
|
var fetch_items = function() {
|
|
return $.ajax({
|
|
url: 'data/items.json',
|
|
data: {},
|
|
contentType: "application/json; charset=utf-8",
|
|
dataType: "json"
|
|
});
|
|
};
|
|
|
|
$.when(fetch_items()).then(function(data) {
|
|
new BackboneStore(data);
|
|
Backbone.history.start();
|
|
});
|
|
});
|
|
@
|
|
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
|
|
\nwanchorto{https://github.com/elfsternberg/The-Backbone-Store}{The
|
|
Backbone Store}.
|
|
|
|
\end{document}
|