The-Backbone-Store/backbonestore.nw

852 lines
31 KiB
Plaintext
Raw Normal View History

% -*- Mode: noweb; noweb-code-mode: javascript-mode ; noweb-doc-mode: latex-mode -*-
\documentclass{article}
\usepackage{noweb}
2011-08-07 19:33:07 +00:00
\usepackage[T1]{fontenc}
\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}
2011-08-07 19:33:07 +00:00
\nwanchorto{http://documentcloud.github.com/backbone/}{Backbone.js} is
a popular Model-View-Controller (MVC) library that provides a
framework by which models generate events and views reflect those
events. The models represent data and ways in which that data can be
chnaged. The nifty features of backbone are (1) its event-driven
architecture, which separate a complex, working model of
\textbf{objects} and their relationships, and the way those things and
their relationships are presented to the viewer, and (2) its router,
which allows developers to create bookmark-ready URLs for specialized
views. Backbone also provides a Sync library which will RESTfully
shuttle objects back and forth between the browser and the client.
There are a number of good tutorials for Backbone (See:
\nwanchorto{http://www.plexical.com/blog/2010/11/18/backbone-js-tutorial/}{Meta
Cloud},
2011-08-07 19:33:07 +00:00
\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},
\nwanchorto{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
2011-08-07 19:33:07 +00:00
\nwanchorto{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
2011-08-07 19:33:07 +00:00
something that looks like \textless \textless this \textgreater \textgreater, 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
2011-08-07 19:33:07 +00:00
link (U-\textgreater) indicates that the code you're seeing is used later in the
document, and (\textless-U) indicates it was used earlier but is being defined
here.
\subsection{Revision}
2011-08-07 19:33:07 +00:00
This is version 2.0 of \textit{The Backbone Store}. It includes
changes to the store based upon a better understanding of what
2011-08-07 19:33:07 +00:00
Backbone.js can do. This version uses jQuery 1.6.2 and Backbone
0.5.2.
\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.
2011-08-07 19:33:07 +00:00
We will be creating a store for music albums. There will be: (1) The
catalog of products, (2) A detail page for a specific product from the
catalog, (3) A ``checkout page'' where users can add/change/delete
items from their shopping cart, and (4) a shopping cart ``widget''
that is visible on every page, and allows the user to see how many
items are in the cart and how much money those items cost.
This is taken, more or less, straight from The JSON Store. We will be
getting our data from 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}, a meme one popular with graphic designers.)
Under the covers, we have two essential objects: a \textbf{Product}
that we're selling, and a shopping cart \textbf{Item} into which we
put a reference to a Product and a count of the number of that product
that we're selling. In the Backbone idiom, we will be callling the
cart an \textbf{ItemCollection} that the user wants to buy, and the
Products will be kept in a \textbf{ProductCollection}
In backbone's parlance, Product and Item are \textbf{Models}, and Cart
and Catalog are \textbf{Collections}. The idiom is that models are
named for what they represent, and collections are model names
suffixed with the word ``collection.'' The pages ``catalog,''
``product detail,'' and ``checkout'' are \textbf{Routable Views},
while the shopping cart widget is just a \textbf{View}. There's no
programmatic difference internally between the two kinds of views;
instead, the difference is in how they're accessed.
\subsection{Models}
The first version of this tutorial concentrated on the HTML. In this
version, we're going to start logically, with the models. The first
model is \textbf{Product}, that is, the thing we're selling in our
store. We will create Products by inheriting from Backbone's
\textbf{Model}.
Backbone models use the methods [[get()]] and [[set()]] to access the
attributes of the model. When you want to change a model's attribute,
you must do so through those methods. Any other object that has even
a fleeting reference to the model can then subscribe to the
\textbf{change} event on that model, and whenever [[set()]] is called,
those other objects can react in some way. This is one of the most
important features of Backbone, and you'll see why shortly.
Because a Backbone model maintains its attributes as a javascript
object, it is schema-free. So the Product model is ridiculously
simple:
<<product models>>=
var Product = Backbone.Model.extend({})
@
And we said before, the products are kept in a catalog. Backbone's
``list of models'' feature is called a \textbf{Collection}, and to
stay in Backbone's idioms, rather than call it ``Catalog'', we'll call
it a \textbf{ProductCollection}:
<<product models>>=
var ProductCollection = Backbone.Collection.extend({
model: Product,
2011-08-07 19:33:07 +00:00
comparator: function(item) {
return item.get('title');
}
});
@
Collections have a reference to the Product constructor; if you call
[[Collection.add()]] with a JSON object, it will use that
constructor to create the associated Backbone model object.
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.
Shopping carts have always seemed a bit strange to me, because each
item isn't a one-to-one with a product, but a reference to the product
and a quantity. 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 Item = 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 product it contains, and I want the
Cart to be able to host any product, even it it has none of those, So
I have added the method [[getOrCreateItemForProduct]]. The
[[detect()]] and [[reduce()]] methods ares provided by Backbone's one
major dependency, a wonderful utility library called
\texttt{Underscore}. [[detect()]] returns the first object for which
the anonymous function return [[true]]. The [[reduce()]] functions
take an intitial value and a means of calculating a per-object value,
and reduce all that to a final value for all objects in the
collection.
<<shopping cart models>>=
var ItemCollection = Backbone.Collection.extend({
model: Item,
getOrCreateItemForProduct: function(product) {
var i,
pid = product.get('id'),
o = this.detect(function(obj) {
return (obj.get('product').get('id') == pid);
});
if (o) {
return o;
}
i = new Item({'product': product, 'quantity': 0})
this.add(i, {silent: true})
return i;
},
getTotalCount: function() {
return this.reduce(function(memo, obj) {
return obj.get('quantity') + memo; }, 0);
}
getTotalCost: function() {
return this.reduce(function(memo, obj) {
return (obj.get('product').get('price') *
obj.get('quantity')) + memo; }, 0);
}
});
@
\subsection {Views}
Now that we have the structure for our catalog and our shopping cart
laid out, let's show you how those are organized visually. I'd like
to say that it's possible to completely separate View and their
descriptions of how to interact with the DOM with DOM development, but
we must have some preliminary plans for dealing with the display.
The plan is to have a one-page display for everything. We will have
an area of the screen allocated for our major, routable views (the
product list display, the product detail display, and the checkout
display), and a small area of the screen allocated for our shopping
cart. Let's put the shopping cart link in the upper-right-hand
corner; everybody does.
As an additional feature, we want the views to transition elegantly,
using the jQuery [[fadeIn()]] and [[fadeOut()]] animations.
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.
A Backbone view can be either an existing DOM element, or it can
generate one automatically at (client-side) run time. In the previous
version of the tutorial, I used existing DOM elements, but for this
one, almost everything will be generated at run time.
To achieve the animations and enforce consistency, we're going to
engage in classic object-oriented programming. We're going to create
a base class that contains knowledge about the main area into which
all views are rendered, and that manages these transitions. With this
technique, you can do lots of navigation-related tricks: you can
highlight where the user is in breadcrumb-style navigation; you can
change the class and highlight an entry on a nav bar; you can add and
remove tabs from the top of the viewport as needed.
<<base view>>=
var _BaseView = Backbone.View.extend({
parent: '#main',
className: 'viewport',
@
The above says that we're creating a class called \texttt{BaseView}
and defining two fields. The first, 'parent', will be used by all
child views to identify in which DOM object the view will be rendered.
The second defines a common class we will use for the purpose of
identifying these views to jQuery. Backbone automatically creates a
new [[DIV]] object with the class 'viewport' when a view
constructor is called. It will be our job to attach that [[DIV]]
to the DOM.
<<base view>>=
initialize: function() {
this.el = $(this.el); //$
this.el.hide();
this.parent.append(this.el);
return this.
},
@
The method above ensures that the element is rendered, but not
visible, and contained within the [[DIV#main]]. Note also that
the element is not a sacrosanct object; the Backbone.View is more a
collection of standards than a mechanism of enforcement, and so
defining it from a raw DOM object to a jQuery object will not break
anything.
Next, we will define the hide and show functions:
<<base view>>=
hide: function() {
if (this.el.is(":visible") === false) {
return null;
}
promise = $.Deferred(function(dfd) { //$
this.el.fadeOut('fast', dfd.resolve)
}).promise();
this.trigger('hide', this);
return promise;
},
show: function() {
if (this.el.is(':visible')) {
return;
}
promise = $.Deferred(function(dfd) { //$
this.el.fadeIn('fast', dfd.resolve)
}).promise();
this.trigger('show', this);
return promise;
}
@
\textbf{Deferred} is a new feature of jQuery. It is a different
mechanism for invoking callbacks by attaching attributes and behavior
to the callback function. By using this, we can say thing like
``\textit{When} everything is hidden (when every deferred returned
from \textbf{hide} has been resolved), \textit{then} show the
appropriate viewport.'' Deferreds are incredibly powerful, and this
is a small taste of what can be done with them.
Before we move on, let's take a look at the HTML we're going to use
for our one-page application:
<<index.html>>=
2011-08-07 19:33:07 +00:00
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>
The Backbone Store
</title>
2011-08-07 19:33:07 +00:00
<link rel="stylesheet" href="jsonstore.css" type="text/css">
<<product list template>>
<<product template>>
2011-08-07 19:33:07 +00:00
<<checkout template>>
</head>
<body>
<div id="container">
<div id="header">
<h1>
The Backbone Store
</h1>
<div class="cart-info">
</div>
</div>
<div id="main"> </div>
</div>
2011-08-07 19:33:07 +00:00
<script src="jquery-1.6.2.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>
@
2011-08-07 19:33:07 +00:00
It's not much to look at, but already you can see where that
[[DIV#main]] goes, as well as where we are putting our templates.
The [[DIV#main]] will host a number of viewports, only one of
which will be visible at any given time.
2011-08-07 19:33:07 +00:00
Our first view is going to be the product list view, named, well,
guess. Or just look down a few lines.
2011-08-07 19:33:07 +00:00
This gives us a chance to discuss one of the big confusions new
Backbone users frequently have: \textit{What is \texttt{render()}
for?}. Render is not there to show or hide the view.
\texttt{Render()} is there to \textit{change the view when the
underlying data changes}. It renders the data into a view. In our
functionality, we use the parent class's \texttt{show()} and
\texttt{hide()} methods to actually show the view.
2011-08-07 19:33:07 +00:00
<<product list view>>=
var ProductListView = _BaseView.extend({
id: 'productlistview',
indexTemplate: $("#store_index_template").template(), //$
2011-08-07 19:33:07 +00:00
render: function() {
self.el.html(_.template(this.template, {'products': this.model.toJSON()}))
return this;
}
});
2011-08-07 19:33:07 +00:00
@
2011-08-07 19:33:07 +00:00
That \texttt{\_.template()} method is provided by undescore.js, and is
a fairly powerful templating method. It's not the fastest or the most
feature-complete, but it is more than adequate for our purposes and it
means we don't have to import another library. It vaguely resembles
ERB from Rails, so if you are familiar with that, you should
understand this fairly easily.
2011-08-07 19:33:07 +00:00
And here is the HTML:
2011-08-07 19:33:07 +00:00
<<product list template>>=
<script id="store_index_template" type="text/x-jquery-tmpl">
<h1>Product Catalog</h1>
<ul>
<% for(i=0,l=products.length;i<l;++i) { p = products[i]; %>
<li class="item">
<div class="item-image">
<a href="#item/<%= p.cid%>">
<img src="<%= p.image %>" alt="<%= p.title %>" /></a>
</div>
<div class="item-artist"><%= p.artist %></div>
<div class="item-title"><%= p.title %></div>
<div class="item-price">$<%= p.price %></div>
</li>
<% } %>
</ul>
</script>
@
%$
2011-08-07 19:33:07 +00:00
One of the most complicated objects in our ecosystem is the product
view. It actually does something! The prefix ought to be familiar,
but note that we are again using [[#main]] as our target; we will be
showing and hiding the various [[DIV]] objects in [[#main]] again and
again.
The only trickiness here is twofold: the (rather hideous) means by
which one calls the method of a parnt class from a child class via
Backbone's class heirarchy (this is most definitely \textbf{not}
Javascript standard), and keeping track of the itemcollection object,
so we can add and change items as needed.
<<product detail view>>=
var ProductListView = _BaseView.extend({
id: 'productlistview',
indexTemplate: $("#store_item_template").template(), //$
initialize: function(options) {
this.constructor.__super__.initialize.apply(this, [options])
this.itemcollection = options.itemcollection;
return this;
},
@
2011-08-07 19:33:07 +00:00
We want to update the cart as needed. Remember the way Backbone is
supposed to work: when we update the cart, it will send out a signal
automatically, and subscribers (in this case, that little widget in
the upper right hand corner we mentioned earlier) will show the
changes.
2011-08-07 19:33:07 +00:00
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.
2011-08-07 19:33:07 +00:00
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.
2011-08-07 19:33:07 +00:00
<<product view>>=
events: {
"keypress .uqf" : "updateOnEnter",
"click .uq" : "update",
},
@
2011-08-07 19:33:07 +00:00
And now we will deal with the update. 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()]].
In the original tutorial, this code had a lot of responsibility for
managing the shopping cart, looking into it and seeing if it had an
item for this product, and there was lots of accessing the model to
get its id and so forth. All of that has been put into the shopping
cart model, which is where it belongs: \textit{knowledge about items
and each item's relationship to its collection belongs in the
collection}.
%'
2011-08-07 19:33:07 +00:00
<<product view>>=
update: function(e) {
e.preventDefault();
var item = this.itemcollection.getOrCreateItemProduct(this.model);
item.update(parseInt($('.uqf').val()));
},
updateOnEnter: function(e) {
if (e.keyCode == 13) {
return this.update(e);
}
},
2011-08-07 19:33:07 +00:00
@
%$
2011-08-07 19:33:07 +00:00
So, let's talk about that shopping cart thing. We've been making the
point that when it changes, automatically you should see just how many
2011-08-07 19:33:07 +00:00
\section{The Program}
2011-08-07 19:33:07 +00:00
And here's the skeleton of the program we're going to be writing:
2011-08-07 19:33:07 +00:00
<<store.js>>=
(function() {
2011-08-07 19:33:07 +00:00
<<product models>>
2011-08-07 19:33:07 +00:00
<<shopping cart models>>
2011-08-07 19:33:07 +00:00
<<shopping cart view>>
2011-08-07 19:33:07 +00:00
<<product list view>>
<<product view>>
<<application>>
2011-08-07 19:33:07 +00:00
<<initialization>>
}).call(this);
@
2011-08-07 19:33:07 +00:00
\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>
@
%$
2011-08-07 19:33:07 +00:00
The most complicated object .
<<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}