Compare commits

...

19 Commits

Author SHA1 Message Date
Elf M. Sternberg 366dc6159d [★] Added built products.
There's no reason other people need to replicate my idiosyncratic toolchain.
2016-04-15 22:05:02 -07:00
Elf M. Sternberg 645d8650c4 Added note about inotify and Linux. 2016-04-15 22:02:07 -07:00
Elf M. Sternberg e099afe7f3 Stray directory; not needed. 2016-04-15 22:00:59 -07:00
Elf M. Sternberg 84980f77a2 Cleaned up the LaTeX a *lot*, and updated the README. 2016-04-15 21:59:22 -07:00
Elf M. Sternberg 075949dcc7 Updated CSS to try and overcome a FLOUT issue. 2016-04-15 21:38:22 -07:00
Ken Elf Mathieu Sternberg d85b81ff9c Added bower to local node_modules for devDependencies. 2016-04-15 21:14:55 -07:00
Elf M. Sternberg 39bd7ee63b Trying to clean up the promises patterns. 2016-04-15 17:36:32 -07:00
Elf M. Sternberg 334fb0efff HTML production is... marginally okay. 2016-04-15 17:00:55 -07:00
Elf M. Sternberg 08a5111d00 Adding notes; fixing documentation production. 2016-04-15 16:54:52 -07:00
Elf M. Sternberg 137e788261 The codebase works end-to-end. Huzzah. 2016-04-15 16:42:23 -07:00
Elf M. Sternberg 7033753dc7 Fixed Makefile to error out when libraries not installed correctly.
Update .gitignore to ignore that frippin' YAML file.  Hate that damn thing.
2016-04-15 15:48:59 -07:00
Elf M. Sternberg fdf2b4367d Slowly modernizing the tutorial.
Fixed autoreload and Makefile to be more, um, relative.
2016-04-15 15:43:05 -07:00
Elf M. Sternberg 2b09593ec7 Still working on the Makefile. 2016-04-15 15:22:16 -07:00
Elf M. Sternberg 02828b2bae Modernization proceeds apace. 2016-04-15 15:06:16 -07:00
Elf M. Sternberg f6ba14f17b Updated readme. 2011-08-07 21:37:40 -07:00
Elf M. Sternberg 6c6772dc79 Handled quantity update error that was fixed in Master, but not in Modern. 2011-08-07 21:34:41 -07:00
Elf M. Sternberg b7603b24f8 Everything comes out of the Noweb file now. 2011-08-07 21:18:47 -07:00
Elf M. Sternberg 40df6a023f Built to work with Coffee, Style, and HAML. 2011-08-07 19:53:05 -07:00
Elf M. Sternberg 88cf744296 Updating to post-modern standards. 2011-08-07 12:34:16 -07:00
36 changed files with 1906 additions and 3266 deletions

7
.gitignore vendored
View File

