Modernization proceeds apace.

This commit is contained in:
Elf M. Sternberg 2016-04-15 15:06:16 -07:00
parent f6ba14f17b
commit 02828b2bae
26 changed files with 337 additions and 2804 deletions

6
.gitignore vendored
View File

@ -4,3 +4,9 @@
.#* .#*
.DS_Store .DS_Store
*~ *~
node_modules/*
bower_components/*
npm-debug.log
work/*
htdocs/*.*
htdocs/lib

View File

@ -1,4 +1,5 @@
.SUFFIXES: .nw .js .pdf .html .tex .haml .css .stylus .SUFFIXES: .nw .js .pdf .html .tex .haml .css .stylus
.PHONY: setup
NOTANGLE= notangle NOTANGLE= notangle
NOWEAVE= noweave NOWEAVE= noweave
@ -7,28 +8,58 @@ STYLUS= stylus
HAML= haml HAML= haml
COFFEE= coffee COFFEE= coffee
all: index.html store.js jsonstore.css all: htdocs htdocs/index.html htdocs/store.js htdocs/jsonstore.css htdocs/lib/underscore.js htdocs/lib/jquery.js htdocs/lib/backbone.js htdocs/data/items.json
index.html: index.haml setup:
$(HAML) --unix-newlines --no-escape-attrs --double-quote-attribute $*.haml > $*.html npm install
bower install jquery underscore backbone
mkdir -p htdocs/lib
index.haml: backbonestore.nw htdocs/lib/underscore.js:
$(NOTANGLE) -c -R$@ $< > $*.haml mkdir -p htdocs/lib
cp bower_components/underscore/underscore.js htdocs/lib
jsonstore.css: jsonstore.styl htdocs/lib/jquery.js:
$(STYLUS) $*.styl mkdir -p htdocs/lib
cp bower_components/jquery/dist/jquery.js htdocs/lib
jsonstore.styl: backbonestore.nw htdocs/lib/backbone.js:
$(NOTANGLE) -c -R$@ $< > $@ cp bower_components/backbone/backbone.js htdocs/lib
store.js: store.coffee work:
$(COFFEE) --compile $< mkdir -p work
store.coffee: backbonestore.nw docs:
$(NOTANGLE) -c -R$@ $< > $@ mkdir -p docs
htdocs:
mkdir -p htdocs
htdocs/index.html: htdocs work/index.haml
$(HAML) --unix-newlines --no-escape-attrs --double-quote-attribute work/index.haml > htdocs/index.html
work/index.haml: work src/backbonestore.nw
$(NOTANGLE) -c -Rindex.haml src/backbonestore.nw > work/index.haml
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 src/items.json
$(NOTANGLE) -c -Rstore.coffee src/backbonestore.nw > work/store.coffee
htdocs/data/items.json: src/items.json
mkdir -p htdocs/data
cp src/items.json htdocs/data/items.json
.nw.tex: .nw.tex:
$(NOWEAVE) -x -delay $*.nw > $*.tex #$ $(NOWEAVE) -x -delay $*.nw > $*.tex
.tex.pdf: .tex.pdf:
xelatex $*.tex; \ xelatex $*.tex; \
@ -38,8 +69,9 @@ store.coffee: backbonestore.nw
done done
clean: clean:
- rm -f *.tex *.dvi *.aux *.toc *.log *.out *.html *.js - rm -f docs/*.tex docs/*.dvi docs/*.aux docs/*.toc docs/*.log docs/*.out htdocs/*.html htdocs/*.js htdocs/*.css
realclean: clean realclean: clean
- rm -f *.pdf - rm -f docs/*.pdf
- rm -fr work htdocs

File diff suppressed because it is too large Load Diff

21
bin/autoreload Executable file
View File

@ -0,0 +1,21 @@
#!/usr/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']
make.stdout.on 'data', spew
monitor.addWatch
path: "./src/backbonestore.nw"
watch_for: Inotify.IN_CLOSE_WRITE
callback: reBuild

56
htdocs/data/items.json Normal file
View File

@ -0,0 +1,56 @@
[
{
"id": "unless-you-have-been-drinking",
"title": "Unless You Have Been Drinking",
"artist": "Adventures in Odyssey",
"image": "images/AdventuresInOdyssey_t.jpg",
"large_image": "images/AdventuresInOdyssey.jpg",
"price": 9.98,
"url": "http://www.amazon.com/Door-Religious-Knives/dp/B001FGW0UQ/?tag=quirkey-20"
},
{
"id": "leave-to-do-my-utmost",
"title": "Leave To Do My Utmost",
"artist": "American Attorneys",
"image": "images/AmericanAttorneys_t.jpg",
"large_image": "images/AmericanAttorneys.jpg",
"price": 13.98,
"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",
"title": "The Dead Sleep Encircled by The Living",
"artist": "British Civil Light Transport",
"image": "images/BritishCivilLightTransport_t.jpg",
"large_image": "images/BritishCivilLightTransport.jpg",
"price": 13.98,
"url": "http://www.amazon.com/Bitte-Orca-Dirty-Projectors/dp/B0026T4RTI/ref=pd_sim_m_12?tag=quirkey-20"
},
{
"id": "periods-of-mental-assimilation",
"title": "Periods of Mental Assimilation",
"artist": "Grigory Szondy",
"image": "images/PeriodsofMentalAssimilation_t.jpg",
"large_image": "images/PeriodsofMentalAssimilation.jpg",
"price": 13.99,
"url": "http://www.amazon.com/Pains-Being-Pure-Heart/dp/B001LGXIDS/ref=pd_sim_m_44?tag=quirkey-20"
},
{
"id": "keenly-developed-moral-bankruptcy",
"title": "Keenly Developed Moral Bankruptcy",
"artist": "Stealth Monkey Virus",
"image": "images/StealthMonkeyVirus_t.jpg",
"large_image": "images/StealthMonkeyVirus.png",
"price": 13.99,
"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",
"title": "My Mistress's Sparrow is Dead",
"artist": "Sums of Mongolia",
"image": "images/SumsofMagnolia_t.jpg",
"large_image": "images/SumsofMagnolia.jpg",
"price": 13.99,
"url": "http://www.amazon.com/Pains-Being-Pure-Heart/dp/B001LGXIDS/ref=pd_sim_m_44?tag=quirkey-20"
}
]

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

View File

@ -1,71 +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="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,78 +0,0 @@
body {
font-family: "Lucida Grande", Lucida, Helvetica, Arial, sans-serif;
background: #fff;
color: #333;
margin: 0px;
padding: 0px;
}
#header {
background: #999;
background: -webkit-gradient(linear, left top, left bottom, from(#b8b8b8), to(#7a7a7a));
background: -moz-linear-gradient(top, #b8b8b8, #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(#666), to(#444));
background: -moz-linear-gradient(top, #666, #444);
color: #fff;
font-size: 12px;
font-weight: bold;
}
img {
border: 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,8 +1,10 @@
% -*- Mode: noweb; noweb-code-mode: coffee-mode ; noweb-doc-mode: latex-mode -*- % -*- Mode: poly-noweb+coffee -*-
\documentclass{article} \documentclass{article}
\usepackage{noweb} \usepackage{noweb}
\usepackage[T1]{fontenc} \usepackage[T1]{fontenc}
\usepackage{hyperref} \usepackage{hyperref}
\usepackage{fontspec, xunicode, xltxtra}
\setromanfont{Georgia}
\begin{document} \begin{document}
% Generate code and documentation with: % Generate code and documentation with:
@ -13,39 +15,47 @@
\section{Introduction} \section{Introduction}
This is version 2.0\textit{bis} of \textbf{The Backbone Store}, a This is version 3.0 of \textbf{The Backbone Store}, a brief tutorial on
brief tutorial on using [[backbone.js]]. It uses the original using [[backbone.js]]. The version you are currently reading has been
Backbone Store as a reference, but using a modern suite of tools: tested with the latest versions of the supporting software as of April,
Coffeescript (version 1.1), HAML (Ruby version 3.1.1), and Stylus. 2016.
\nwanchorto{http://jashkenas.github.com/coffee-script/}{CoffeeScript} \nwanchorto{http://documentcloud.github.com/backbone/}{Backbone.js} is a
is a lovely little languange that compiles into Javascript. It popular Model-View-Controller (MVC) library that provides a framework
provides a class-based architecture (that is compatible with for creating data-rich, single-page web applications. It provides (1) a
Backbone), has an elegant structure for defining functions and two-layer scheme for separating data from presentation, (2) a means of
methods, and strips out as much extraneous punctuation as possible. automatically synchronizing data with a server in a RESTful manner, and
Some people find the whitespace-as-semantics a'la Python offputting, (3) a mechanism for making some views bookmarkable and navigable.
but most disciplined developers already indent appropriately.
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 \nwanchorto{http://haml-lang.com/}{HAML} is a languange that compiles
into HTML. Like CoffeeScript, it uses whitespace for semantics: into HTML. Like CoffeeScript, it uses whitespace for semantics:
indentation levels correspond to HTML containerizations. It allows indentation levels correspond to HTML containerizations. It allows you
you to use rich scripting while preventing heirarchy misplacement to use rich scripting while preventing heirarchy misplacement mistakes.
mistakes. Its shorthand also makes writing HTML much faster. Its shorthand also makes writing HTML much faster.
\nwanchorto{https://github.com/LearnBoost/stylus/}{Stylus} is \nwanchorto{https://github.com/LearnBoost/stylus/}{Stylus} is languange
languange that compiles into CSS. Like CoffeeScript and HAML, it uses that compiles into CSS. Like CoffeeScript and HAML, it uses whitespace
whitespace for semantics. It also provides mixins and functions that for semantics. It also provides mixins and functions that allow you to
allow you to define visual styles such as borders and gradients, and define visual styles such as borders and gradients, and mix them into
mix them into specific selectors in the CSS rather than having to specific selectors in the CSS rather than having to write them into the
write them into the HTML. HTML.
\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: There are a number of other good tutorials for Backbone (See:
\nwanchorto{http://www.plexical.com/blog/2010/11/18/backbone-js-tutorial/}{Meta \nwanchorto{http://www.plexical.com/blog/2010/11/18/backbone-js-tutorial/}{Meta
@ -56,7 +66,7 @@ There are a number of other good tutorials for Backbone (See:
Mobile} (which is written in Mobile} (which is written in
\nwanchorto{http://jashkenas.github.com/coffee-script/}{Coffee}), and \nwanchorto{http://jashkenas.github.com/coffee-script/}{Coffee}), and
\nwanchorto{http://joshbohde.com/2010/11/25/backbonejs-and-django/}{Backbone \nwanchorto{http://joshbohde.com/2010/11/25/backbonejs-and-django/}{Backbone
and Django}. However, a couple of months ago I was attempting to 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 learn Sammy.js, a library very similar to Backbone, and they had a
nifty tutorial called nifty tutorial called
\nwanchorto{http://code.quirkey.com/sammy/tutorials/json_store_part1.html}{The \nwanchorto{http://code.quirkey.com/sammy/tutorials/json_store_part1.html}{The
@ -79,151 +89,154 @@ here.
\subsection{Revision} \subsection{Revision}
This is version 2.0 of \textit{The Backbone Store}. It includes This is version 3.0 of \textit{The Backbone Store}. It includes several
changes to the store based upon a better understanding of what significant updates, including the use of both NPM and Bower to build
Backbone.js can do. This version uses jQuery 1.6.2 and Backbone the final application.
0.5.2.
\subsection{The Store} \subsection{The Store: What We're Going to Build}
To demonstrate the basics of Backbone, I'm going to create a simple To demonstrate the basics of Backbone, I'm going to create a simple
one-page application, a store for record albums, with two unique one-page application, a store for record albums, with two unique views:
views: a list of all products and a product detail view. I will also a list of all products and a product detail view. I will also put a
put a shopping cart widget on the page that shows the user how many shopping cart widget on the page that shows the user how many products
products he or she has dropped into the cart. I'll use jQuery's he or she has dropped into the cart. I'll use some simple animations to
[[fadeIn()]] and [[fadeOut()]] features to transition between the transition between the catalog and the product detail pages.
catalog and the product detail pages.
\subsection{Models, Collections, and Controllers} \subsection{Models, Collections, and Controllers}
Backbone's data layer provides two classes, [[Model]] and Backbone's data layer provides two classes, [[Collection]] and
[[Collection]]. To use the Model, you inherit from it, modify the [[Model]].
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 Every web application has data, often tabular data. Full-stack web
extend the Collection class in a child class, and as you do you inform developers are (or ought to be) familiar with the \textit{triples} of
the Collection of what Model it represents, what URL you use to addressing objects on the web: Table URL → Row → Field, or Page URL →
push/pull the full list of objects, and on what field the list should HTML Node → Content. The [[Collection]] object represents just that: a
be sorted by default. If you attempt to add a raw JSON object to a collection of similar items. The [[Model]] represents exactly one of
collection, it constructs a corresponding Model object out of the JSON those items.
and manipulates that.
I will be getting the data from a simplified JSON file that comes in To use the Model, you inherit from it using Backbone's own [[.extend()]]
the download; it contains six record albums that the store sells. class method, adding or replacing methods in the child object as
(Unlike the JSON store, these albums do not exist; the covers were needed. For our purposes, we have two models: [[Product]] represents
generated during a round of something we wish to sell, and [[Item]] represents something currently
\nwanchorto{http://elfs.livejournal.com/756709.html}{The Album Cover in the customer's shopping cart.
Game}, a meme one popular with graphic designers.)
For our purposes, then, we have a [[Product]] and a Shopping carts are a little odd; the convention is that [[Item]] is not a
[[ProductCollection]]. A popular convention in Backbone is to use single instance of the product, but instead has a reference to the
concrete names for models, and Name\textbf{Collection} for the product, and a count of how many the buyer wants. To that end, I am
collection. adding two methods that extend Item: [[.update()]], which changes the
current quantity, and [[.price()]], which calculates the product's price
times the quantity:
Models are duck-typed by default; they do not care what you put into <<models>>=
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.
Note that Coffeescript uses '@' to represent [[this]], and always
returns the last lvalue generated by every function and method. So
the last line of [[initialize]] below compiles to [[return this]].
<<product models>>=
class Product extends Backbone.Model 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 class ProductCollection extends Backbone.Collection
model: Product model: Product
initialize: (models, options) -> initialize: (models, options) ->
@url = options.url @url = options.url
@
comparator: (item) -> comparator: (item) ->
item.get('title') 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.
For the shopping cart, our cart will hold [[Item]]s, and the cart The [[ItemCollection]] doesn't have a URL, but we do have several helper
itself will be an [[ItemCollection]]. Shoppings carts are a little methods to add. We don't want to add Items; instead, we want to add
odd; the convention is that an [[Item]] is not a single instance of a products as needed, then update the count as requested. If the product
product, but a reference to the products and a quantity. is already in our system, we don't want to create duplicates.
One thing we will be doing is changing the quantity, so I have <<cart collection>>=
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>>=
class Item extends Backbone.Model
update: (amount) ->
if amount == @get('quantity')
return
@set {quantity: amount}, {silent: true}
@collection.trigger('change', this)
price: () ->
@get('product').get('price') * @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>>=
class ItemCollection extends Backbone.Collection class ItemCollection extends Backbone.Collection
model: Item model: Item
getOrCreateItemForProduct: (product) -> @
pid = product.get('id')
i = this.detect (obj) -> (obj.get('product').get('id') == pid)
if (i)
return i
i = new Item
product: product
quantity: 0
@add i, {silent: true}
i
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 cart collection>>=
@
getTotalCount: () -> getTotalCount: () ->
addup = (memo, obj) -> obj.get('quantity') + memo addup = (memo, obj) -> memo + obj.get 'quantity'
@reduce addup, 0 @reduce addup, 0
getTotalCost: () -> getTotalCost: () ->
addup = (memo, obj) ->obj.price() + memo addup = (memo, obj) -> memo + obj.price()
@reduce(addup, 0); @reduce addup, 0
@ @
@ -235,12 +248,11 @@ listen for events, and a model or collection they represent within
that element. Views are not rigid; it's just Javascript and the DOM, that element. Views are not rigid; it's just Javascript and the DOM,
and you can hook external events as needed. and you can hook external events as needed.
More importantly, a View is sensitive to events \textit{within its More importantly, a View is sensitive to events \textit{within its model
model or collection}, and can respond to changes automatically. or collection}, and can respond to changes automatically. This way,
This way, if you have a rich data ecosystem, when changes to one data if you have a rich data ecosystem, when changes to one data item results
item results in a cascade of changes throughout your datasets, the in a cascade of changes throughout your datasets, the views will receive
views will receive ``change'' events and can update themselves ``change'' events and can update themselves accordingly.
accordingly.
I will show how this works with the shopping cart widget. I will show how this works with the shopping cart widget.
@ -263,13 +275,17 @@ class _BaseView extends Backbone.View
The above says that I am creating a class called \texttt{BaseView} and 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 defining two fields. The first, 'parent', will be used by all child
views to identify into which DOM object the View root element will views to identify into which DOM object the View root element will be
be rendered. The second defines a common class we will use for the rendered. The second defines a common class we will use for the purpose
purpose of identifying these views to jQuery. Backbone automatically of identifying these views to jQuery. Backbone automatically creates a
creates a new [[DIV]] object with the class 'viewport' when a view new [[DIV]] object with the class 'viewport' when a view constructor is
constructor is called. It will be our job to attach that [[DIV]] to called. It will be our job to attach that [[DIV]] to the DOM. In the
the DOM. In the HTML, you will see the [[DIV\#main]] object where most HTML, you will see the [[DIV\#main]] object where most of the work will
of the work will be rendered. 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>>= <<base view>>=
initialize: () -> initialize: () ->
@ -290,8 +306,8 @@ anything.
Next, we will define the hide and show functions. Next, we will define the hide and show functions.
Note that in coffeescript, the [[=>]] operator completely replaces the Note that in Coffeescript, the [[=>]] operator completely replaces the
[[_.bind()]] function provided by underscore. [[.bind()]] function provided by modern Javascript.
<<base view>>= <<base view>>=
hide: () -> hide: () ->
@ -309,17 +325,17 @@ Note that in coffeescript, the [[=>]] operator completely replaces the
@ @
\textbf{Deferred} is a new feature of jQuery. It is a different \textbf{Deferred} is a feature of jQuery called ``promises''. It is a
mechanism for invoking callbacks by attaching attributes and behavior different mechanism for invoking callbacks by attaching attributes and
to the callback function. By using this, we can say thing like behavior to the callback function. By using this, we can say thing like
``\textit{When} everything is hidden (when every deferred returned ``\textit{When} everything is hidden (when every deferred returned from
from \textbf{hide} has been resolved), \textit{then} show the \textbf{hide} has been resolved), \textit{then} show the appropriate
appropriate viewport.'' Deferreds are incredibly powerful, and this viewport.'' Deferreds are incredibly powerful, and this is a small
is a small taste of what can be done with them. 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 Before we move on, let's take a look at the HAML we're going to use for
for our one-page application. The code below compiles beautifully our one-page application. The code below compiles beautifully into the
into the same HTML seen in the original Backbone Store. same HTML seen in the original Backbone Store.
<<index.haml>>= <<index.haml>>=
!!! 5 !!! 5
@ -338,9 +354,9 @@ into the same HTML seen in the original Backbone Store.
The Backbone Store The Backbone Store
.cart-info .cart-info
#main #main
%script{:src => "jquery-1.6.2.min.js", :type => "text/javascript"} %script{:src => "lib/jquery.js", :type => "text/javascript"}
%script{:src => "underscore.js", :type => "text/javascript"} %script{:src => "lib/underscore.js", :type => "text/javascript"}
%script{:src => "backbone.js", :type => "text/javascript"} %script{:src => "lib/backbone.js", :type => "text/javascript"}
%script{:src => "store.js", :type => "text/javascript"} %script{:src => "store.js", :type => "text/javascript"}
@ @
@ -360,10 +376,10 @@ Backbone users frequently have: \textit{What is \texttt{render()}
functionality, we use the parent class's \texttt{show()} and functionality, we use the parent class's \texttt{show()} and
\texttt{hide()} methods to actually show the view. \texttt{hide()} methods to actually show the view.
That call to [[\_super\_]] is a Backbone idiom for calling a method on That call to [[.prototype]] is a Javascript idiom for calling a method
the parent object. It is, as far as anyone knows, the only way to 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. invoke a superclass method if it has been redefined in a subclass. It
It is rather ugly, but useful. is rather ugly, but useful.
<<product list view>>= <<product list view>>=
class ProductListView extends _BaseView class ProductListView extends _BaseView
@ -371,7 +387,7 @@ class ProductListView extends _BaseView
template: $("#store_index_template").html() template: $("#store_index_template").html()
initialize: (options) -> initialize: (options) ->
@constructor.__super__.initialize.apply @, [options] _BaseView.prototype.initialize.apply @, [options]
@collection.bind 'reset', _.bind(@render, @) @collection.bind 'reset', _.bind(@render, @)
render: () -> render: () ->
@ -414,21 +430,19 @@ 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 showing and hiding the various [[DIV]] objects in [[\#main]] again and
again. again.
The only trickiness here is twofold: the (rather hideous) means by The only trickiness here is twofold: the means by which one calls the
which one calls the method of a parnt class from a child class via method of a parent class from a child class via Backbone's class
Backbone's class heirarchy (this is most definitely \textbf{not} heirarchy, and keeping track of the itemcollection object, so we can add
Javascript standard), and keeping track of the itemcollection object, and change items as needed.
so we can add and change items as needed.
<<product detail view>>= <<product detail view>>=
class ProductView extends _BaseView class ProductView extends _BaseView
id: 'productitemview' id: 'productitemview'
template: $("#store_item_template").html() template: $("#store_item_template").html()
initialize: (options) -> initialize: (options) ->
@constructor.__super__.initialize.apply @, [options] _BaseView.prototype.initialize.apply @, [options]
@itemcollection = options.itemcollection @itemcollection = options.itemcollection
@item = @itemcollection.getOrCreateItemForProduct @model @item = @itemcollection.getOrCreateItemForProduct @model
@
@ @
%$ %$
@ -691,9 +705,11 @@ Here's the entirety of the program. Coffeescript provides its own
namespace wrapper: namespace wrapper:
<<store.coffee>>= <<store.coffee>>=
<<product models>> <<models>>
<<cart models>> <<product collection>>
<<cart collection>>
<<base view>> <<base view>>

View File

@ -1,168 +0,0 @@
class Product extends Backbone.Model
class ProductCollection extends Backbone.Collection
model: Product
initialize: (models, options) ->
@url = options.url
@
comparator: (item) ->
item.get('title')
class Item extends Backbone.Model
update: (amount) ->
if amount == @get('quantity')
return
@set {quantity: amount}, {silent: true}
@collection.trigger('change', this)
price: () ->
@get('product').get('price') * @get('quantity')
class ItemCollection extends Backbone.Collection
model: Item
getOrCreateItemForProduct: (product) ->
pid = product.get('id')
i = this.detect (obj) -> (obj.get('product').get('id') == pid)
if (i)
return i
i = new Item
product: product
quantity: 0
@add i, {silent: true}
i
getTotalCount: () ->
addup = (memo, obj) -> obj.get('quantity') + memo
@reduce addup, 0
getTotalCost: () ->
addup = (memo, obj) ->obj.price() + memo
@reduce(addup, 0);
class _BaseView extends Backbone.View
parent: $('#main')
className: 'viewport'
initialize: () ->
@el = $(@el)
@el.hide()
@parent.append(@el)
@
hide: () ->
if not @el.is(':visible')
return null
promise = $.Deferred (dfd) => @el.fadeOut('fast', dfd.resolve)
promise.promise()
show: () ->
if @el.is(':visible')
return
promise = $.Deferred (dfd) => @el.fadeIn('fast', dfd.resolve)
promise.promise()
class ProductListView extends _BaseView
id: 'productlistview'
template: $("#store_index_template").html()
initialize: (options) ->
@constructor.__super__.initialize.apply @, [options]
@collection.bind 'reset', _.bind(@render, @)
render: () ->
@el.html(_.template(@template, {'products': @collection.toJSON()}))
@
class ProductView extends _BaseView
id: 'productitemview'
template: $("#store_item_template").html()
initialize: (options) ->
@constructor.__super__.initialize.apply @, [options]
@itemcollection = options.itemcollection
@item = @itemcollection.getOrCreateItemForProduct @model
@
events:
"keypress .uqf" : "updateOnEnter"
"click .uq" : "update"
update: (e) ->
e.preventDefault()
@item.update parseInt(@$('.uqf').val())
updateOnEnter: (e) ->
if (e.keyCode == 13)
@update e
render: () ->
@el.html(_.template(@template, @model.toJSON()));
@
class CartWidget extends Backbone.View
el: $('.cart-info')
template: $('#store_cart_template').html()
initialize: () ->
@collection.bind('change', _.bind(@render, @));
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(@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();

270
store.js
View File

@ -1,270 +0,0 @@
(function() {
var BackboneStore, CartWidget, Item, ItemCollection, Product, ProductCollection, ProductListView, ProductView, _BaseView;
var __hasProp = Object.prototype.hasOwnProperty, __extends = 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;
}, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
Product = (function() {
__extends(Product, Backbone.Model);
function Product() {
Product.__super__.constructor.apply(this, arguments);
}
return Product;
})();
ProductCollection = (function() {
__extends(ProductCollection, Backbone.Collection);
function ProductCollection() {
ProductCollection.__super__.constructor.apply(this, arguments);
}
ProductCollection.prototype.model = Product;
ProductCollection.prototype.initialize = function(models, options) {
this.url = options.url;
return this;
};
ProductCollection.prototype.comparator = function(item) {
return item.get('title');
};
return ProductCollection;
})();
Item = (function() {
__extends(Item, Backbone.Model);
function Item() {
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('change', this);
};
Item.prototype.price = function() {
return this.get('product').get('price') * this.get('quantity');
};
return Item;
})();
ItemCollection = (function() {
__extends(ItemCollection, Backbone.Collection);
function ItemCollection() {
ItemCollection.__super__.constructor.apply(this, arguments);
}
ItemCollection.prototype.model = Item;
ItemCollection.prototype.getOrCreateItemForProduct = function(product) {
var i, pid;
pid = product.get('id');
i = this.detect(function(obj) {
return obj.get('product').get('id') === pid;
});
if (i) {
return i;
}
i = new Item({
product: product,
quantity: 0
});
this.add(i, {
silent: true
});
return i;
};
ItemCollection.prototype.getTotalCount = function() {
var addup;
addup = function(memo, obj) {
return obj.get('quantity') + memo;
};
return this.reduce(addup, 0);
};
ItemCollection.prototype.getTotalCost = function() {
var addup;
addup = function(memo, obj) {
return obj.price() + memo;
};
return this.reduce(addup, 0);
};
return ItemCollection;
})();
_BaseView = (function() {
__extends(_BaseView, Backbone.View);
function _BaseView() {
_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 promise;
if (!this.el.is(':visible')) {
return null;
}
promise = $.Deferred(__bind(function(dfd) {
return this.el.fadeOut('fast', dfd.resolve);
}, this));
return promise.promise();
};
_BaseView.prototype.show = function() {
var promise;
if (this.el.is(':visible')) {
return;
}
promise = $.Deferred(__bind(function(dfd) {
return this.el.fadeIn('fast', dfd.resolve);
}, this));
return promise.promise();
};
return _BaseView;
})();
ProductListView = (function() {
__extends(ProductListView, _BaseView);
function ProductListView() {
ProductListView.__super__.constructor.apply(this, arguments);
}
ProductListView.prototype.id = 'productlistview';
ProductListView.prototype.template = $("#store_index_template").html();
ProductListView.prototype.initialize = function(options) {
this.constructor.__super__.initialize.apply(this, [options]);
return this.collection.bind('reset', _.bind(this.render, this));
};
ProductListView.prototype.render = function() {
this.el.html(_.template(this.template, {
'products': this.collection.toJSON()
}));
return this;
};
return ProductListView;
})();
ProductView = (function() {
__extends(ProductView, _BaseView);
function ProductView() {
ProductView.__super__.constructor.apply(this, arguments);
}
ProductView.prototype.id = 'productitemview';
ProductView.prototype.template = $("#store_item_template").html();
ProductView.prototype.initialize = function(options) {
this.constructor.__super__.initialize.apply(this, [options]);
this.itemcollection = options.itemcollection;
this.item = this.itemcollection.getOrCreateItemForProduct(this.model);
return this;
};
ProductView.prototype.events = {
"keypress .uqf": "updateOnEnter",
"click .uq": "update"
};
ProductView.prototype.update = function(e) {
e.preventDefault();
return this.item.update(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;
})();
CartWidget = (function() {
__extends(CartWidget, Backbone.View);
function CartWidget() {
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('change', _.bind(this.render, 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;
})();
BackboneStore = (function() {
__extends(BackboneStore, Backbone.Router);
function BackboneStore() {
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(this.hideAllViews()).then(function() {
return view.show();
});
};
BackboneStore.prototype.product = function(id) {
var product, view, _base, _name;
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;
})();
$(document).ready(function() {
new BackboneStore();
return 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;
};
})();