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
*~
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
.PHONY: setup
NOTANGLE= notangle
NOWEAVE= noweave
@ -7,28 +8,58 @@ STYLUS= stylus
HAML= haml
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
$(HAML) --unix-newlines --no-escape-attrs --double-quote-attribute $*.haml > $*.html
setup:
npm install
bower install jquery underscore backbone
mkdir -p htdocs/lib
index.haml: backbonestore.nw
$(NOTANGLE) -c -R$@ $< > $*.haml
htdocs/lib/underscore.js:
mkdir -p htdocs/lib
cp bower_components/underscore/underscore.js htdocs/lib
jsonstore.css: jsonstore.styl
$(STYLUS) $*.styl
htdocs/lib/jquery.js:
mkdir -p htdocs/lib
cp bower_components/jquery/dist/jquery.js htdocs/lib
jsonstore.styl: backbonestore.nw
$(NOTANGLE) -c -R$@ $< > $@
htdocs/lib/backbone.js:
cp bower_components/backbone/backbone.js htdocs/lib
store.js: store.coffee
$(COFFEE) --compile $<
work:
mkdir -p work
store.coffee: backbonestore.nw
$(NOTANGLE) -c -R$@ $< > $@
docs:
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:
$(NOWEAVE) -x -delay $*.nw > $*.tex #$
$(NOWEAVE) -x -delay $*.nw > $*.tex
.tex.pdf:
xelatex $*.tex; \
@ -38,8 +69,9 @@ store.coffee: backbonestore.nw
done
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
- 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}
\usepackage{noweb}
\usepackage[T1]{fontenc}
\usepackage{hyperref}
\usepackage{fontspec, xunicode, xltxtra}
\setromanfont{Georgia}
\begin{document}
% Generate code and documentation with:
@ -13,39 +15,47 @@
\section{Introduction}
This is version 2.0\textit{bis} of \textbf{The Backbone Store}, a
brief tutorial on using [[backbone.js]]. It uses the original
Backbone Store as a reference, but using a modern suite of tools:
Coffeescript (version 1.1), HAML (Ruby version 3.1.1), and Stylus.
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://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.
\nwanchorto{http://documentcloud.github.com/backbone/}{Backbone.js} is a
popular Model-View-Controller (MVC) library that provides a framework
for creating data-rich, single-page web applications. It provides (1) a
two-layer scheme for separating data from presentation, (2) a means of
automatically synchronizing data with a server in a RESTful manner, and
(3) a mechanism for making some views bookmarkable and navigable.
Backbone is dependent upon \nwanchorto{http://jquery.com}{jQuery} and
\nwanchorto{http://underscorejs.org}{Underscore}. Both of those
dependencies are encoded into the build process automatically.
The version of this tutorial you are currently reading uses
Coffeescript, Stylus, and Ruby's HAML. The purpose of this tutorial is
to show how to use Backbone in a modern, constrained programming
environment.
\nwanchorto{http://jashkenas.github.com/coffee-script/}{CoffeeScript} is
a lovely little languange that compiles into Javascript. It provides a
class-based architecture (that is compatible with Backbone), has an
elegant structure for defining functions and methods, and strips out as
much extraneous punctuation as possible. Some people find the
whitespace-as-semantics a'la Python offputting, but most disciplined
developers already indent appropriately anyway.
\nwanchorto{http://haml-lang.com/}{HAML} is a languange that compiles
into HTML. Like CoffeeScript, it uses whitespace for semantics:
indentation levels correspond to HTML containerizations. It allows
you to use rich scripting while preventing heirarchy misplacement
mistakes. Its shorthand also makes writing HTML much faster.
indentation levels correspond to HTML containerizations. It allows you
to use rich scripting while preventing heirarchy misplacement mistakes.
Its shorthand also makes writing HTML much faster.
\nwanchorto{https://github.com/LearnBoost/stylus/}{Stylus} is
languange that compiles into CSS. Like CoffeeScript and HAML, it uses
whitespace for semantics. It also provides mixins and functions that
allow you to define visual styles such as borders and gradients, and
mix them into specific selectors in the CSS rather than having to
write them into the HTML.
\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.
\nwanchorto{https://github.com/LearnBoost/stylus/}{Stylus} is languange
that compiles into CSS. Like CoffeeScript and HAML, it uses whitespace
for semantics. It also provides mixins and functions that allow you to
define visual styles such as borders and gradients, and mix them into
specific selectors in the CSS rather than having to write them into the
HTML.
There are a number of other good tutorials for Backbone (See:
\nwanchorto{http://www.plexical.com/blog/2010/11/18/backbone-js-tutorial/}{Meta
@ -56,7 +66,7 @@ There are a number of other good tutorials for Backbone (See:
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
and Django}. However, a couple of years ago I was attempting to
learn Sammy.js, a library very similar to Backbone, and they had a
nifty tutorial called
\nwanchorto{http://code.quirkey.com/sammy/tutorials/json_store_part1.html}{The
@ -79,151 +89,154 @@ 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.
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}
\subsection{The Store: What We're Going to Build}
To demonstrate the basics of Backbone, I'm going to create a simple
one-page application, a store for record albums, with two unique
views: a list of all products and a product detail view. I will also
put a shopping cart widget on the page that shows the user how many
products he or she has dropped into the cart. I'll use jQuery's
[[fadeIn()]] and [[fadeOut()]] features to transition between the
catalog and the product detail pages.
one-page application, a store for record albums, with two unique views:
a list of all products and a product detail view. I will also put a
shopping cart widget on the page that shows the user how many products
he or she has dropped into the cart. I'll use some simple animations to
transition between the catalog and the product detail pages.
\subsection{Models, Collections, and Controllers}
Backbone's data layer provides two classes, [[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.
Backbone's data layer provides two classes, [[Collection]] and
[[Model]].
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.
Every web application has data, often tabular data. Full-stack web
developers are (or ought to be) familiar with the \textit{triples} of
addressing objects on the web: Table URL → Row → Field, or Page URL →
HTML Node → Content. The [[Collection]] object represents just that: a
collection of similar items. The [[Model]] represents exactly one of
those items.
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.)
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.
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.
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 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.
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>>=
<<models>>=
class Product extends Backbone.Model
class Item extends Backbone.Model
update: (amount) ->
return if amount == @get 'quantity'
@set {quantity: amount}, {silent: true}
@collection.trigger 'update', @
price: () ->
@get('product').get('price') * @get('quantity')
@
The methods [[.get(item)]] and [[.set(item, value)]] are at the heart of
Backbone.Model. They're how you set individual attributes on the
object being manipulated. Notice how I can 'get' the product, which is
a Backbone.Model, and then 'get' its price.
Backbone supplies its own event management toolkit. Changing a model
triggers various events, none of which matter here in this context so I
silence the event, but then I tell the Item's Backbone.Collection that
the Model has changed. Events are the primary way in which Backbone
objects interact, so understanding them is key to using Backbone
correctly.
Collections, like Models, are just objects you can (and often must)
extend to support your application's needs. Just as a Model has
\texttt{.get()} and \texttt{.set()}, a Collection has [[.add(item)]] and
[[.remove(id)]] as methods. Collections have a lot more than that.
Both Models and Collections also have [[.fetch()]] and [[.save()]]. If
either has a URL, these methods allow the collection to represent data
on the server, and to save that data back to the server. The default
method is a simple JSON object representing either a Model's attributes,
or a JSON list of the Collection's models' attributes.
The [[Product.Collection]] will be loading it's list of albums via these
methods to (in our case) static JSON back-end.
<<product collection>>=
class ProductCollection extends Backbone.Collection
model: Product
initialize: (models, options) ->
@url = options.url
@
comparator: (item) ->
item.get('title')
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
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.
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.
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>>=
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>>=
<<cart collection>>=
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
@
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: () ->
addup = (memo, obj) -> obj.get('quantity') + memo
addup = (memo, obj) -> memo + obj.get 'quantity'
@reduce addup, 0
getTotalCost: () ->
addup = (memo, obj) ->obj.price() + memo
@reduce(addup, 0);
addup = (memo, obj) -> memo + obj.price()
@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,
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.
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.
@ -263,13 +275,17 @@ class _BaseView extends Backbone.View
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.
views to identify into which DOM object the View root element will be
rendered. The second defines a common class we will use for the purpose
of identifying these views to jQuery. Backbone automatically creates a
new [[DIV]] object with the class 'viewport' when a view constructor is
called. It will be our job to attach that [[DIV]] to the DOM. In the
HTML, you will see the [[DIV\#main]] object where most of the work will
be rendered.
As an alternative, the viewport object may already exist, in which case
you just find it with a selector, and the view attaches itself to that
DOM object from then on.
<<base view>>=
initialize: () ->
@ -290,8 +306,8 @@ anything.
Next, we will define the hide and show functions.
Note that in coffeescript, the [[=>]] operator completely replaces the
[[_.bind()]] function provided by underscore.
Note that in Coffeescript, the [[=>]] operator completely replaces the
[[.bind()]] function provided by modern Javascript.
<<base view>>=
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
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.
\textbf{Deferred} is a feature of jQuery called ``promises''. It is a
different mechanism for invoking callbacks by attaching attributes and
behavior to the callback function. By using this, we can say thing like
``\textit{When} everything is hidden (when every deferred returned from
\textbf{hide} has been resolved), \textit{then} show the appropriate
viewport.'' Deferreds are incredibly powerful, and this is a small
taste of what can be done with them.
Before we move on, let's take a look at the HAML we're going to use
for our one-page application. The code below compiles beautifully
into the same HTML seen in the original Backbone Store.
Before we move on, let's take a look at the HAML we're going to use for
our one-page application. The code below compiles beautifully into the
same HTML seen in the original Backbone Store.
<<index.haml>>=
!!! 5
@ -338,9 +354,9 @@ into the same HTML seen in the original Backbone Store.
The Backbone Store
.cart-info
#main
%script{:src => "jquery-1.6.2.min.js", :type => "text/javascript"}
%script{:src => "underscore.js", :type => "text/javascript"}
%script{:src => "backbone.js", :type => "text/javascript"}
%script{:src => "lib/jquery.js", :type => "text/javascript"}
%script{:src => "lib/underscore.js", :type => "text/javascript"}
%script{:src => "lib/backbone.js", :type => "text/javascript"}
%script{:src => "store.js", :type => "text/javascript"}
@
@ -360,10 +376,10 @@ Backbone users frequently have: \textit{What is \texttt{render()}
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.
That call to [[.prototype]] is a Javascript idiom for calling a method
on the parent object. It is, as far as anyone knows, the only way to
invoke a superclass method if it has been redefined in a subclass. It
is rather ugly, but useful.
<<product list view>>=
class ProductListView extends _BaseView
@ -371,7 +387,7 @@ class ProductListView extends _BaseView
template: $("#store_index_template").html()
initialize: (options) ->
@constructor.__super__.initialize.apply @, [options]
_BaseView.prototype.initialize.apply @, [options]
@collection.bind 'reset', _.bind(@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
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.
The only trickiness here is twofold: the means by which one calls the
method of a parent class from a child class via Backbone's class
heirarchy, and keeping track of the itemcollection object, so we can add
and change items as needed.
<<product detail view>>=
class ProductView extends _BaseView
id: 'productitemview'
template: $("#store_item_template").html()
initialize: (options) ->
@constructor.__super__.initialize.apply @, [options]
@itemcollection = options.itemcollection
@item = @itemcollection.getOrCreateItemForProduct @model
@
_BaseView.prototype.initialize.apply @, [options]
@itemcollection = options.itemcollection
@item = @itemcollection.getOrCreateItemForProduct @model
@
%$
@ -691,9 +705,11 @@ Here's the entirety of the program. Coffeescript provides its own
namespace wrapper:
<<store.coffee>>=
<<product models>>
<<models>>
<<cart models>>
<<product collection>>
<<cart collection>>
<<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;
};
})();