@ -4,3 +4,10 @@
.#*
.DS_Store
*~
node_modules/*
bower_components/*
npm-debug.log
docs/*.html
docs/*.tex
htdocs/lib
package.yml

120
Makefile
View File

@ -1,58 +1,94 @@
.SUFFIXES: .nw .js .pdf .html .tex
.PHONY: setup store serve
NOTANGLE= notangle
NOWEAVE= noweave
ECHO= /bin/echo
ECHO= echo
STYLUS= ./node_modules/stylus/bin/stylus
HAML= haml
COFFEE= ./node_modules/coffee-script/bin/coffee
all: index.html store.js
LIBS:= htdocs/lib/underscore.js htdocs/lib/jquery.js htdocs/lib/backbone.js
.nw.html:
$(NOWEAVE) -filter l2h -delay -x -index -autodefs c -html $*.nw > $*.html
all: htdocs/index.html htdocs/store.js htdocs/jsonstore.css htdocs/data/items.json
@if [ ! -e "./htdocs/lib" ]; then \
echo "Please do 'make setup' before continuing"; \
exit 1; \
fi
.nw.tex:
$(NOWEAVE) -x -delay $*.nw > $*.tex #$
serve: all
$(COFFEE) ./bin/autoreload
.tex.pdf:
xelatex $*.tex; \
while grep -s 'Rerun to get cross-references right' $*.log; \
store: all
htdocs/lib:
mkdir -p htdocs/lib
htdocs/lib/underscore.js: htdocs/lib
cp bower_components/underscore/underscore.js htdocs/lib
htdocs/lib/jquery.js: htdocs/lib
cp bower_components/jquery/dist/jquery.js htdocs/lib
htdocs/lib/backbone.js:
cp bower_components/backbone/backbone.js htdocs/lib
install:
npm install
./node_modules/bower/bin/bower install jquery underscore backbone
setup: install $(LIBS)
work:
mkdir -p work
docs:
mkdir -p docs
work/index.haml: work src/backbonestore.nw
$(NOTANGLE) -c -Rindex.haml src/backbonestore.nw > work/index.haml
htdocs/index.html: htdocs work/index.haml
$(HAML) --unix-newlines --no-escape-attrs --double-quote-attribute work/index.haml > htdocs/index.html
htdocs/jsonstore.css: htdocs work/jsonstore.styl
$(STYLUS) -o htdocs work/jsonstore.styl
work/jsonstore.styl: work src/backbonestore.nw
$(NOTANGLE) -c -Rjsonstore.styl src/backbonestore.nw > work/jsonstore.styl
htdocs/store.js: htdocs work/store.coffee
$(COFFEE) -o htdocs --compile work/store.coffee
work/store.coffee: work src/backbonestore.nw
$(NOTANGLE) -c -Rstore.coffee src/backbonestore.nw > work/store.coffee
docs/backbonestore.tex: docs src/backbonestore.nw
${NOWEAVE} -x -delay src/backbonestore.nw > docs/backbonestore.tex
docs/backbonestore.pdf: docs/backbonestore.tex
xelatex docs/backbonestore.tex; \
while grep -s 'Rerun to get cross-references right' ./backbonestore.log; \
do \
xelatex *$.tex; \
xelatex docs/backbonestore.tex; \
done
mv backbonestore.pdf docs
rm -f ./backbonestore.log ./backbonestore.aux ./backbonestore.out
.nw.js:
@ $(ECHO) $(NOTANGLE) -c -R$@ $<
@ - $(NOTANGLE) -c -R$@ $< > $*.nw-js-tmp
@ if [ -s "$*.nw-js-tmp" ]; then \
mv $*.nw-js-tmp $@; \
else \
echo "$@ not found in $<"; \
rm $*.nw-js-tmp; \
fi
pdf: docs/backbonestore.pdf
store.js: backbonestore.nw
@ $(ECHO) $(NOTANGLE) -c -R$@ $<
@ - $(NOTANGLE) -c -R$@ $< > $*.nw-html-tmp
@ if [ -s "$*.nw-html-tmp" ]; then \
mv $*.nw-html-tmp $@; \
else \
echo "$@ not found in $<"; \
rm $*.nw-tmp; \
fi
index.html:
@ $(ECHO) $(NOTANGLE) -c -R$@ $<
@ - $(NOTANGLE) -c -R$@ $< > $*.nw-html-tmp
@ if [ -s "$*.nw-html-tmp" ]; then \
mv $*.nw-html-tmp $@; \
else \
echo "$@ not found in $<"; \
rm $*.nw-tmp; \
fi
docs/backbonestore.html: docs src/backbonestore.nw
$(NOWEAVE) -filter l2h -delay -x -autodefs c -html src/backbonestore.nw > docs/backbonestore.html
html: docs/backbonestore.html
clean:
- rm -f *.tex *.dvi *.aux *.toc *.log *.out *.html *.js
- rm -f htdocs/*.* docs/*.tex docs/*.dvi docs/*.aux docs/*.toc docs/*.log docs/*.out
- rm -fr ./work
distclean: clean
- rm -fr ./htdocs/lib
realclean: distclean
- rm -fr docs
realclean: clean
- rm -f *.pdf

27
README
View File

@ -1,27 +0,0 @@
The Backbone Store is a simple demonstration application, a Backbone.js
version of the Sammy.js tutorial.
## Installation
The Backbone store must be installed under a webserver in order to
operate correctly. Chrome, especially, will not initialize the
application from the filesystem. Just unpack it in a target directory
of your choosing and then browse to that directory.
## Copyright
Store.js is entirely my own work, and is Copyright (c) 2010 Elf
M. Sternberg. Included libraries are covered by their respective
copyright holders, and are used with permission of the licenses
included. Store.js is intended for educational purposes only, rather
than to be working code, and is hereby licensed under the Creative
Commons Attribution Non-Commercial Share Alike (by-nc-sa) licence.
The images contained herein are derivative works of photographs
licensed under Creative Commons licences for non-commercial purposes.
## Contribution
Please look in backbonestore.nw for the base code. Backbonestore.nw
is produced using the Noweb Literate Programming toolkit by Norman
Ramsey (http://www.cs.tufts.edu/~nr/noweb/).

67
README.md Normal file
View File

@ -0,0 +1,67 @@
# About
The Backbone Store is a tutorial and demonstration application for the
BackboneJS framework.
## Installation
After checking out the source code, type
$ make setup all serve
This will automatically run the NPM and Bower install scripts, placing
the correct libraries into the target tree, build the actual application
from the original source material, and start a server.
## Requirements
The build tool relies upon GNU Make, node-js, and the Ruby HAML
application. It also uses the NoWeb Literate Programming documentation
tools, and building the documentation from source requires Xelatex be
installed as well.
The command 'make serve' probably only works under a fairly modern
Linux, as it's dependent upon the kernel's inotify facility.
## Branches
There are two major development branches for The Backbone Store.
Branch 'master' uses HTML, CSS, and Javascript.
Branch 'modern' uses HAML, Stylus, and Coffee.
## Changelog
### Changes from 2.0
Version 3.0 has the following notable changes:
* Replace __super__ with prototype
* Replace Backbone-generated internal IDs with supplied IDs
* Updates the use of Deferred
* Updates to the current Underscore Template mechanism
### Changes from 1.0
Version 2.0 has the following notable changes:
* Use of jQuery animations
* Better Styling
* Proper event management. Version 1.0 was just doin' it WRONG.
## Copyright
Store.js is entirely my own work, and is Copyright (c) 2010 Elf
M. Sternberg. Included libraries are covered by their respective
copyright holders, and are used with permission of the licenses
included. Store.js is intended for educational purposes only, rather
than to be working code, and is hereby licensed under the Creative
Commons Attribution Non-Commercial Share Alike (by-nc-sa) licence.
The images contained herein are derivative works of photographs
licensed under Creative Commons licences for non-commercial purposes.
## Contribution
Please look in backbonestore.nw for the base code. Backbonestore.nw
is produced using the Noweb Literate Programming toolkit by Norman
Ramsey (http://www.cs.tufts.edu/~nr/noweb/).

File diff suppressed because it is too large Load Diff

View File

@ -1,851 +0,0 @@
% -*- Mode: noweb; noweb-code-mode: javascript-mode ; noweb-doc-mode: latex-mode -*-
\documentclass{article}
\usepackage{noweb}
\usepackage[T1]{fontenc}
\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}
\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},
\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
\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
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
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}
This is version 2.0 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.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.
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,
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>>=
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>
The Backbone Store
</title>
<link rel="stylesheet" href="jsonstore.css" type="text/css">
<<product list template>>
<<product template>>
<<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>
<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>
@
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.
Our first view is going to be the product list view, named, well,
guess. Or just look down a few lines.
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.
<<product list view>>=
var ProductListView = _BaseView.extend({
id: 'productlistview',
indexTemplate: $("#store_index_template").template(), //$
render: function() {
self.el.html(_.template(this.template, {'products': this.model.toJSON()}))
return this;
}
});
@
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.
And here is the HTML:
<<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>
@
%$
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;
},
@
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.
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.
<<product view>>=
events: {
"keypress .uqf" : "updateOnEnter",
"click .uq" : "update",
},
@
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}.
%'
<<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);
}
},
@
%$
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
\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{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 .
<<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}

21
bin/autoreload Executable file
View File

@ -0,0 +1,21 @@
#!/bin/env coffee
fs = require 'fs'
Inotify = require('inotify').Inotify
spawn = require('child_process').spawn
spew = (data) -> console.log data.toString 'utf8'
server = spawn './node_modules/http-server/bin/http-server', ['./htdocs/']
server.stdout.on 'data', spew
monitor = new Inotify()
reBuild = ->
maker = spawn 'make', ['store']
maker.stdout.on 'data', spew
monitor.addWatch
path: "./src/backbonestore.nw"
watch_for: Inotify.IN_CLOSE_WRITE
callback: reBuild

BIN
docs/backbonestore.pdf Normal file

Binary file not shown.

View File

@ -1,6 +1,6 @@
[
{
"id": "unless",
"id": "unless-you-have-been-drinking",
"title": "Unless You Have Been Drinking",
"artist": "Adventures in Odyssey",
"image": "images/AdventuresInOdyssey_t.jpg",
@ -9,7 +9,7 @@
"url": "http://www.amazon.com/Door-Religious-Knives/dp/B001FGW0UQ/?tag=quirkey-20"
},
{
"id": "utmost",
"id": "leave-to-do-my-utmost",
"title": "Leave To Do My Utmost",
"artist": "American Attorneys",
"image": "images/AmericanAttorneys_t.jpg",
@ -18,7 +18,7 @@
"url": "http://www.amazon.com/gp/product/B002GNOMJE?ie=UTF8&tag=quirkeycom-20&linkCode=as2&camp=1789&creative=390957&creativeASIN=B002GNOMJE"
},
{
"id": "encircled",
"id": "the-dead-sleep-encircled-by-the-living",
"title": "The Dead Sleep Encircled by The Living",
"artist": "British Civil Light Transport",
"image": "images/BritishCivilLightTransport_t.jpg",
@ -27,7 +27,7 @@
"url": "http://www.amazon.com/Bitte-Orca-Dirty-Projectors/dp/B0026T4RTI/ref=pd_sim_m_12?tag=quirkey-20"
},
{
"id": "assimilation",
"id": "periods-of-mental-assimilation",
"title": "Periods of Mental Assimilation",
"artist": "Grigory Szondy",
"image": "images/PeriodsofMentalAssimilation_t.jpg",
@ -36,7 +36,7 @@
"url": "http://www.amazon.com/Pains-Being-Pure-Heart/dp/B001LGXIDS/ref=pd_sim_m_44?tag=quirkey-20"
},
{
"id": "bankruptcy",
"id": "keenly-developed-moral-bankruptcy",
"title": "Keenly Developed Moral Bankruptcy",
"artist": "Stealth Monkey Virus",
"image": "images/StealthMonkeyVirus_t.jpg",
@ -45,7 +45,7 @@
"url": "http://www.amazon.com/Pains-Being-Pure-Heart/dp/B001LGXIDS/ref=pd_sim_m_44?tag=quirkey-20"
},
{
"id": "sparrow",
"id": "my-mistresss-sparrow-is-dead",
"title": "My Mistress's Sparrow is Dead",
"artist": "Sums of Mongolia",
"image": "images/SumsofMagnolia_t.jpg",

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
htdocs/images/Pulaski.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
htdocs/images/Pulaski_t.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
htdocs/images/SumsofMagnolia.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

71
htdocs/index.html Normal file
View File

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>The Backbone Store</title>
<link charset="utf-8" href="jsonstore.css" rel="stylesheet" type="text/css">
<script id="store_index_template" type="text/x-underscore-tmplate">
<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.id %>">
<img alt="<%= p.title %>" src="<%= p.image %>">
</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>
<script id="store_item_template" type="text/x-underscore-template">
<div class="item-detail">
<div class="item-image">
<img alt="<%= title %>" src="<%= large_image %>">
</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"></div>
<form action="#/cart" method="post">
<p>
<label>Quantity:</label>
<input class="uqf" name="quantity" size="2" type="text" value="1">
</p>
<p>
<input class="uq" type="submit" value="Add to Cart">
</p>
</form>
<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>
<script id="store_cart_template" type="text/x-underscore-template">
<p>Items: <%= count %> ($<%= cost %>)</p>
</script>
</head>
</head>
<body>
<div id="container">
<div id="header">
<h1>
The Backbone Store
</h1>
<div class="cart-info"></div>
</div>
<div id="main"></div>
</div>
<script src="lib/jquery.js" type="text/javascript"></script>
<script src="lib/underscore.js" type="text/javascript"></script>
<script src="lib/backbone.js" type="text/javascript"></script>
<script src="store.js" type="text/javascript"></script>
</body>
</html>

91
htdocs/jsonstore.css Normal file
View File

@ -0,0 +1,91 @@
body {
font-family: "Lucida Grande", Lucida, Helvetica, Arial, sans-serif;
background: #fff;
color: #333;
margin: 0px;
padding: 0px;
}
#main {
position: relative;
}
#header {
background: #999;
background: -webkit-gradient(linear, left top, left bottom, from(#adadad), to(#7a7a7a));
background: -moz-linear-gradient(top, #adadad, #7a7a7a);
margin: 0px;
padding: 20px;
border-bottom: 1px solid #ccc;
}
#header h1 {
font-family: Inconsolata, Monaco, Courier, mono;
color: #fff;
margin: 0px;
}
#header .cart-info {
position: absolute;
top: 0px;
right: 0px;
text-align: right;
padding: 10px;
background: #555;
background: -webkit-gradient(linear, left top, left bottom, from(#777), to(#444));
background: -moz-linear-gradient(top, #777, #444);
color: #fff;
font-size: 12px;
font-weight: bold;
}
img {
border: 0;
}
.productitemview {
position: absolute;
top: 0;
left: 0;
}
#productlistview {
position: absolute;
top: 0;
left: 0;
}
#productlistview ul {
list-style: none;
}
.item {
float: left;
width: 250px;
margin-right: 10px;
margin-bottom: 10px;
padding: 5px;
-moz-border-radius-topleft: 5px;
-moz-border-radius-topright: 5px;
-moz-border-radius-bottomleft: 5px;
-moz-border-radius-bottomright: 5px;
-webkit-border-bottom-right-radius: 5px;
-webkit-border-top-left-radius: 5px;
-webkit-border-top-right-radius: 5px;
-webkit-border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom-left-radius: 5px;
border: 1px solid #ccc;
text-align: center;
font-size: 12px;
}
.item-title {
font-weight: bold;
}
.item-artist {
font-weight: bold;
font-size: 14px;
}
.item-detail {
margin: 10px 0 0 10px;
}
.item-detail .item-image {
float: left;
margin-right: 10px;
}
.item-detail .item-info {
padding: 100px 10px 0px 10px;
}

337
htdocs/store.js Normal file
View File

@ -0,0 +1,337 @@
// Generated by CoffeeScript 1.10.0
(function() {
var BackboneStore, CartWidget, Item, ItemCollection, Product, ProductCollection, ProductListView, ProductView, _BaseView,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
Product = (function(superClass) {
extend(Product, superClass);
function Product() {
return Product.__super__.constructor.apply(this, arguments);
}
return Product;
})(Backbone.Model);
Item = (function(superClass) {
extend(Item, superClass);
function Item() {
return Item.__super__.constructor.apply(this, arguments);
}
Item.prototype.update = function(amount) {
if (amount === this.get('quantity')) {
return;
}
this.set({
quantity: amount
}, {
silent: true
});
return this.collection.trigger('update', this);
};
Item.prototype.price = function() {
return this.get('product').get('price') * this.get('quantity');
};
return Item;
})(Backbone.Model);
ProductCollection = (function(superClass) {
extend(ProductCollection, superClass);
function ProductCollection() {
return ProductCollection.__super__.constructor.apply(this, arguments);
}
ProductCollection.prototype.model = Product;
ProductCollection.prototype.initialize = function(models, options) {
return this.url = options.url;
};
ProductCollection.prototype.comparator = function(item) {
return item.get('title');
};
return ProductCollection;
})(Backbone.Collection);
ItemCollection = (function(superClass) {
extend(ItemCollection, superClass);
function ItemCollection() {
return ItemCollection.__super__.constructor.apply(this, arguments);
}
ItemCollection.prototype.model = Item;
ItemCollection.prototype.updateItemForProduct = function(product, amount) {
var i, pid;
amount = amount != null ? amount : 0;
pid = product.get('id');
i = this.detect(function(obj) {
return obj.get('product').get('id') === pid;
});
if (i) {
i.update(amount);
return i;
}
return this.add({
product: product,
quantity: amount
});
};
ItemCollection.prototype.getTotalCount = function() {
var addup;
addup = function(memo, obj) {
return memo + obj.get('quantity');
};
return this.reduce(addup, 0);
};
ItemCollection.prototype.getTotalCost = function() {
var addup;
addup = function(memo, obj) {
return memo + obj.price();
};
return this.reduce(addup, 0);
};
return ItemCollection;
})(Backbone.Collection);
_BaseView = (function(superClass) {
extend(_BaseView, superClass);
function _BaseView() {
return _BaseView.__super__.constructor.apply(this, arguments);
}
_BaseView.prototype.parent = $('#main');
_BaseView.prototype.className = 'viewport';
_BaseView.prototype.initialize = function() {
this.el = $(this.el);
this.el.hide();
this.parent.append(this.el);
return this;
};
_BaseView.prototype.hide = function() {
var dfd;
dfd = $.Deferred();
if (!this.el.is(':visible')) {
return dfd.resolve();
}
this.el.fadeOut('fast', function() {
return dfd.resolve();
});
return dfd.promise();
};
_BaseView.prototype.show = function() {
var dfd;
dfd = $.Deferred();
if (this.el.is(':visible')) {
return dfd.resolve();
}
this.el.fadeIn('fast', function() {
return dfd.resolve();
});
return dfd.promise();
};
return _BaseView;
})(Backbone.View);
ProductListView = (function(superClass) {
extend(ProductListView, superClass);
function ProductListView() {
return ProductListView.__super__.constructor.apply(this, arguments);
}
ProductListView.prototype.id = 'productlistview';
ProductListView.prototype.template = $("#store_index_template").html();
ProductListView.prototype.initialize = function(options) {
_BaseView.prototype.initialize.apply(this, [options]);
return this.collection.bind('reset', this.render.bind(this));
};
ProductListView.prototype.render = function() {
this.el.html(_.template(this.template)({
'products': this.collection.toJSON()
}));
return this;
};
return ProductListView;
})(_BaseView);
ProductView = (function(superClass) {
extend(ProductView, superClass);
function ProductView() {
return ProductView.__super__.constructor.apply(this, arguments);
}
ProductView.prototype.className = 'productitemview';
ProductView.prototype.template = $("#store_item_template").html();
ProductView.prototype.initialize = function(options) {
_BaseView.prototype.initialize.apply(this, [options]);
return this.itemcollection = options.itemcollection;
};
ProductView.prototype.events = {
"keypress .uqf": "updateOnEnter",
"click .uq": "update"
};
ProductView.prototype.update = function(e) {
e.preventDefault();
return this.itemcollection.updateItemForProduct(this.model, parseInt(this.$('.uqf').val()));
};
ProductView.prototype.updateOnEnter = function(e) {
if (e.keyCode === 13) {
return this.update(e);
}
};
ProductView.prototype.render = function() {
this.el.html(_.template(this.template)(this.model.toJSON()));
return this;
};
return ProductView;
})(_BaseView);
CartWidget = (function(superClass) {
extend(CartWidget, superClass);
function CartWidget() {
return CartWidget.__super__.constructor.apply(this, arguments);
}
CartWidget.prototype.el = $('.cart-info');
CartWidget.prototype.template = $('#store_cart_template').html();
CartWidget.prototype.initialize = function() {
return this.collection.bind('update', this.render.bind(this));
};
CartWidget.prototype.render = function() {
var tel;
tel = this.$el.html(_.template(this.template)({
'count': this.collection.getTotalCount(),
'cost': this.collection.getTotalCost()
}));
tel.animate({
paddingTop: '30px'
}).animate({
paddingTop: '10px'
});
return this;
};
return CartWidget;
})(Backbone.View);
BackboneStore = (function(superClass) {
extend(BackboneStore, superClass);
function BackboneStore() {
return BackboneStore.__super__.constructor.apply(this, arguments);
}
BackboneStore.prototype.views = {};
BackboneStore.prototype.products = null;
BackboneStore.prototype.cart = null;
BackboneStore.prototype.routes = {
"": "index",
"item/:id": "product"
};
BackboneStore.prototype.initialize = function(data) {
this.cart = new ItemCollection();
new CartWidget({
collection: this.cart
});
this.products = new ProductCollection([], {
url: 'data/items.json'
});
this.views = {
'_index': new ProductListView({
collection: this.products
})
};
$.when(this.products.fetch({
reset: true
})).then(function() {
return window.location.hash = '';
});
return this;
};
BackboneStore.prototype.hideAllViews = function() {
return _.select(_.map(this.views, function(v) {
return v.hide();
}), function(t) {
return t !== null;
});
};
BackboneStore.prototype.index = function() {
var view;
view = this.views['_index'];
return $.when.apply($, this.hideAllViews()).then(function() {
return view.show();
});
};
BackboneStore.prototype.product = function(id) {
var base, name, product, view;
product = this.products.detect(function(p) {
return p.get('id') === id;
});
view = ((base = this.views)[name = 'item.' + id] || (base[name] = new ProductView({
model: product,
itemcollection: this.cart
}).render()));
return $.when(this.hideAllViews()).then(function() {
return view.show();
});
};
return BackboneStore;
})(Backbone.Router);
$(document).ready(function() {
new BackboneStore();
return Backbone.history.start();
});
}).call(this);

View File

@ -1,68 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>The Backbone Store</title>
<link charset="utf-8" href="jsonstore.css" rel="stylesheet" type="text/css" />
<script id="store_index_template" type="text/x-underscore-tmplate">
<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.id %>">
<img alt="<%= p.title %>" src="<%= p.image %>" />
</a>
</div>
</li>
<div class="item-artist"><%= p.artist %></div>
<div class="item-title"><%= p.title %></div>
<div class="item-price">$<%= p.price %></div>
<% } %>
</ul>
</script>
<script id="store_item_template" type="text/x-underscore-template">
<div class="item-detail"></div>
<div class="item-image">
<img alt="<%= title %>" src="<%= large_image %>" />
</div>
<div class="item-info"></div>
<div class="item-artist"><%= artist %></div>
<div class="item-title"><%= title %></div>
<div class="item-price">$<%= price %></div>
<div class="item-form"></div>
<form action="#/cart" method="post">
<p>
<label>Quantity:</label>
<input class="uqf" name="quantity" size="2" type="text" value="1" />
</p>
<p>
<input class="uq" type="submit" value="Add to Cart" />
</p>
</form>
<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>
</script>
<script id="store_cart_template" type="text/x-underscore-template">
<p>Items: <%= count %> ($<%= cost %>)</p>
</script>
</head>
<body>
<div id="container">
<div id="header">
<h1>
The Backbone Store
</h1>
<div class="cart-info"></div>
</div>
<div id="main"></div>
</div>
<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>

18
jquery-1.6.2.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,58 +0,0 @@
/******************************************************
* json store *
\*****************************************************/
body {
font-family: "Lucida Grande", Lucida, Helvetica, Arial, sans-serif;
background: #FFF;
color: #333;
margin: 0px;
padding: 0px;
}
#header {
background: #C97E41;
margin: 0px;
padding: 20px;
}
#header h1 {
font-family: Inconsolata, Monaco, Courier, mono;
color: #FFF;
margin: 0px;
}
#header .cart-info {
position: absolute;
top: 0px;
right: 0px;
text-align: right;
padding: 10px;
background: #714625;
color: #FFF;
font-size: 12px;
font-weight: bold;
}
img {
border: 0;
}
.item {
float:left;
width: 250px;
margin-right: 3px;
padding: 2px;
border: 1px solid #ccc;
text-align:center;
font-size: 12px;
}
.item-title {
font-weight: bold;
}
.item-artist {
font-weight: bold;
font-size: 14px;
}
.item-detail .item-image {
float:left;
}
.item-detail .item-info {
padding: 100px 10px 0px 10px;
}

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "the-backbone-store",
"version": "3.0.1",
"description": "A comprehensive (one hopes) tutorial on a simple development platform for Backbone.",
"main": "htdocs/index.html",
"dependencies": {
"http-server": "^0.9.0",
"coffee-script": "^1.10.0",
"stylus": "^0.54.0"
},
"devDependencies": {
"inotify": "^1.4.0",
"bower": "^1.7.0"
},
"scripts": {
"test": "make serve"
},
"repository": {
"type": "git",
"url": "git+https://github.com/elfsternberg/The-Backbone-Store.git"
},
"keywords": [
"backbone",
"javascript",
"makefiles",
"node",
"coffee",
"haml",
"stylus",
"tutorial"
],
"author": "Kenneth M. \"Elf\" Sternberg <elf.sternberg@gmail.com>",
"license": "BSD-2-Clause",
"bugs": {
"url": "https://github.com/elfsternberg/The-Backbone-Store/issues"
},
"homepage": "https://github.com/elfsternberg/The-Backbone-Store#readme"
}

