The-Backbone-Store/backbonestore.nw

562 lines
19 KiB
Plaintext
Raw Normal View History

% -*- Mode: noweb; noweb-code-mode: javascript-mode ; noweb-doc-mode: latex-mode -*-
\documentclass{article}
\usepackage{noweb}
\usepackage{hyperref}
\begin{document}
2010-12-09 01:38:29 +00:00
% 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="#">&laquo; 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}