Compare commits

..

2 Commits

Author SHA1 Message Date
Elf M. Sternberg 113f49979b Updating. This is so eff'd up. 2016-04-05 13:52:44 -07:00
Elf M. Sternberg d57177c22f Updating. 2016-04-05 13:40:44 -07:00
30 changed files with 1208 additions and 1411 deletions

10
.gitignore vendored
View File

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

119
Makefile
View File

@ -1,75 +1,70 @@
.PHONY: setup store serve .SUFFIXES: .nw .js .pdf .html .tex
NOTANGLE= notangle NOTANGLE= notangle
NOWEAVE= noweave NOWEAVE= noweave
ECHO= echo ECHO= /bin/echo
LIBS:= htdocs/lib/underscore.js htdocs/lib/jquery.js htdocs/lib/backbone.js all: index.html store.js
all: htdocs/index.html htdocs/store.js htdocs/data/items.json init:
@if [ ! -e "./htdocs/lib" ]; then \ mkdir -p thestore/lib
echo "Please do 'make setup' before continuing"; \ bower install jquery backbone underscore
exit 1; \ cp bower_components/backbone/backbone.js thestore/lib
cp bower_components/jquery/dist/jquery.js thestore/lib
cp bower_components/underscore/underscore.js thestore/lib
npm install
serve:
./node_modules/.bin/http-server thestore/
.nw.html:
$(NOWEAVE) -filter l2h -delay -x -index -autodefs c -html $*.nw > $*.html
.nw.tex:
$(NOWEAVE) -x -delay $*.nw > $*.tex #$
.tex.pdf:
xelatex $*.tex; \
while grep -s 'Rerun to get cross-references right' $*.log; \
do \
xelatex *$.tex; \
done
.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 fi
serve: all store.js: backbonestore.nw
./bin/autoreload @ $(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
store: all index.html: 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
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)
docs:
mkdir -p docs
htdocs/index.html: src/backbonestore.nw
$(NOTANGLE) -c -Rindex.html src/backbonestore.nw > htdocs/index.html
htdocs/store.js: src/backbonestore.nw
$(NOTANGLE) -c -Rstore.js src/backbonestore.nw > htdocs/store.js
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 docs/backbonestore.tex; \
done
mv backbonestore.pdf docs
rm -f ./backbonestore.log ./backbonestore.aux ./backbonestore.out
pdf: docs/backbonestore.pdf
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: clean:
- rm -f htdocs/*.js htdocs/*.html docs/*.tex docs/*.dvi docs/*.aux docs/*.toc docs/*.log docs/*.out - rm -f *.tex *.dvi *.aux *.toc *.log *.out *.html *.js
distclean: clean
- rm -fr ./htdocs/lib
realclean: distclean
- rm -fr docs
realclean: clean
- rm -f *.pdf
- rm -fr bower_components node_modules thestore/lib

35
README Normal file
View File

@ -0,0 +1,35 @@
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.
## 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.
## 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/).

View File

@ -1,66 +0,0 @@
# About
The Backbone Store is a tutorial and demonstration application for
BackboneJS, a javascript framework for managing data-driven websites.
## 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 local server.
## Requirements
The build tool relies upon GNU Make and node-js. 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 is 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/).

751
backbonestore.nw Normal file
View File

@ -0,0 +1,751 @@
% -*- Mode: poly-noweb+coffee; -*-
\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}
This is version 2.0 of \textbf{The Backbone Store}, a brief tutorial
on using [[backbone.js]].
\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.
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 months ago I was attempting to
learn Sammy.js, a library very similar to Backbone, and they had a
nifty tutorial called
\nwanchorto{http://code.quirkey.com/sammy/tutorials/json_store_part1.html}{The
JsonStore}.
In the spirit of The JSON Store, I present The Backbone Store.
\subsection{Literate Program}
A note: this article was written with the
\nwanchorto{http://en.wikipedia.org/wiki/Literate_programming}{Literate
Programming} toolkit
\nwanchorto{http://www.cs.tufts.edu/~nr/noweb/}{Noweb}. Where you see
something that looks like \\<\\<this\\>\\>, it's a placeholder for code
described elsewhere in the document. Placeholders with an equal sign
at the end of them indicate the place where that code is defined. The
link (U->) indicates that the code you're seeing is used later in the
document, and (<-U) indicates it was used earlier but is being defined
here.
\subsection{Revision}
This is version 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}
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 jQuery's
[[fadeIn()]] and [[fadeOut()]] features to transition between the
catalog and the product detail pages.
\subsection{Models, Collections, and Controllers}
Backbone's data layer provides two classes, [[Model]] and
[[Collection]]. To use the Model, you inherit from it, modify the
subclasss as needed, and then create new objects from the subclass by
constructing the model with a JSON object. You modify the object by
calling [[get()]] or [[set()]] on named attributes, rather than on the
Model object directly; this allows Model to notify other interested
objects that the object has been changed. And Model comes with
[[fetch()]] and [[save()]] methods that will automatically pull or
push a JSON representatino of the model to a server, if the Model has
[[url]] as one of its attributes.
Collections are just that: lists of objects of a specific model. You
extend the Collection class in a child class, and as you do you inform
the Collection of what Model it represents, what URL you use to
push/pull the full list of objects, and on what field the list should
be sorted by default. If you attempt to add a raw JSON object to a
collection, it constructs a corresponding Model object out of the JSON
and manipulates that.
I will be getting the 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.)
For our purposes, then, we have a [[Product]] and a
[[ProductCollection]]. A popular convention in Backbone is to use
concrete names for models, and Name\textbf{Collection} for the
collection.
Models are duck-typed by default; they do not care what you put into
them. So all I need to say is that a [[Product]] is-a [[Model]]. The
Collection is straightforward as well; I tell it what model it
represents, override the [[initialize()]] method (which is empty in
the Backbone default) to inform this Collection that it has a url, and
create the comparator function for default sorting.
<<product models>>=
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');
}
});
@
For the shopping cart, our cart will hold [[Item]]s, and the cart
itself will be an [[ItemCollection]]. Shoppings carts are a little
odd; the convention is that an [[Item]] is not a single instance of a
product, but a reference to the products and a quantity.
One thing we will be doing is changing the quantity, so I have
provided a convenience function for the Item that allows you to do
that. Now, no client classes such as Views need to know how the
quantity is updated.
Also, it would be nice to know the total price of the Item.
<<cart models>>=
var Item = Backbone.Model.extend({
update: function(amount) {
this.set({'quantity': amount}, {silent: true});
this.collection.trigger('change', this);
},
price: function() {
console.log(this.get('product').get('title'), this.get('quantity'));
return this.get('product').get('price') * this.get('quantity');
}
});
@
The [[ItemCollection]] is a little trickier. It is entirely
client-side; it has no synchronization with the backend at all. But
it does have a model.
The [[ItemCollection]] must be able to find an Item in the cart to
update when a view needs it. If the Item is not in the Collection, it
must create one. The method [[getOrCreateItemForProduct]] does this.
It uses the [[detect()]] method, a method [[Collection]] inherits from
Backbone's one dependency, Underscore.js; [[detect()]] returns the
first [[Item]] in the [[ItemCollection]] for which the function
returns [[true]]. Also, when I have to create a new Item, I want to
add it to the collection, and I pass the parameter [[silent]], which
prevents the Collection from notifying event subscribers that the
collection has changed. Since this is an Item with zero objects in
it, this is not a change to what the collection represents, and I
don't want Views to react without having to.
Finally, I add two methods that return the total count of objects in
the collection (not [[Items]], but actual [[Products]]), and the total
cost of those items in the cart. The Underscore method [[reduce()]]
does this by taking a function for adding progressive items, and a
starting value.
<<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.price() + memo; }, 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, a View is 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.
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>>=
var _BaseView = Backbone.View.extend({
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's 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.
<<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(_.bind(function(dfd) {
this.el.fadeOut('fast', dfd.resolve)}, this));
return promise.promise();
},
show: function() {
if (this.el.is(':visible')) {
return;
}
promise = $.Deferred(_.bind(function(dfd) {
this.el.fadeIn('fast', dfd.resolve) }, this))
return promise.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 detail template>>
<<cart 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.
That call to [[\_super\_]] is a Backbone 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>>=
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;
}
});
@
%$
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.
And here is the HTML:
<<product list template>>=
<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>
@
%$
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 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;
},
@
%$
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 [[this.\$]] 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. [[this.\$('.uqf')]] is shorthand for [[$('uqf', this.el)]], and
helps clarify what it is you're looking for.
%'
<<product detail view>>=
update: function(e) {
e.preventDefault();
this.item.update(parseInt(this.$('.uqf').val()));
},
updateOnEnter: function(e) {
if (e.keyCode == 13) {
return this.update(e);
}
},
@
%$
The render is straightforward:
<<product detail view>>=
render: function() {
this.el.html(_.template(this.template, this.model.toJSON()));
return this;
}
});
@
The product detail template is fairly straightforward. There is no
[[underscore]] magic because there are no loops.
<<product detail template>>=
<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>
<div class="item-info">
<div class="item-artist"><%= artist %></div>
<div class="item-title"><%= title %></div>
<div class="item-price">$<%= price %></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>
</script>
@
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>>=
var CartWidget = Backbone.View.extend({
el: $('.cart-info'),
template: $('#store_cart_template').html(),
initialize: function() {
this.collection.bind('change', _.bind(this.render, this));
},
@
%$
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: function() {
this.el.html(
_.template(this.template, {
'count': this.collection.getTotalCount(),
'cost': this.collection.getTotalCost()
})).animate({paddingTop: '30px'})
.animate({paddingTop: '10px'});
}
});
@
And the HTML for the template is dead simple:
<<cart template>>=
<script id="store_cart_template" type="text/x-underscore-template">
<p>Items: <%= count %> ($<%= cost %>)</p>
</script>
@
%$
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>>=
var BackboneStore = Backbone.Router.extend({
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: 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;
},
@
%$
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: function () {
return _.select(
_.map(this.views, function(v) { return v.hide(); }),
function (t) { return t != null });
},
@
Showing the product list view is basically hiding everything, then
showing the index:
<<router>>=
index: function() {
var view = this.views['_index'];
$.when(this.hideAllViews()).then(
function() { return 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: 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(); });
}
});
@
%$
Finally, we need to start the program
<<initialization>>=
$(document).ready(function() {
new BackboneStore();
Backbone.history.start();
});
@
%$
\section{The Program}
Here's the entirety of the program:
<<store.js>>=
(function() {
<<product models>>
<<cart models>>
<<base view>>
<<product list view>>
<<product detail view>>
<<cart widget>>
<<router>>
<<initialization>>
}).call(this);
@
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}

View File

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

Binary file not shown.

View File

@ -1,75 +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>
<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>

View File

@ -1,91 +0,0 @@
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;
}

View File

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

View File

@ -1,31 +1,26 @@
{ {
"name": "the-backbone-store", "name": "the-backbone-store",
"version": "3.0.1", "version": "1.0.0",
"description": "A comprehensive (one hopes) tutorial on a simple development platform for Backbone.", "description": "A Backbone Tutorial",
"main": "htdocs/index.html", "main": "backbone.js",
"dependencies": {
"http-server": "^0.9.0"
},
"devDependencies": {
"inotify": "^1.4.0",
"bower": "^1.7.0"
},
"scripts": { "scripts": {
"test": "make serve" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/elfsternberg/The-Backbone-Store.git" "url": "git+ssh://git@github.com/elfsternberg/The-Backbone-Store.git"
}, },
"keywords": [ "keywords": [
"tutorial",
"backbone", "backbone",
"javascript", "jquery"
"makefiles",
"node",
"tutorial"
], ],
"author": "Kenneth M. \"Elf\" Sternberg <elf.sternberg@gmail.com>", "devDependencies": {
"license": "BSD-2-Clause", "coffee-script": "1.6.x",
"http-server": "0.9.x"
},
"author": "Elf M. Sternberg <elf.sternberg@gmail.com>",
"license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/elfsternberg/The-Backbone-Store/issues" "url": "https://github.com/elfsternberg/The-Backbone-Store/issues"
}, },

View File

@ -1,839 +0,0 @@
% -*- Mode: poly-noweb+javascript -*-
\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.
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 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 $\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
have been 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. Addressing tabular
data usually involves three parts: The \textit{table}, \textit{row}, and
\textit{column}. In Backbone, these are represented by the
[[Collection]], the [[Model]], and the [[attribute]]. The
[[Collection]] often has a URL indicating the back-end source of the
table; the [[Model]] may have a URL indicating its specific row in the
table, as a way of efficiently saving itserlf back to the table.
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.
The Product literally has nothing to modify. It already provides all
the methods we need.
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>>=
var Product = Backbone.Model.extend({});
var Item = Backbone.Model.extend({
update: function(amount) {
if (amount === this.get('quantity')) {
return this;
}
this.set({quantity: amount}, {silent: true});
this.collection.trigger('update', this);
return this;
},
price: function() {
return this.get('product').get('price') * this.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. This is called a
\textit{chain}, and is fairly common in Javascript.
The big secret to Backbone is that it supplies an advanced 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. For
this program, it is the collection as a whole whose value matters,
because that collection as a whole represents our shopping cart. 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 its list of albums via these
methods to (in our case) static JSON back-end. Backbone provides a
mechanism for fetching JSON (and you can override the [[.parse()]]
method to handle XML, CSV, or whatever strikes your fancy); to use the
default [[.fetch()]] method, capture and set the Collection's [[.url]]
field:
<<product collection>>=
var ProductCollection = Backbone.Collection.extend({
model: Product,
initialize: function(models, options) {
this.url = options.url;
},
comparator: function(product) {
return product.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.
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>>=
var ItemCollection = Backbone.Collection.extend({
model: Item,
updateItemForProduct: function(product, amount) {
amount = amount != null ? amount : 0;
var pid = product.get('id');
var item = this.detect(function(obj) {
return obj.get('product').get('id') === pid;
});
if (item) {
item.update(amount);
return item;
}
return this.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: function() {
var addup = function(memo, obj) {
return memo + obj.get('quantity');
};
return this.reduce(addup, 0);
},
getTotalCost: function() {
var addup = function(memo, obj) {
return memo + obj.price();
};
return this.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>>=
var BaseView = Backbone.View.extend({
parent: $('#main'),
className: 'viewport',
initialize: function(options) {
Backbone.View.prototype.initialize.apply(this, arguments);
this.$el.hide();
this.parent.append(this.el);
},
@
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. In older versions of the Backbone Store, we
used to assign [[this.el]] to a jQuery-wrapped version of the element;
that's no longer necessary, as Backbone provides you with its own
version automatically in [[this.$el]].
The 'parent' field is something I created for my own use, since I intend
to have multiple child objects share the same piece of real-estate. The
'className' field is something Backbone automatically applies to the
generated [[DIV]] at construction time. If you pass in an existing
element at construction time for the View to use (which is not an
uncommon use case!), Backbone will \textit{not} apply the 'className' to
it; you'll have to do that yourself.
I use the [[initialize]] method above to ensure 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 or a jQuery object will not break
anything.
Next, we will define the hide and show functions.
<<base view>>=
hide: function() {
var dfd = $.Deferred();
if (!this.$el.is(':visible')) {
return dfd.resolve();
}
this.$el.fadeOut('fast', function() {
return dfd.resolve();
});
return dfd.promise();
},
show: function() {
var dfd = $.Deferred();
if (this.$el.is(':visible')) {
return dfd.resolve();
}
this.$el.fadeIn('fast', function() {
return dfd.resolve();
});
return 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 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 charset="utf-8" href="jsonstore.css" rel="stylesheet" type="text/css">
<<product list template>>
<<product detail template>>
<<cart template>>
</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>
@
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>>=
var ProductListView = BaseView.extend({
id: 'productlistview',
template: _.template($("#store_index_template").html()),
initialize: function(options) {
BaseView.prototype.initialize.apply(this, arguments);
this.collection.bind('reset', this.render.bind(this));
},
render: function() {
this.$el.html(this.template({
'products': this.collection.toJSON()
}));
return this;
}
});
@
%$
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. It takes a template and returns a
function ready to render the template. What we're saying here is that
we want this View to automatically re-render itself every time the given
collection changes in a significant way, using the given template, into
the given element. That's what this view ``means.''
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 HTML for our home page's template:
<<product list template>>=
<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>
@
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>>=
var ProductView = BaseView.extend({
className: 'productitemview',
template: _.template($("#store_item_template").html()),
initialize: function(options) {
BaseView.prototype.initialize.apply(this, [options]);
this.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 [[this.$]] 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. [[this.$('.uqf')]] is shorthand for [[$('uqf', this.el)]], and helps
clarify what it is you're looking for.
<<product detail view>>=
update: function(e) {
e.preventDefault();
return this.itemcollection.updateItemForProduct(this.model, parseInt(this.$('.uqf').val()));
},
updateOnEnter: function(e) {
if (e.keyCode === 13) {
this.update(e);
}
},
@
The render is straightforward:
<<product detail view>>=
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
@
The product detail template is fairly straightforward. There is no
[[underscore]] magic because there are no loops.
<<product detail template>>=
<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>
@
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>>=
var CartWidget = Backbone.View.extend({
el: $('.cart-info'),
template: _.template($('#store_cart_template').html()),
initialize: function() {
Backbone.View.prototype.initialize.apply(this, arguments);
this.collection.bind('update', this.render.bind(this));
},
@
%$
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: function() {
var tel = this.$el.html(this.template({
'count': this.collection.getTotalCount(),
'cost': this.collection.getTotalCost()
}));
tel.animate({ paddingTop: '30px' }).animate({ paddingTop: '10px' });
return this;
}
});
@
You may have noticed that every render ends in [[return this]]. There's
a reason for that. Render is supposed to be pure statement: it's not
supposed to calculate anything, nor is it supposed to mutate anything on
the Javascript side. It can and frequently does, but that's beside the
point. By returning [[this]], we can then call something immediately
afterward.
For example, let's say you have a pop-up dialog. It starts life
hidden. You need to update the dialog, then show it:
<<example>>=
myDialog.render().show();
@
Because what render() return is [[this]], this code works as expected.
That's how you do chaining in HTML/Javascript.
Back to our code. The HTML for the Cart widget template is dead simple:
<<cart template>>=
<script id="store_cart_template" type="text/x-underscore-template">
<p>Items: <%= count %> ($<%= cost %>)</p>
</script>
@
%$
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>>=
var BackboneStore = Backbone.Router.extend({
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: function(data) {
Backbone.Router.prototype.initialize.apply(this, arguments);
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 = '';
});
},
@
%$
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 [[_.filter()]] call at the end means that this
method returns only an array of deferreds.
<<router>>=
hideAllViews: function() {
return _.filter(_.map(this.views, function(v) { return v.hide(); }),
function(t) { return 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: function() {
var view = this.views['_index'];
return $.when.apply($, this.hideAllViews()).then(function() {
return 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. Note that the view only needs to be rendered \textit{once}, after
which we can just hide or show it on request.
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: function(id) {
var view = this.views[id];
if (!view) {
var product = this.products.detect(function(p) {
return p.get('id') === id;
});
view = this.views[id] = new ProductView({
model: product,
itemcollection: this.cart
}).render();
}
return $.when(this.hideAllViews()).then(function() {
return view.show();
});
}
});
@
%$
Finally, we need to start the program
<<initialization>>=
$(document).ready(function() {
new BackboneStore();
return Backbone.history.start();
});
@
%$
\section{The Program}
Here's the entirety of the program. Coffeescript provides its own
namespace wrapper:
<<store.js>>=
<<models>>
<<product collection>>
<<cart collection>>
<<base view>>
<<product list view>>
<<product detail view>>
<<cart widget>>
<<router>>
<<initialization>>
@
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}

View File

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

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

75
thestore/index.html Normal file
View File

@ -0,0 +1,75 @@
<!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">
<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>
<div class="item-info">
<div class="item-artist"><%= artist %></div>
<div class="item-title"><%= title %></div>
<div class="item-price">$<%= price %></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>
</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="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>

63
thestore/jsonstore.css Normal file
View File

@ -0,0 +1,63 @@
/******************************************************
* 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;
}
#productlistview ul {
list-style: none;
}
.item {
display: border;
float:left;
width: 250px;
margin-right: 10px;
margin-bottom: 10px;
padding: 10px;
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;
}

211
thestore/store.js Normal file
View File

@ -0,0 +1,211 @@
(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': amount}, {silent: true});
this.collection.trigger('change', this);
},
price: function() {
console.log(this.get('product').get('title'), this.get('quantity'));
return this.get('product').get('price') * this.get('quantity');
}
});
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.price() + 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));
return promise.promise();
},
show: function() {
if (this.el.is(':visible')) {
return;
}
promise = $.Deferred(_.bind(function(dfd) {
this.el.fadeIn('fast', dfd.resolve) }, this))
return promise.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(this.$('.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() {
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);