877
src/backbonestore.nw Normal file
View File

@ -0,0 +1,877 @@
% -*- Mode: poly-noweb+coffee -*-
\documentclass{article}
\usepackage{noweb}
\usepackage[T1]{fontenc}
\usepackage{hyperref}
\usepackage{fontspec, xunicode, xltxtra}
\setromanfont{Georgia}
\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}
This is version 3.0 of \textbf{The Backbone Store}, a brief tutorial on
using [[backbone.js]]. The version you are currently reading has been
tested with the latest versions of the supporting software as of April,
2016.
\nwanchorto{http://documentcloud.github.com/backbone/}{Backbone.js} is a
popular Model-View-Controller (MVC) library that provides a framework
for creating data-rich, single-page web applications. It provides (1) a
two-layer scheme for separating data from presentation, (2) a means of
automatically synchronizing data with a server in a RESTful manner, and
(3) a mechanism for making some views bookmarkable and navigable.
Backbone is dependent upon \nwanchorto{http://jquery.com}{jQuery} and
\nwanchorto{http://underscorejs.org}{Underscore}. Both of those
dependencies are encoded into the build process automatically.
The version of this tutorial you are currently reading uses
Coffeescript, Stylus, and Ruby's HAML. The purpose of this tutorial is
to show how to use Backbone in a modern, constrained programming
environment.
\nwanchorto{http://jashkenas.github.com/coffee-script/}{CoffeeScript} is
a lovely little languange that compiles into Javascript. It provides a
class-based architecture (that is compatible with Backbone), has an
elegant structure for defining functions and methods, and strips out as
much extraneous punctuation as possible. Some people find the
whitespace-as-semantics a'la Python offputting, but most disciplined
developers already indent appropriately anyway.
\nwanchorto{http://haml-lang.com/}{HAML} is a languange that compiles
into HTML. Like CoffeeScript, it uses whitespace for semantics:
indentation levels correspond to HTML containerizations. It allows you
to use rich scripting while preventing heirarchy misplacement mistakes.
Its shorthand also makes writing HTML much faster.
\nwanchorto{https://github.com/LearnBoost/stylus/}{Stylus} is languange
that compiles into CSS. Like CoffeeScript and HAML, it uses whitespace
for semantics. It also provides mixins and functions that allow you to
define visual styles such as borders and gradients, and mix them into
specific selectors in the CSS rather than having to write them into the
HTML.
There are a number of other 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},
\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
\nwanchorto{http://joshbohde.com/2010/11/25/backbonejs-and-django/}{Backbone
and Django}. However, a couple of years 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 $\langle\langle$this$\rangle\rangle$, 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 3.0 of \textit{The Backbone Store}. It includes several
significant updates, including the use of both NPM and Bower to build
the final application.
\subsection{The Store: What We're Going to Build}
To demonstrate the basics of Backbone, I'm going to create a simple
one-page application, a store for record albums, with two unique views:
a list of all products and a product detail view. I will also put a
shopping cart widget on the page that shows the user how many products
he or she has dropped into the cart. I'll use some simple animations to
transition between the catalog and the product detail pages.
\subsection{Models, Collections, and Controllers}
Backbone's data layer provides two classes, [[Collection]] and
[[Model]].
Every web application has data, often tabular data. Full-stack web
developers are (or ought to be) familiar with the \textit{triples} of
addressing objects on the web: Table URL → Row → Field, or Page URL →
HTML Node → Content. The [[Collection]] object represents just that: a
collection of similar items. The [[Model]] represents exactly one of
those items.
To use the Model, you inherit from it using Backbone's own [[.extend()]]
class method, adding or replacing methods in the child object as
needed. For our purposes, we have two models: [[Product]] represents
something we wish to sell, and [[Item]] represents something currently
in the customer's shopping cart.
Shopping carts are a little odd; the convention is that [[Item]] is not a
single instance of the product, but instead has a reference to the
product, and a count of how many the buyer wants. To that end, I am
adding two methods that extend Item: [[.update()]], which changes the
current quantity, and [[.price()]], which calculates the product's price
times the quantity:
<<models>>=
class Product extends Backbone.Model
class Item extends Backbone.Model
update: (amount) ->
return if amount == @get 'quantity'
@set {quantity: amount}, {silent: true}
@collection.trigger 'update', @
price: () ->
@get('product').get('price') * @get('quantity')
@
The methods [[.get(item)]] and [[.set(item, value)]] are at the heart of
Backbone.Model. They're how you set individual attributes on the
object being manipulated. Notice how I can 'get' the product, which is
a Backbone.Model, and then 'get' its price.
Backbone supplies its own event management toolkit. Changing a model
triggers various events, none of which matter here in this context so I
silence the event, but then I tell the Item's Backbone.Collection that
the Model has changed. Events are the primary way in which Backbone
objects interact, so understanding them is key to using Backbone
correctly.
Collections, like Models, are just objects you can (and often must)
extend to support your application's needs. Just as a Model has
\texttt{.get()} and \texttt{.set()}, a Collection has [[.add(item)]] and
[[.remove(id)]] as methods. Collections have a lot more than that.
Both Models and Collections also have [[.fetch()]] and [[.save()]]. If
either has a URL, these methods allow the collection to represent data
on the server, and to save that data back to the server. The default
method is a simple JSON object representing either a Model's attributes,
or a JSON list of the Collection's models' attributes.
The [[Product.Collection]] will be loading it's list of albums via these
methods to (in our case) static JSON back-end.
<<product collection>>=
class ProductCollection extends Backbone.Collection
model: Product
initialize: (models, options) ->
@url = options.url
comparator: (item) ->
item.get 'title'
@
The [[.model]] attribute tells the [[ProductCollection]] that if
[[.add()]] or [[.fetch()]] are called and the contents are plain JSON,
a new [[Product]] Model should be initialized with the JSON data and
that will be used as a new object for the Collection.
The [[.comparator()]] method specifies the per-model value by which the
Collection should be sorted. Sorting happens automatically whenever the
Collection receives an event indicating its contents have been altered.
The [[ItemCollection]] doesn't have a URL, but we do have several helper
methods to add. We don't want to add Items; instead, we want to add
products as needed, then update the count as requested. If the product
is already in our system, we don't want to create duplicates.
<<cart collection>>=
class ItemCollection extends Backbone.Collection
model: Item
@
First, we ensure that if we don't receive an amount, we at least provide
a valid \textit{numerical} value to our code. The [[.detect()]] method
lets us find an object in our Collection using a function to compare
them; it returns the first object that matches.
If we find the object, we update it and return. If we don't, we create
a new one, exploiting the fact that, since we specified the Collection's
Model above, it will automatically be created as a Model in the
Collection at the end of this call. In either case, we return the new
Item to be handled further by the calling code.
<<cart collection>>=
updateItemForProduct: (product, amount) ->
amount = if amount? then amount else 0
pid = product.get 'id'
i = this.detect (obj) -> (obj.get('product').get('id') == pid)
if i
i.update(amount)
return i
@add {product: product, quantity: amount}
@
And finally, two methods to add up how many objects are in your cart,
and the total price. The first line creates a function to get the
number for a single object and add it to a memo. The second line uses
the [[.reduce()]] method, which goes through each object in the
collection and runs the function, passing the results of each run to the
next as the memo.
<<cart collection>>=
getTotalCount: () ->
addup = (memo, obj) -> memo + obj.get 'quantity'
@reduce addup, 0
getTotalCost: () ->
addup = (memo, obj) -> memo + obj.price()
@reduce addup, 0
@
\subsection {Views}
Backbone Views are simple policy objects. They have a root DOM
element, the contents of which they manipulate and to which they
listen for events, and a model or collection they represent within
that element. Views are not rigid; it's just Javascript and the DOM,
and you can hook external events as needed.
More importantly, if you pass a model or collection to a View, that View
becomes sensitive to events \textit{within its model or collection}, and
can respond to changes automatically. This way, if you have a rich data
ecosystem, when changes to one data item results in a cascade of changes
throughout your datasets, the views will receive ``change'' events and
can update themselves accordingly.
In a way, a View can be thought of as two separate but important
sub-programs, each based on events. The first listens to events from
the DOM, and forwards data-changing events to associated models or
collections. The second listens to events from data objects and
re-draws the View's contents when the data changes. Keeping these
separate in your mind will help you design Backbone applications
successfully.
I will show how this works with the shopping cart widget.
To achieve the [[fadeIn/fadeOut]] animations and enforce consistency,
I'm going to do some basic object-oriented programming. I'm 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>>=
class _BaseView extends Backbone.View
parent: $('#main')
className: 'viewport'
@
The above says that I am creating a class called \texttt{BaseView} and
defining two fields. The first, 'parent', will be used by all child
views to identify into which DOM object the View root element 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. In the
HTML, you will see the [[DIV#main]] object where most of the work will
be rendered.
As an alternative, the viewport object may already exist, in which case
you just find it with a selector, and the view attaches itself to that
DOM object from then on.
<<base view>>=
initialize: () ->
@el = $(@el)
@el.hide()
@parent.append(@el)
@
@
%$
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.
Note that in Coffeescript, the [[=>]] operator completely replaces the
[[.bind()]] function provided by modern Javascript.
<<base view>>=
hide: () ->
dfd = $.Deferred()
if not @el.is(':visible')
return dfd.resolve()
@el.fadeOut('fast', () -> dfd.resolve())
dfd.promise()
show: () ->
dfd = $.Deferred()
if @el.is(':visible')
return dfd.resolve()
@el.fadeIn('fast', () -> dfd.resolve())
dfd.promise()
@
\textbf{Deferred} is a feature of jQuery called ``promises''. 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 HAML we're going to use for
our one-page application. The code below compiles beautifully into the
same HTML seen in the original Backbone Store.
<<index.haml>>=
!!! 5
%html{:xmlns => "http://www.w3.org/1999/xhtml"}
%head
%title The Backbone Store
%link{:charset => "utf-8", :href => "jsonstore.css", :rel => "stylesheet", :type => "text/css"}/
<<product list template>>
<<product detail template>>
<<cart template>>
</head>
%body
#container
#header
%h1
The Backbone Store
.cart-info
#main
%script{:src => "lib/jquery.js", :type => "text/javascript"}
%script{:src => "lib/underscore.js", :type => "text/javascript"}
%script{:src => "lib/backbone.js", :type => "text/javascript"}
%script{:src => "store.js", :type => "text/javascript"}
@
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.
Our first view is going to be the product list view, named, well,
guess. Or just look down a few lines.
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.
That call to [[.prototype]] is a Javascript idiom for calling a method
on the parent object. It is, as far as anyone knows, the only way to
invoke a superclass method if it has been redefined in a subclass. It
is rather ugly, but useful.
<<product list view>>=
class ProductListView extends _BaseView
id: 'productlistview'
template: $("#store_index_template").html()
initialize: (options) ->
_BaseView.prototype.initialize.apply @, [options]
@collection.bind 'reset', @render.bind @
render: () ->
@el.html(_.template(@template)({'products': @collection.toJSON()}))
@
@
%$
That \texttt{\_.template()} method is provided by undescore.js, and is
a full-featured, javascript-based 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.
There are many different ways of providing templates to Backbone. The
most common, especially for small templates, is to just include it as an
inline string inside the View. The \textit{least} common, I'm afraid,
is the one I'm doing here: using the $<$script$>$ tag with an
unusual mime type to include it with the rest of the HTML. I like this
method because it means all of my HTML is in one place.
For much larger programs, those that use features such as
\nwanchorto{http://requirejs.org/}{Require.js}, a common technique is to
keep the HTML template fragment in its own file and to import it using
Require's ``text'' plugin.
Here is the HAML for our home page's template:
<<product list template>>=
%script#store_index_template(type="text/x-underscore-tmplate")
%h1 Product Catalog
%ul
<% for(i=0,l=products.length;i<l;++i) { p = products[i]; %>
%li.item
.item-image
%a{:href => "#item/<%= p.id %>"}
%img{:src => "<%= p.image %>", :alt => "<%= p.title %>"}/
.item-artist <%= p.artist %>
.item-title <%= p.title %>
.item-price $<%= p.price %>
<% } %>
@
%$
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 means by which one calls the
method of a parent class from a child class via Backbone's class
heirarchy, and keeping track of the ItemCollection object, so we can add
and change items as needed.
<<product detail view>>=
class ProductView extends _BaseView
className: 'productitemview'
template: $("#store_item_template").html()
initialize: (options) ->
_BaseView.prototype.initialize.apply @, [options]
@itemcollection = options.itemcollection
@
%$
There are certain 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.
<<product detail view>>=
events:
"keypress .uqf" : "updateOnEnter"
"click .uq" : "update"
@
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}.
Look closely at the [[update()]] method. The reference [[@\$]] is a
special Backbone object that limits selectors to objects inside the
element of the view. Without it, jQuery would have found the first
input field of class 'uqf' in the DOM, not the one for this specific
view. [[@\$('.uqf')]] is shorthand for [[$('uqf', @el)]], and helps
clarify what it is you're looking for.
<<product detail view>>=
update: (e) ->
e.preventDefault()
@itemcollection.updateItemForProduct @model, parseInt(@$('.uqf').val())
updateOnEnter: (e) ->
@update(e) if e.keyCode == 13
@
The render is straightforward:
<<product detail view>>=
render: () ->
@el.html(_.template(@template)(@model.toJSON()));
@
@
The product detail template is fairly straightforward. There is no
[[underscore]] magic because there are no loops.
<<product detail template>>=
%script#store_item_template(type= "text/x-underscore-template")
.item-detail
.item-image
%img(src="<%= large_image %>" alt="<%= title %>")/
.item-info
.item-artist <%= artist %>
.item-title <%= title %>
.item-price $<%= price %>
.item-form
%form(action="#/cart" method="post")
%p
%label Quantity:
%input(type="text" size="2" name="quantity" value="1" class="uqf")/
%p
%input(type="submit" value="Add to Cart" class="uq")/
.item-link
%a(href="<%= url %>") Buy this item on Amazon
.back-link
%a(href="#") &laquo; Back to Items
@
So, let's talk about that shopping cart thing. We've been making the
point that when it changes, when you call [[item.update]] within the
product detail view, any corresponding subscribing views sholud
automatically update.
<<cart widget>>=
class CartWidget extends Backbone.View
el: $('.cart-info')
template: $('#store_cart_template').html()
initialize: () ->
@collection.bind 'update', @render.bind @
@
%$
And there is the major magic. CartWidget will be initialized with the
ItemCollection; when there is any change in the collection, the widget
will receive the 'change' event, which will automatically trigger the
call to the widget's [[render()]] method.
The render method will refill that widget's HTML with a re-rendered
template with the new count and cost, and then wiggle it a little to
show that it did changed:
<<cart widget>>=
render: () ->
tel = @$el.html _.template(@template)({
'count': @collection.getTotalCount()
'cost': @collection.getTotalCost()
})
tel.animate({paddingTop: '30px'}).animate({paddingTop: '10px'})
@
@
And the HTML for the template is dead simple:
<<cart template>>=
%script#store_cart_template(type="text/x-underscore-template")
%p Items: <%= count %> ($<%= cost %>)
@
%$
Lastly, there is the [[Router]]. In Backbone, the Router is a
specialized View for invoking other views. It listens for one
specific event: when the [[window.location.hash]] object, the part of
the URL after the hash symbol, changes. When the hash changes, the
Router invokes an event handler. The Router, since its purpose is to
control the major components of the one-page display, is also a good
place to keep all the major components of the sytem. We'll keep track
of the [[Views]], the [[ProductCollection]], and the
[[ItemCollection]].
<<router>>=
class BackboneStore extends Backbone.Router
views: {}
products: null
cart: null
@
There are two events we care about: view the list, and view a detail.
They are routed like this:
<<router>>=
routes:
"": "index"
"item/:id": "product"
@
Like most Backbone objects, the Router has an initialization feature.
I create a new, empty shopping cart and corresponding cart widget,
which doesn't render because it's empty. I then create a new
[[ProductCollection]] and and corresponding [[ProductListView]].
These are all processes that happen immediately.
What does not happen immediately is the [[fetch()]] of data from the
back-end server. For that, I use the jQuery deferred again, because
[[fetch()]] ultimately returns the results of [[sync()]], which
returns the result of an [[ajax()]] call, which is a deferred.
<<router>>=
initialize: (data) ->
@cart = new ItemCollection()
new CartWidget
collection: @cart
@products = new ProductCollection [],
url: 'data/items.json'
@views =
'_index': new ProductListView
collection: @products
$.when(@products.fetch({reset: true}))
.then(() -> window.location.hash = '')
@
@
%$
There are two things to route \textit{to}, but we must also route
\textit{from}. Remember that our two major views, the product list
and the product detail, inherited from [[\_BaseView]], which has the
[[hide()]] and [[show()]] methods. We want to hide all the views,
then show the one invoked. First, let's hide every view we know
about. [[hide()]] returns either a deferred (if the object is being
hidden) or null. The [[_.select()]] call at the end means that this
method returns only an array of deferreds.
<<router>>=
hideAllViews: () ->
_.select(_.map(@views, (v) -> return v.hide()),
(t) -> t != null)
@
Showing the product list view is basically hiding everything, then
showing the index. The function [[$$.when]] takes arguments of what to
wait for; to make it take an array of arguments, you use the
[[.apply()]] method.
<<router>>=
index: () ->
view = @views['_index']
$.when.apply($, @hideAllViews()).then(() -> view.show())
@
On the other hand, showing the product detail page is a bit trickier.
In order to avoid re-rendering all the time, I am going to create a
view for every product in which the user shows interest, and keep it
around, showing it a second time if the user wants to see it a second
time.
Not that we pass it the [[ItemCollection]] instance. It uses this to
create a new item, which (if you recall from our discussion of
[[getOrCreateItemForProduct()]]) is automagically put into the
collection as needed. Which means all we need to do is update this
item and the item collection \textit{changes}, which in turn causes
the [[CartWidget]] to update automagically as well.
<<router>>=
product: (id) ->
product = @products.detect (p) -> p.get('id') == (id)
view = (@views['item.' + id] ||= new ProductView(
model: product,
itemcollection: @cart
).render())
$.when(@hideAllViews()).then(
() -> view.show())
@
%$
Finally, we need to start the program
<<initialization>>=
$(document).ready () ->
new BackboneStore();
Backbone.history.start();
@
%$
\section{The Program}
Here's the entirety of the program. Coffeescript provides its own
namespace wrapper:
<<store.coffee>>=
<<models>>
<<product collection>>
<<cart collection>>
<<base view>>
<<product list view>>
<<product detail view>>
<<cart widget>>
<<router>>
<<initialization>>
@
\section{A Little Stylus}
Stylus is a beautiful little language that compiles down to CSS. The
original version of The Backbone Store used the same CSS provided from
the original Sammy tutorial, but I wanted to show you this one extra
tool because it's an essential part of my kit.
If you want rounded borders, you know that writing all that code, for
older browsers as well as modern ones, and providing it to all the
different objects you want styled that way, can be time consuming.
Stylus allows you to define a function that can be called from within
any style, thus allowing you to define the style here, and attach a
set style to a semantic value in your HTML:
<<jsonstore.styl>>=
rounded(radius)
-moz-border-radius-topleft: radius
-moz-border-radius-topright: radius
-moz-border-radius-bottomleft: radius
-moz-border-radius-bottomright: radius
-webkit-border-bottom-right-radius: radius
-webkit-border-top-left-radius: radius
-webkit-border-top-right-radius: radius
-webkit-border-bottom-left-radius: radius
border-bottom-right-radius: radius
border-top-left-radius: radius
border-top-right-radius: radius
border-bottom-left-radius: radius
background_gradient(base)
background: base
background: -webkit-gradient(linear, left top, left bottom, from(lighten(base, 20%)), to(darken(base, 20%)))
background: -moz-linear-gradient(top, lighten(base, 20%), darken(base, 20%))
@
And if you look down below you'll see the [[rounded()]] function
called for the list items, which have borders.
One of the real beauties of Stylus is that you can contains some style
definitions within others. You can see below that the header
contains an H1, and the H1 definitions will be compiled to only apply
within the context of the header. Stylus allows you to write CSS the
way you write HTML!
<<jsonstore.styl>>=
body
font-family: "Lucida Grande", Lucida, Helvetica, Arial, sans-serif
background: #FFF
color: #333
margin: 0px
padding: 0px
#main
position: relative
#header
background_gradient(#999)
margin: 0px
padding: 20px
border-bottom: 1px solid #ccc
h1
font-family: Inconsolata, Monaco, Courier, mono
color: #FFF
margin: 0px
.cart-info
position: absolute
top: 0px
right: 0px
text-align: right
padding: 10px
background_gradient(#555)
color: #FFF
font-size: 12px
font-weight: bold
img
border: 0
.productitemview
position: absolute
top: 0
left: 0
#productlistview
position: absolute
top: 0
left: 0
ul
list-style: none
.item
float:left
width: 250px
margin-right: 10px
margin-bottom: 10px
padding: 5px
rounded(5px)
border: 1px solid #ccc
text-align:center
font-size: 12px
.item-title
font-weight: bold
.item-artist
font-weight: bold
font-size: 14px
.item-detail
margin: 10px 0 0 10px
.item-image
float:left
margin-right: 10px
.item-info
padding: 100px 10px 0px 10px
@
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}

203
store.js
View File

@ -1,203 +0,0 @@
(function() {
var Product = Backbone.Model.extend({})
var ProductCollection = Backbone.Collection.extend({
model: Product,
initialize: function(models, options) {
this.url = options.url;
},
comparator: function(item) {
return item.get('title');
}
});
var Item = Backbone.Model.extend({
update: function(amount) {
this.set({'quantity': this.get('quantity') + amount});
}
});
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);
}
});
var _BaseView = Backbone.View.extend({
parent: $('#main'),
className: 'viewport',
initialize: function() {
this.el = $(this.el);
this.el.hide();
this.parent.append(this.el);
return this;
},
hide: function() {
if (this.el.is(":visible") === false) {
return null;
}
promise = $.Deferred(_.bind(function(dfd) {
this.el.fadeOut('fast', dfd.resolve)}, this)).promise();
this.trigger('hide', this);
return promise;
},
show: function() {
if (this.el.is(':visible')) {
return;
}
promise = $.Deferred(_.bind(function(dfd) {
this.el.fadeIn('fast', dfd.resolve) }, this)).promise();
this.trigger('show', this);
return promise;
}
});
var ProductListView = _BaseView.extend({
id: 'productlistview',
template: $("#store_index_template").html(),
initialize: function(options) {
this.constructor.__super__.initialize.apply(this, [options])
this.collection.bind('reset', _.bind(this.render, this));
},
render: function() {
this.el.html(_.template(this.template,
{'products': this.collection.toJSON()}))
return this;
}
});
var ProductView = _BaseView.extend({
id: 'productitemview',
template: $("#store_item_template").html(),
initialize: function(options) {
this.constructor.__super__.initialize.apply(this, [options])
this.itemcollection = options.itemcollection;
this.item = this.itemcollection.getOrCreateItemForProduct(this.model);
return this;
},
events: {
"keypress .uqf" : "updateOnEnter",
"click .uq" : "update",
},
update: function(e) {
e.preventDefault();
this.item.update(parseInt($('.uqf').val()));
},
updateOnEnter: function(e) {
if (e.keyCode == 13) {
return this.update(e);
}
},
render: function() {
this.el.html(_.template(this.template, this.model.toJSON()));
return this;
}
});
var CartWidget = Backbone.View.extend({
el: $('.cart-info'),
template: $('#store_cart_template').html(),
initialize: function() {
this.collection.bind('change', _.bind(this.render, this));
},
render: function() {
console.log(arguments);
this.el.html(
_.template(this.template, {
'count': this.collection.getTotalCount(),
'cost': this.collection.getTotalCost()
})).animate({paddingTop: '30px'})
.animate({paddingTop: '10px'});
}
});
var BackboneStore = Backbone.Router.extend({
views: {},
products: null,
cart: null,
routes: {
"": "index",
"item/:id": "product",
},
initialize: function(data) {
this.cart = new ItemCollection();
new CartWidget({collection: this.cart});
this.products = new ProductCollection([], {
url: 'data/items.json'});
this.views = {
'_index': new ProductListView({
collection: this.products
})
};
$.when(this.products.fetch({reset: true}))
.then(function() { window.location.hash = ''; });
return this;
},
hideAllViews: function () {
return _.select(
_.map(this.views, function(v) { return v.hide(); }),
function (t) { return t != null });
},
index: function() {
var view = this.views['_index'];
$.when(this.hideAllViews()).then(
function() { return view.show(); });
},
product: function(id) {
var product, v, view;
product = this.products.detect(function(p) { return p.get('id') == (id); })
view = ((v = this.views)['item.' + id]) || (v['item.' + id] = (
new ProductView({model: product,
itemcollection: this.cart}).render()));
$.when(this.hideAllViews()).then(
function() { view.show(); });
}
});
$(document).ready(function() {
new BackboneStore();
Backbone.history.start();
});
}).call(this);

View File

@ -1,839 +0,0 @@
// Underscore.js 1.1.7
// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
// For all details and documentation:
// http://documentcloud.github.com/underscore
(function() {
// Baseline setup
// --------------
// Establish the root object, `window` in the browser, or `global` on the server.
var root = this;
// Save the previous value of the `_` variable.
var previousUnderscore = root._;
// Establish the object that gets returned to break out of a loop iteration.
var breaker = {};
// Save bytes in the minified (but not gzipped) version:
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
// Create quick reference variables for speed access to core prototypes.
var slice = ArrayProto.slice,
unshift = ArrayProto.unshift,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// All **ECMAScript 5** native function implementations that we hope to use
// are declared here.
var
nativeForEach = ArrayProto.forEach,
nativeMap = ArrayProto.map,
nativeReduce = ArrayProto.reduce,
nativeReduceRight = ArrayProto.reduceRight,
nativeFilter = ArrayProto.filter,
nativeEvery = ArrayProto.every,
nativeSome = ArrayProto.some,
nativeIndexOf = ArrayProto.indexOf,
nativeLastIndexOf = ArrayProto.lastIndexOf,
nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeBind = FuncProto.bind;
// Create a safe reference to the Underscore object for use below.
var _ = function(obj) { return new wrapper(obj); };
// Export the Underscore object for **CommonJS**, with backwards-compatibility
// for the old `require()` API. If we're not in CommonJS, add `_` to the
// global object.
if (typeof module !== 'undefined' && module.exports) {
module.exports = _;
_._ = _;
} else {
// Exported as a string, for Closure Compiler "advanced" mode.
root['_'] = _;
}
// Current version.
_.VERSION = '1.1.7';
// Collection Functions
// --------------------
// The cornerstone, an `each` implementation, aka `forEach`.
// Handles objects with the built-in `forEach`, arrays, and raw objects.
// Delegates to **ECMAScript 5**'s native `forEach` if available.
var each = _.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (hasOwnProperty.call(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
// Return the results of applying the iterator to each element.
// Delegates to **ECMAScript 5**'s native `map` if available.
_.map = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
return results;
};
// **Reduce** builds up a single result from a list of values, aka `inject`,
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
var initial = memo !== void 0;
if (obj == null) obj = [];
if (nativeReduce && obj.reduce === nativeReduce) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
}
each(obj, function(value, index, list) {
if (!initial) {
memo = value;
initial = true;
} else {
memo = iterator.call(context, memo, value, index, list);
}
});
if (!initial) throw new TypeError("Reduce of empty array with no initial value");
return memo;
};
// The right-associative version of reduce, also known as `foldr`.
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
if (obj == null) obj = [];
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
}
var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();
return _.reduce(reversed, iterator, memo, context);
};
// Return the first value which passes a truth test. Aliased as `detect`.
_.find = _.detect = function(obj, iterator, context) {
var result;
any(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) {
result = value;
return true;
}
});
return result;
};
// Return all the elements that pass a truth test.
// Delegates to **ECMAScript 5**'s native `filter` if available.
// Aliased as `select`.
_.filter = _.select = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
each(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Return all the elements for which a truth test fails.
_.reject = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
each(obj, function(value, index, list) {
if (!iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Determine whether all of the elements match a truth test.
// Delegates to **ECMAScript 5**'s native `every` if available.
// Aliased as `all`.
_.every = _.all = function(obj, iterator, context) {
var result = true;
if (obj == null) return result;
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
each(obj, function(value, index, list) {
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
});
return result;
};
// Determine if at least one element in the object matches a truth test.
// Delegates to **ECMAScript 5**'s native `some` if available.
// Aliased as `any`.
var any = _.some = _.any = function(obj, iterator, context) {
iterator = iterator || _.identity;
var result = false;
if (obj == null) return result;
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
each(obj, function(value, index, list) {
if (result |= iterator.call(context, value, index, list)) return breaker;
});
return !!result;
};
// Determine if a given value is included in the array or object using `===`.
// Aliased as `contains`.
_.include = _.contains = function(obj, target) {
var found = false;
if (obj == null) return found;
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
any(obj, function(value) {
if (found = value === target) return true;
});
return found;
};
// Invoke a method (with arguments) on every item in a collection.
_.invoke = function(obj, method) {
var args = slice.call(arguments, 2);
return _.map(obj, function(value) {
return (method.call ? method || value : value[method]).apply(value, args);
});
};
// Convenience version of a common use case of `map`: fetching a property.
_.pluck = function(obj, key) {
return _.map(obj, function(value){ return value[key]; });
};
// Return the maximum element or (element-based computation).
_.max = function(obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
var result = {computed : -Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed >= result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Return the minimum element (or element-based computation).
_.min = function(obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
var result = {computed : Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed < result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Sort the object's values by a criterion produced by an iterator.
_.sortBy = function(obj, iterator, context) {
return _.pluck(_.map(obj, function(value, index, list) {
return {
value : value,
criteria : iterator.call(context, value, index, list)
};
}).sort(function(left, right) {
var a = left.criteria, b = right.criteria;
return a < b ? -1 : a > b ? 1 : 0;
}), 'value');
};
// Groups the object's values by a criterion produced by an iterator
_.groupBy = function(obj, iterator) {
var result = {};
each(obj, function(value, index) {
var key = iterator(value, index);
(result[key] || (result[key] = [])).push(value);
});
return result;
};
// Use a comparator function to figure out at what index an object should
// be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iterator) {
iterator || (iterator = _.identity);
var low = 0, high = array.length;
while (low < high) {
var mid = (low + high) >> 1;
iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
}
return low;
};
// Safely convert anything iterable into a real, live array.
_.toArray = function(iterable) {
if (!iterable) return [];
if (iterable.toArray) return iterable.toArray();
if (_.isArray(iterable)) return slice.call(iterable);
if (_.isArguments(iterable)) return slice.call(iterable);
return _.values(iterable);
};
// Return the number of elements in an object.
_.size = function(obj) {
return _.toArray(obj).length;
};
// Array Functions
// ---------------
// Get the first element of an array. Passing **n** will return the first N
// values in the array. Aliased as `head`. The **guard** check allows it to work
// with `_.map`.
_.first = _.head = function(array, n, guard) {
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
};
// Returns everything but the first entry of the array. Aliased as `tail`.
// Especially useful on the arguments object. Passing an **index** will return
// the rest of the values in the array from that index onward. The **guard**
// check allows it to work with `_.map`.
_.rest = _.tail = function(array, index, guard) {
return slice.call(array, (index == null) || guard ? 1 : index);
};
// Get the last element of an array.
_.last = function(array) {
return array[array.length - 1];
};
// Trim out all falsy values from an array.
_.compact = function(array) {
return _.filter(array, function(value){ return !!value; });
};
// Return a completely flattened version of an array.
_.flatten = function(array) {
return _.reduce(array, function(memo, value) {
if (_.isArray(value)) return memo.concat(_.flatten(value));
memo[memo.length] = value;
return memo;
}, []);
};
// Return a version of the array that does not contain the specified value(s).
_.without = function(array) {
return _.difference(array, slice.call(arguments, 1));
};
// Produce a duplicate-free version of the array. If the array has already
// been sorted, you have the option of using a faster algorithm.
// Aliased as `unique`.
_.uniq = _.unique = function(array, isSorted) {
return _.reduce(array, function(memo, el, i) {
if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el;
return memo;
}, []);
};
// Produce an array that contains the union: each distinct element from all of
// the passed-in arrays.
_.union = function() {
return _.uniq(_.flatten(arguments));
};
// Produce an array that contains every item shared between all the
// passed-in arrays. (Aliased as "intersect" for back-compat.)
_.intersection = _.intersect = function(array) {
var rest = slice.call(arguments, 1);
return _.filter(_.uniq(array), function(item) {
return _.every(rest, function(other) {
return _.indexOf(other, item) >= 0;
});
});
};
// Take the difference between one array and another.
// Only the elements present in just the first array will remain.
_.difference = function(array, other) {
return _.filter(array, function(value){ return !_.include(other, value); });
};
// Zip together multiple lists into a single array -- elements that share
// an index go together.
_.zip = function() {
var args = slice.call(arguments);
var length = _.max(_.pluck(args, 'length'));
var results = new Array(length);
for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
return results;
};
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
// we need this function. Return the position of the first occurrence of an
// item in an array, or -1 if the item is not included in the array.
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
_.indexOf = function(array, item, isSorted) {
if (array == null) return -1;
var i, l;
if (isSorted) {
i = _.sortedIndex(array, item);
return array[i] === item ? i : -1;
}
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
return -1;
};
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
_.lastIndexOf = function(array, item) {
if (array == null) return -1;
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
var i = array.length;
while (i--) if (array[i] === item) return i;
return -1;
};
// Generate an integer Array containing an arithmetic progression. A port of
// the native Python `range()` function. See
// [the Python documentation](http://docs.python.org/library/functions.html#range).
_.range = function(start, stop, step) {
if (arguments.length <= 1) {
stop = start || 0;
start = 0;
}
step = arguments[2] || 1;
var len = Math.max(Math.ceil((stop - start) / step), 0);
var idx = 0;
var range = new Array(len);
while(idx < len) {
range[idx++] = start;
start += step;
}
return range;
};
// Function (ahem) Functions
// ------------------
// Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Binding with arguments is also known as `curry`.
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
// We check for `func.bind` first, to fail fast when `func` is undefined.
_.bind = function(func, obj) {
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
var args = slice.call(arguments, 2);
return function() {
return func.apply(obj, args.concat(slice.call(arguments)));
};
};
// Bind all of an object's methods to that object. Useful for ensuring that
// all callbacks defined on an object belong to it.
_.bindAll = function(obj) {
var funcs = slice.call(arguments, 1);
if (funcs.length == 0) funcs = _.functions(obj);
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
return obj;
};
// Memoize an expensive function by storing its results.
_.memoize = function(func, hasher) {
var memo = {};
hasher || (hasher = _.identity);
return function() {
var key = hasher.apply(this, arguments);
return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
};
};
// Delays a function for the given number of milliseconds, and then calls
// it with the arguments supplied.
_.delay = function(func, wait) {
var args = slice.call(arguments, 2);
return setTimeout(function(){ return func.apply(func, args); }, wait);
};
// Defers a function, scheduling it to run after the current call stack has
// cleared.
_.defer = function(func) {
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
};
// Internal function used to implement `_.throttle` and `_.debounce`.
var limit = function(func, wait, debounce) {
var timeout;
return function() {
var context = this, args = arguments;
var throttler = function() {
timeout = null;
func.apply(context, args);
};
if (debounce) clearTimeout(timeout);
if (debounce || !timeout) timeout = setTimeout(throttler, wait);
};
};
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time.
_.throttle = function(func, wait) {
return limit(func, wait, false);
};
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds.
_.debounce = function(func, wait) {
return limit(func, wait, true);
};
// Returns a function that will be executed at most one time, no matter how
// often you call it. Useful for lazy initialization.
_.once = function(func) {
var ran = false, memo;
return function() {
if (ran) return memo;
ran = true;
return memo = func.apply(this, arguments);
};
};
// Returns the first function passed as an argument to the second,
// allowing you to adjust arguments, run code before and after, and
// conditionally execute the original function.
_.wrap = function(func, wrapper) {
return function() {
var args = [func].concat(slice.call(arguments));
return wrapper.apply(this, args);
};
};
// Returns a function that is the composition of a list of functions, each
// consuming the return value of the function that follows.
_.compose = function() {
var funcs = slice.call(arguments);
return function() {
var args = slice.call(arguments);
for (var i = funcs.length - 1; i >= 0; i--) {
args = [funcs[i].apply(this, args)];
}
return args[0];
};
};
// Returns a function that will only be executed after being called N times.
_.after = function(times, func) {
return function() {
if (--times < 1) { return func.apply(this, arguments); }
};
};
// Object Functions
// ----------------
// Retrieve the names of an object's properties.
// Delegates to **ECMAScript 5**'s native `Object.keys`
_.keys = nativeKeys || function(obj) {
if (obj !== Object(obj)) throw new TypeError('Invalid object');
var keys = [];
for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key;
return keys;
};
// Retrieve the values of an object's properties.
_.values = function(obj) {
return _.map(obj, _.identity);
};
// Return a sorted list of the function names available on the object.
// Aliased as `methods`
_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};
// Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
if (source[prop] !== void 0) obj[prop] = source[prop];
}
});
return obj;
};
// Fill in a given object with default properties.
_.defaults = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
if (obj[prop] == null) obj[prop] = source[prop];
}
});
return obj;
};
// Create a (shallow-cloned) duplicate of an object.
_.clone = function(obj) {
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};
// Invokes interceptor with the obj, and then returns obj.
// The primary purpose of this method is to "tap into" a method chain, in
// order to perform operations on intermediate results within the chain.
_.tap = function(obj, interceptor) {
interceptor(obj);
return obj;
};
// Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) {
// Check object identity.
if (a === b) return true;
// Different types?
var atype = typeof(a), btype = typeof(b);
if (atype != btype) return false;
// Basic equality test (watch out for coercions).
if (a == b) return true;
// One is falsy and the other truthy.
if ((!a && b) || (a && !b)) return false;
// Unwrap any wrapped objects.
if (a._chain) a = a._wrapped;
if (b._chain) b = b._wrapped;
// One of them implements an isEqual()?
if (a.isEqual) return a.isEqual(b);
if (b.isEqual) return b.isEqual(a);
// Check dates' integer values.
if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime();
// Both are NaN?
if (_.isNaN(a) && _.isNaN(b)) return false;
// Compare regular expressions.
if (_.isRegExp(a) && _.isRegExp(b))
return a.source === b.source &&
a.global === b.global &&
a.ignoreCase === b.ignoreCase &&
a.multiline === b.multiline;
// If a is not an object by this point, we can't handle it.
if (atype !== 'object') return false;
// Check for different array lengths before comparing contents.
if (a.length && (a.length !== b.length)) return false;
// Nothing else worked, deep compare the contents.
var aKeys = _.keys(a), bKeys = _.keys(b);
// Different object sizes?
if (aKeys.length != bKeys.length) return false;
// Recursive comparison of contents.
for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false;
return true;
};
// Is a given array or object empty?
_.isEmpty = function(obj) {
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;
return true;
};
// Is a given value a DOM element?
_.isElement = function(obj) {
return !!(obj && obj.nodeType == 1);
};
// Is a given value an array?
// Delegates to ECMA5's native Array.isArray
_.isArray = nativeIsArray || function(obj) {
return toString.call(obj) === '[object Array]';
};
// Is a given variable an object?
_.isObject = function(obj) {
return obj === Object(obj);
};
// Is a given variable an arguments object?
_.isArguments = function(obj) {
return !!(obj && hasOwnProperty.call(obj, 'callee'));
};
// Is a given value a function?
_.isFunction = function(obj) {
return !!(obj && obj.constructor && obj.call && obj.apply);
};
// Is a given value a string?
_.isString = function(obj) {
return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
};
// Is a given value a number?
_.isNumber = function(obj) {
return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed));
};
// Is the given value `NaN`? `NaN` happens to be the only value in JavaScript
// that does not equal itself.
_.isNaN = function(obj) {
return obj !== obj;
};
// Is a given value a boolean?
_.isBoolean = function(obj) {
return obj === true || obj === false;
};
// Is a given value a date?
_.isDate = function(obj) {
return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear);
};
// Is the given value a regular expression?
_.isRegExp = function(obj) {
return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false));
};
// Is a given value equal to null?
_.isNull = function(obj) {
return obj === null;
};
// Is a given variable undefined?
_.isUndefined = function(obj) {
return obj === void 0;
};
// Utility Functions
// -----------------
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
// previous owner. Returns a reference to the Underscore object.
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};
// Keep the identity function around for default iterators.
_.identity = function(value) {
return value;
};
// Run a function **n** times.
_.times = function (n, iterator, context) {
for (var i = 0; i < n; i++) iterator.call(context, i);
};
// Add your own custom functions to the Underscore object, ensuring that
// they're correctly added to the OOP wrapper as well.
_.mixin = function(obj) {
each(_.functions(obj), function(name){
addToWrapper(name, _[name] = obj[name]);
});
};
// Generate a unique integer id (unique within the entire client session).
// Useful for temporary DOM ids.
var idCounter = 0;
_.uniqueId = function(prefix) {
var id = idCounter++;
return prefix ? prefix + id : id;
};
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g
};
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(str, data) {
var c = _.templateSettings;
var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
'with(obj||{}){__p.push(\'' +
str.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(c.interpolate, function(match, code) {
return "'," + code.replace(/\\'/g, "'") + ",'";
})
.replace(c.evaluate || null, function(match, code) {
return "');" + code.replace(/\\'/g, "'")
.replace(/[\r\n\t]/g, ' ') + "__p.push('";
})
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t')
+ "');}return __p.join('');";
var func = new Function('obj', tmpl);
return data ? func(data) : func;
};
// The OOP Wrapper
// ---------------
// If Underscore is called as a function, it returns a wrapped object that
// can be used OO-style. This wrapper holds altered versions of all the
// underscore functions. Wrapped objects may be chained.
var wrapper = function(obj) { this._wrapped = obj; };
// Expose `wrapper.prototype` as `_.prototype`
_.prototype = wrapper.prototype;
// Helper function to continue chaining intermediate results.
var result = function(obj, chain) {
return chain ? _(obj).chain() : obj;
};
// A method to easily add functions to the OOP wrapper.
var addToWrapper = function(name, func) {
wrapper.prototype[name] = function() {
var args = slice.call(arguments);
unshift.call(args, this._wrapped);
return result(func.apply(_, args), this._chain);
};
};
// Add all of the Underscore functions to the wrapper object.
_.mixin(_);
// Add all mutator Array functions to the wrapper.
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
method.apply(this._wrapped, arguments);
return result(this._wrapped, this._chain);
};
});
// Add all accessor Array functions to the wrapper.
each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
return result(method.apply(this._wrapped, arguments), this._chain);
};
});
// Start chaining a wrapped Underscore object.
wrapper.prototype.chain = function() {
this._chain = true;
return this;
};
// Extracts the result from a wrapped and chained object.
wrapper.prototype.value = function() {
return this._wrapped;
};
})();

