468 lines
16 KiB
Plaintext
468 lines
16 KiB
Plaintext
|
\documentclass{article}
|
||
|
\usepackage{noweb}
|
||
|
\usepackage{hyperref}
|
||
|
\begin{document}
|
||
|
|
||
|
I've been learning how to use \nwanchorto{http://documentcloud.github.com/backbone/}{Backbone.js}, a nifty little library for
|
||
|
organizing your client-side Javascript into a classic
|
||
|
Model-View-Controller paradigm while trying (and to some extent,
|
||
|
succeeding) in trying to burden you, the user, with as little
|
||
|
additional learning as possible. I consider this a good thing; the
|
||
|
overhead of learning a library and its accompaning DSL represent
|
||
|
additional cognitive loads that developers can better use elsewhere.
|
||
|
Keeping as much as possible within familiar paradigms is not just
|
||
|
useful, it's necessary as our programs get bigger.
|
||
|
|
||
|
The tutorial for Backbone is woefully lacking in specifics, and the
|
||
|
example program, Todo, doesn't really have much chops in teaching you
|
||
|
the ins and outs of Backbone, especially not its new Controller and
|
||
|
History modules. But in the announcement for Backbone.Controller,
|
||
|
Jeremy Ashkenas hid a clue: There's another library, Sammy.js, that
|
||
|
does something similar, and they do have a 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, and
|
||
|
online store written entirely in JSON and operating entirely within a
|
||
|
single page.
|
||
|
|
||
|
Let's start by showing you the HTML that we're going to be exploiting:
|
||
|
|
||
|
<<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>
|
||
|
<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" />
|
||
|
|
||
|
<<index 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.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 \nwanchorto{https://github.com/jquery/jquery-tmpl}{jQuery Templates kit}. We'll discuss those in a minute. There's 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 \nwanchorto{http://elfs.livejournal.com/756709.html}{The Album Cover
|
||
|
Game}.)
|
||
|
|
||
|
There are two views, the index and the item. So, using
|
||
|
[[Backbone.Controller]], we're going to route the following:
|
||
|
|
||
|
<<routes>>=
|
||
|
routes: {
|
||
|
"": "index",
|
||
|
"item/:id": "item",
|
||
|
},
|
||
|
@
|
||
|
|
||
|
Unlike Sammy, Backbone mostly only routes GET commands. Routes are to
|
||
|
routes to views; everything else happens more or less under the
|
||
|
covers.
|
||
|
|
||
|
As Backbone is running, the [[Backbone.History]] module is listening to
|
||
|
the hash object, waiting for it to change so that it can trigger a
|
||
|
``route'' event, in which case the function named as the value in the
|
||
|
route is called.
|
||
|
|
||
|
There are a few things I want to track: the index view, the individual
|
||
|
product views, and the shopping cart.
|
||
|
|
||
|
<<application variables>>=
|
||
|
_index: null,
|
||
|
_products: null,
|
||
|
_cart :null,
|
||
|
@
|
||
|
|
||
|
Using backbone, I have a list of products. So, I should declare
|
||
|
those. 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.
|
||
|
|
||
|
<<product models>>=
|
||
|
var Product = Backbone.Model.extend({});
|
||
|
|
||
|
var ProductCollection = Backbone.Collection.extend({
|
||
|
model: Product,
|
||
|
comparator: function(item) {
|
||
|
return item.get('title');
|
||
|
}
|
||
|
});
|
||
|
@
|
||
|
|
||
|
That's not very exciting. Let's show this list, with a View. Let's
|
||
|
call it IndexView:
|
||
|
|
||
|
<<index view>>=
|
||
|
var IndexView = 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;
|
||
|
}
|
||
|
|
||
|
});
|
||
|
@
|
||
|
|
||
|
This code defines a [[Backbone.View]] object, in which the parent element
|
||
|
is #main, the render function fades out the existing elements in that
|
||
|
position and replaces them with the contents of a rendered jQuery
|
||
|
template, and then fades the element back in.
|
||
|
|
||
|
The index template looks like this:
|
||
|
|
||
|
<<index template>>=
|
||
|
<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
|
||
|
\nwanchorto{http://en.wikipedia.org/wiki/Law_of_Demeter}{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
|
||
|
\nwanchorto{http://ejohn.org/blog/javascript-getters-and-setters/}{Javascript
|
||
|
Getter and Setters} become so well-established as to make [[set]]
|
||
|
and [[get]] irrelevant.)
|
||
|
|
||
|
Now, we can render the index view:
|
||
|
|
||
|
<<index render call>>=
|
||
|
index: function() {
|
||
|
this._index.render();
|
||
|
},
|
||
|
@
|
||
|
|
||
|
At this point, well, we need an application. A controller. And we
|
||
|
need to initialize it, and call it. Here's what it looks like (some
|
||
|
of this, you've already seen):
|
||
|
|
||
|
<<workspace>>=
|
||
|
var Workspace = Backbone.Controller.extend({
|
||
|
<<application variables>>
|
||
|
|
||
|
<<routes>>
|
||
|
|
||
|
<<initialization>>
|
||
|
|
||
|
<<index render call>>
|
||
|
|
||
|
<<product render call>>
|
||
|
});
|
||
|
|
||
|
workspace = new Workspace();
|
||
|
Backbone.history.start();
|
||
|
@
|
||
|
|
||
|
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 index view. (Product views, we'll discuss in a moment).
|
||
|
|
||
|
<<initialization>>=
|
||
|
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 IndexView({model: ws._products});
|
||
|
Backbone.history.loadUrl();
|
||
|
}
|
||
|
});
|
||
|
return this;
|
||
|
}
|
||
|
return this;
|
||
|
},
|
||
|
@
|
||
|
|
||
|
We haven't defined the Cart yet, but that's all right. We'll get to
|
||
|
it.) But here you see a lot of what's already existent being used: we
|
||
|
get a ProductCollection, and an IndexView.
|
||
|
|
||
|
That last line is curious. It's an instruction to Backbone to look at
|
||
|
the URL; if the user navigated to something other than the home page,
|
||
|
it's to use the routes defined to go there. Users can now bookmark
|
||
|
places in your site other than the home page. Yes, the bookmark will
|
||
|
be funny and have at least one
|
||
|
\nwanchorto{http://en.wiktionary.org/wiki/octothorpe}{octothorpe} in it, but
|
||
|
it will work.
|
||
|
|
||
|
Let's deal with the shopping cart:
|
||
|
|
||
|
<<shopping cart models>>=
|
||
|
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); });
|
||
|
},
|
||
|
});
|
||
|
@
|
||
|
|
||
|
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.
|
||
|
|
||
|
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:
|
||
|
|
||
|
<<shopping cart view>>=
|
||
|
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 animated, and then pop it off
|
||
|
after the operation has been applied.
|
||
|
|
||
|
The biggest thing left is the [[ProductView]]. It's skeleton looks
|
||
|
like this:
|
||
|
|
||
|
<<product view>>=
|
||
|
var ProductView = Backbone.View.extend({
|
||
|
el: $('#main'),
|
||
|
itemTemplate: $("#itemTmpl").template(),
|
||
|
|
||
|
<<product events>>
|
||
|
|
||
|
initialize: function(options) {
|
||
|
this.cart = options.cart;
|
||
|
},
|
||
|
|
||
|
<<update product>>
|
||
|
|
||
|
<<render product>>
|
||
|
});
|
||
|
|
||
|
@
|
||
|
|
||
|
First, we find the element we're going to work with, and the template.
|
||
|
I expect the ProductView to be where we'll add items to the cart, so
|
||
|
the initializer here expects to have a handle on the cart.
|
||
|
|
||
|
And the template:
|
||
|
|
||
|
<<product template>>=
|
||
|
<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>
|
||
|
@
|
||
|
|
||
|
One extra item: 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.
|
||
|
|
||
|
Rendering the product is not difficult:
|
||
|
|
||
|
<<render product>>=
|
||
|
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;
|
||
|
}
|
||
|
@
|
||
|
|
||
|
That looks familiar.
|
||
|
|
||
|
Updating the product, however, is a whole 'nother story. Note that
|
||
|
each product has a form associated with it. We need to intercept any
|
||
|
form update events and manipulate our shopping cart. We have two
|
||
|
objects that can do that: the input field, and the submit button. I
|
||
|
need to intercept those events:
|
||
|
|
||
|
<<product events>>=
|
||
|
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''.
|
||
|
|
||
|
The update then becomes straightforward. 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:
|
||
|
|
||
|
<<update product>>=
|
||
|
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);
|
||
|
}
|
||
|
},
|
||
|
@
|
||
|
|
||
|
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]].
|
||
|
|
||
|
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.
|
||
|
|
||
|
<<product render call>>=
|
||
|
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 now my store looks like
|
||
|
|
||
|
<<store.js>>=
|
||
|
<<product models>>
|
||
|
|
||
|
<<shopping cart models>>
|
||
|
|
||
|
<<shopping cart view>>
|
||
|
|
||
|
<<product view>>
|
||
|
|
||
|
<<index view>>
|
||
|
|
||
|
<<workspace>>
|
||
|
@
|
||
|
|
||
|
As always, this code is available at github.
|
||
|
|
||
|
\end{document}
|