53
work/index.haml Normal file
View File

@ -0,0 +1,53 @@
!!! 5
%html{:xmlns => "http://www.w3.org/1999/xhtml"}
%head
%title The Backbone Store
%link{:charset => "utf-8", :href => "jsonstore.css", :rel => "stylesheet", :type => "text/css"}/
%script#store_index_template(type="text/x-underscore-tmplate")
%h1 Product Catalog
%ul
<% for(i=0,l=products.length;i<l;++i) { p = products[i]; %>
%li.item
.item-image
%a{:href => "#item/<%= p.id %>"}
%img{:src => "<%= p.image %>", :alt => "<%= p.title %>"}/
.item-artist <%= p.artist %>
.item-title <%= p.title %>
.item-price $<%= p.price %>
<% } %>
%script#store_item_template(type= "text/x-underscore-template")
.item-detail
.item-image
%img(src="<%= large_image %>" alt="<%= title %>")/
.item-info
.item-artist <%= artist %>
.item-title <%= title %>
.item-price $<%= price %>
.item-form
%form(action="#/cart" method="post")
%p
%label Quantity:
%input(type="text" size="2" name="quantity" value="1" class="uqf")/
%p
%input(type="submit" value="Add to Cart" class="uq")/
.item-link
%a(href="<%= url %>") Buy this item on Amazon
.back-link
%a(href="#") &laquo; Back to Items
%script#store_cart_template(type="text/x-underscore-template")
%p Items: <%= count %> ($<%= cost %>)
</head>
%body
#container
#header
%h1
The Backbone Store
.cart-info
#main
%script{:src => "lib/jquery.js", :type => "text/javascript"}
%script{:src => "lib/underscore.js", :type => "text/javascript"}
%script{:src => "lib/backbone.js", :type => "text/javascript"}
%script{:src => "store.js", :type => "text/javascript"}

96
work/jsonstore.styl Normal file
View File

@ -0,0 +1,96 @@
rounded(radius)
-moz-border-radius-topleft: radius
-moz-border-radius-topright: radius
-moz-border-radius-bottomleft: radius
-moz-border-radius-bottomright: radius
-webkit-border-bottom-right-radius: radius
-webkit-border-top-left-radius: radius
-webkit-border-top-right-radius: radius
-webkit-border-bottom-left-radius: radius
border-bottom-right-radius: radius
border-top-left-radius: radius
border-top-right-radius: radius
border-bottom-left-radius: radius
background_gradient(base)
background: base
background: -webkit-gradient(linear, left top, left bottom, from(lighten(base, 20%)), to(darken(base, 20%)))
background: -moz-linear-gradient(top, lighten(base, 20%), darken(base, 20%))
body
font-family: "Lucida Grande", Lucida, Helvetica, Arial, sans-serif
background: #FFF
color: #333
margin: 0px
padding: 0px
#main
position: relative
#header
background_gradient(#999)
margin: 0px
padding: 20px
border-bottom: 1px solid #ccc
h1
font-family: Inconsolata, Monaco, Courier, mono
color: #FFF
margin: 0px
.cart-info
position: absolute
top: 0px
right: 0px
text-align: right
padding: 10px
background_gradient(#555)
color: #FFF
font-size: 12px
font-weight: bold
img
border: 0
.productitemview
position: absolute
top: 0
left: 0
#productlistview
position: absolute
top: 0
left: 0
ul
list-style: none
.item
float:left
width: 250px
margin-right: 10px
margin-bottom: 10px
padding: 5px
rounded(5px)
border: 1px solid #ccc
text-align:center
font-size: 12px
.item-title
font-weight: bold
.item-artist
font-weight: bold
font-size: 14px
.item-detail
margin: 10px 0 0 10px
.item-image
float:left
margin-right: 10px
.item-info
padding: 100px 10px 0px 10px

164
work/store.coffee Normal file
View File

@ -0,0 +1,164 @@
class Product extends Backbone.Model
class Item extends Backbone.Model
update: (amount) ->
return if amount == @get 'quantity'
@set {quantity: amount}, {silent: true}
@collection.trigger 'update', @
price: () ->
@get('product').get('price') * @get('quantity')
class ProductCollection extends Backbone.Collection
model: Product
initialize: (models, options) ->
@url = options.url
comparator: (item) ->
item.get 'title'
class ItemCollection extends Backbone.Collection
model: Item
updateItemForProduct: (product, amount) ->
amount = if amount? then amount else 0
pid = product.get 'id'
i = this.detect (obj) -> (obj.get('product').get('id') == pid)
if i
i.update(amount)
return i
@add {product: product, quantity: amount}
getTotalCount: () ->
addup = (memo, obj) -> memo + obj.get 'quantity'
@reduce addup, 0
getTotalCost: () ->
addup = (memo, obj) -> memo + obj.price()
@reduce addup, 0
class _BaseView extends Backbone.View
parent: $('#main')
className: 'viewport'
initialize: () ->
@el = $(@el)
@el.hide()
@parent.append(@el)
@
hide: () ->
dfd = $.Deferred()
if not @el.is(':visible')
return dfd.resolve()
@el.fadeOut('fast', () -> dfd.resolve())
dfd.promise()
show: () ->
dfd = $.Deferred()
if @el.is(':visible')
return dfd.resolve()
@el.fadeIn('fast', () -> dfd.resolve())
dfd.promise()
class ProductListView extends _BaseView
id: 'productlistview'
template: $("#store_index_template").html()
initialize: (options) ->
_BaseView.prototype.initialize.apply @, [options]
@collection.bind 'reset', @render.bind @
render: () ->
@el.html(_.template(@template)({'products': @collection.toJSON()}))
@
class ProductView extends _BaseView
className: 'productitemview'
template: $("#store_item_template").html()
initialize: (options) ->
_BaseView.prototype.initialize.apply @, [options]
@itemcollection = options.itemcollection
events:
"keypress .uqf" : "updateOnEnter"
"click .uq" : "update"
update: (e) ->
e.preventDefault()
@itemcollection.updateItemForProduct @model, parseInt(@$('.uqf').val())
updateOnEnter: (e) ->
@update(e) if e.keyCode == 13
render: () ->
@el.html(_.template(@template)(@model.toJSON()));
@
class CartWidget extends Backbone.View
el: $('.cart-info')
template: $('#store_cart_template').html()
initialize: () ->
@collection.bind 'update', @render.bind @
render: () ->
tel = @$el.html _.template(@template)({
'count': @collection.getTotalCount()
'cost': @collection.getTotalCost()
})
tel.animate({paddingTop: '30px'}).animate({paddingTop: '10px'})
@
class BackboneStore extends Backbone.Router
views: {}
products: null
cart: null
routes:
"": "index"
"item/:id": "product"
initialize: (data) ->
@cart = new ItemCollection()
new CartWidget
collection: @cart
@products = new ProductCollection [],
url: 'data/items.json'
@views =
'_index': new ProductListView
collection: @products
$.when(@products.fetch({reset: true}))
.then(() -> window.location.hash = '')
@
hideAllViews: () ->
_.select(_.map(@views, (v) -> return v.hide()),
(t) -> t != null)
index: () ->
view = @views['_index']
$.when.apply($, @hideAllViews()).then(() -> view.show())
product: (id) ->
product = @products.detect (p) -> p.get('id') == (id)
view = (@views['item.' + id] ||= new ProductView(
model: product,
itemcollection: @cart
).render())
$.when(@hideAllViews()).then(
() -> view.show())
$(document).ready () ->
new BackboneStore();
Backbone.history.start();