diff --git a/Makefile b/Makefile index 64f4245..baed8bf 100644 --- a/Makefile +++ b/Makefile @@ -1,58 +1,78 @@ -.SUFFIXES: .nw .js .pdf .html .tex +.PHONY: setup store serve NOTANGLE= notangle NOWEAVE= noweave -ECHO= /bin/echo +ECHO= echo -all: index.html store.js +LIBS:= htdocs/lib/underscore.js htdocs/lib/jquery.js htdocs/lib/backbone.js -.nw.html: - $(NOWEAVE) -filter l2h -delay -x -index -autodefs c -html $*.nw > $*.html +all: htdocs/index.html htdocs/store.js htdocs/jsonstore.css htdocs/data/items.json + @if [ ! -e "./htdocs/lib" ]; then \ + echo "Please do 'make setup' before continuing"; \ + exit 1; \ + fi -.nw.tex: - $(NOWEAVE) -x -delay $*.nw > $*.tex #$ +serve: all + $(COFFEE) ./bin/autoreload -.tex.pdf: - xelatex $*.tex; \ - while grep -s 'Rerun to get cross-references right' $*.log; \ +store: all + +htdocs/lib: + mkdir -p htdocs/lib + +htdocs/lib/underscore.js: htdocs/lib + cp bower_components/underscore/underscore.js htdocs/lib + +htdocs/lib/jquery.js: htdocs/lib + cp bower_components/jquery/dist/jquery.js htdocs/lib + +htdocs/lib/backbone.js: + cp bower_components/backbone/backbone.js htdocs/lib + +install: + npm install + ./node_modules/bower/bin/bower install jquery underscore backbone + +setup: install $(LIBS) + +docs: + mkdir -p docs + +htdocs/index.html: src/backbonestore.nw + $(NOTANGLE) -c -Rindex.haml src/backbonestore.nw > htdocs/index.html + +htdocs/jsonstore.css: src/backbonestore.nw + $(NOTANGLE) -c -Rjsonstore.css src/backbonestore.nw > htdocs/jsonstore.css + +htdocs/store.js: src/backbonestore.nw + $(NOTANGLE) -c -Rstore.js src/backbonestore.nw > htdocs/store.js + +docs/backbonestore.tex: docs src/backbonestore.nw + ${NOWEAVE} -x -delay src/backbonestore.nw > docs/backbonestore.tex + +docs/backbonestore.pdf: docs/backbonestore.tex + xelatex docs/backbonestore.tex; \ + while grep -s 'Rerun to get cross-references right' ./backbonestore.log; \ do \ - xelatex *$.tex; \ + xelatex docs/backbonestore.tex; \ done + mv backbonestore.pdf docs + rm -f ./backbonestore.log ./backbonestore.aux ./backbonestore.out -.nw.js: - @ $(ECHO) $(NOTANGLE) -c -R$@ $< - @ - $(NOTANGLE) -c -R$@ $< > $*.nw-js-tmp - @ if [ -s "$*.nw-js-tmp" ]; then \ - mv $*.nw-js-tmp $@; \ - else \ - echo "$@ not found in $<"; \ - rm $*.nw-js-tmp; \ - fi +pdf: docs/backbonestore.pdf -store.js: backbonestore.nw - @ $(ECHO) $(NOTANGLE) -c -R$@ $< - @ - $(NOTANGLE) -c -R$@ $< > $*.nw-html-tmp - @ if [ -s "$*.nw-html-tmp" ]; then \ - mv $*.nw-html-tmp $@; \ - else \ - echo "$@ not found in $<"; \ - rm $*.nw-tmp; \ - fi - -index.html: backbonestore.nw - @ $(ECHO) $(NOTANGLE) -c -R$@ $< - @ - $(NOTANGLE) -c -R$@ $< > $*.nw-html-tmp - @ if [ -s "$*.nw-html-tmp" ]; then \ - mv $*.nw-html-tmp $@; \ - else \ - echo "$@ not found in $<"; \ - rm $*.nw-tmp; \ - fi +docs/backbonestore.html: docs src/backbonestore.nw + $(NOWEAVE) -filter l2h -delay -x -autodefs c -html src/backbonestore.nw > docs/backbonestore.html +html: docs/backbonestore.html clean: - - rm -f *.tex *.dvi *.aux *.toc *.log *.out *.html *.js + - rm -f htdocs/*.* docs/*.tex docs/*.dvi docs/*.aux docs/*.toc docs/*.log docs/*.out + +distclean: clean + - rm -fr ./htdocs/lib + +realclean: distclean + - rm -fr docs -realclean: clean - - rm -f *.pdf diff --git a/README b/README deleted file mode 100644 index 97d5045..0000000 --- a/README +++ /dev/null @@ -1,35 +0,0 @@ -The Backbone Store is a simple demonstration application, a Backbone.js -version of the Sammy.js tutorial. - -## Installation - -The Backbone store must be installed under a webserver in order to -operate correctly. Chrome, especially, will not initialize the -application from the filesystem. Just unpack it in a target directory -of your choosing and then browse to that directory. - -## Branches - -There are two major development branches for The Backbone Store. - -Branch 'master' uses HTML, CSS, and Javascript. - -Branch 'modern' uses HAML, Stylus, and Coffee. - -## Copyright - -Store.js is entirely my own work, and is Copyright (c) 2010 Elf -M. Sternberg. Included libraries are covered by their respective -copyright holders, and are used with permission of the licenses -included. Store.js is intended for educational purposes only, rather -than to be working code, and is hereby licensed under the Creative -Commons Attribution Non-Commercial Share Alike (by-nc-sa) licence. - -The images contained herein are derivative works of photographs -licensed under Creative Commons licences for non-commercial purposes. - -## Contribution - -Please look in backbonestore.nw for the base code. Backbonestore.nw -is produced using the Noweb Literate Programming toolkit by Norman -Ramsey (http://www.cs.tufts.edu/~nr/noweb/). diff --git a/README.md b/README.md new file mode 100644 index 0000000..3225c6a --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# About + +The Backbone Store is a tutorial and demonstration application for the +BackboneJS framework. + +## Installation + +After checking out the source code, type + +$ make setup all serve + +This will automatically run the NPM and Bower install scripts, placing +the correct libraries into the target tree, build the actual application +from the original source material, and start a server. + +## Requirements + +The build tool relies upon GNU Make, node-js, and the Ruby HAML +application. It also uses the NoWeb Literate Programming documentation +tools, and building the documentation from source requires Xelatex be +installed as well. + +The command 'make serve' probably only works under a fairly modern +Linux, as it's dependent upon the kernel's inotify facility. + +## Branches + +There are two major development branches for The Backbone Store. + +Branch 'master' uses HTML, CSS, and Javascript. + +Branch 'modern' uses HAML, Stylus, and Coffee. + +## Changelog + +### Changes from 2.0 + +Version 3.0 has the following notable changes: + * Replace __super__ with prototype + * Replace Backbone-generated internal IDs with supplied IDs + * Updates the use of Deferred + * Updates to the current Underscore Template mechanism + +### Changes from 1.0 + +Version 2.0 has the following notable changes: + * Use of jQuery animations + * Better Styling + * Proper event management. Version 1.0 was just doin' it WRONG. + +## Copyright + +Store.js is entirely my own work, and is Copyright (c) 2010 Elf +M. Sternberg. Included libraries are covered by their respective +copyright holders, and are used with permission of the licenses +included. Store.js is intended for educational purposes only, rather +than to be working code, and is hereby licensed under the Creative +Commons Attribution Non-Commercial Share Alike (by-nc-sa) licence. + +The images contained herein are derivative works of photographs +licensed under Creative Commons licences for non-commercial purposes. + +## Contribution + +Please look in backbonestore.nw for the base code. Backbonestore.nw +is produced using the Noweb Literate Programming toolkit by Norman +Ramsey (http://www.cs.tufts.edu/~nr/noweb/). diff --git a/src/backbonestore.nw b/src/backbonestore.nw index 42975d7..1ff7248 100644 --- a/src/backbonestore.nw +++ b/src/backbonestore.nw @@ -1,8 +1,10 @@ -% -*- Mode: noweb; noweb-code-mode: javascript-mode ; noweb-doc-mode: latex-mode -*- +% -*- Mode: poly-noweb+javascript -*- \documentclass{article} \usepackage{noweb} \usepackage[T1]{fontenc} \usepackage{hyperref} +\usepackage{fontspec, xunicode, xltxtra} +\setromanfont{Georgia} \begin{document} % Generate code and documentation with: @@ -13,8 +15,10 @@ \section{Introduction} -This is version 2.0 of \textbf{The Backbone Store}, a brief tutorial -on using [[backbone.js]]. +This is version 3.0 of \textbf{The Backbone Store}, a brief tutorial on +using [[backbone.js]]. The version you are currently reading has been +tested with the latest versions of the supporting software as of April, +2016. \nwanchorto{http://documentcloud.github.com/backbone/}{Backbone.js} is a popular Model-View-Controller (MVC) library that provides a @@ -45,184 +49,211 @@ In the spirit of The JSON Store, I present The Backbone Store. A note: this article was written with the \nwanchorto{http://en.wikipedia.org/wiki/Literate_programming}{Literate - Programming} toolkit +Programming} toolkit \nwanchorto{http://www.cs.tufts.edu/~nr/noweb/}{Noweb}. Where you see -something that looks like \\<\\\\>, it's a placeholder for code -described elsewhere in the document. Placeholders with an equal sign -at the end of them indicate the place where that code is defined. The -link (U->) indicates that the code you're seeing is used later in the -document, and (<-U) indicates it was used earlier but is being defined -here. +something that looks like $\langle\langle$this$\rangle\rangle$, it's a +placeholder for code described elsewhere in the document. Placeholders +with an equal sign at the end of them indicate the place where that code +is defined. The link (U->) indicates that the code you're seeing is +used later in the document, and (<-U) indicates it was used earlier but +is being defined here. \subsection{Revision} -This is version 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. +The Product literally has nothing to modify. -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. +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: -<>= - var Product = Backbone.Model.extend({}) +<>= +var Product = Backbone.Model.extend({}); - var ProductCollection = Backbone.Collection.extend({ - model: Product, - - initialize: function(models, options) { - this.url = options.url; - }, - - comparator: function(item) { - return item.get('title'); +var Item = Backbone.Model.extend({ + update: function(amount) { + if (amount === this.get('quantity')) { + return; } - }); + this.set({quantity: amount}, {silent: true}); + return this.collection.trigger('update', this); + }, + + price: function() { + return this.get('product').get('price') * this.get('quantity'); + } +}); @ +The methods [[.get(item)]] and [[.set(item, value)]] are at the heart of +Backbone.Model. They're how you set individual attributes on the object +being manipulated. Notice how I can 'get' the product, which is a +Backbone.Model, and then 'get' its price. +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. For this program, it is the collection as a +whole whose value matters, because that collection as a whole represents +our shopping cart. Events are the primary way in which Backbone objects +interact, so understanding them is key to using Backbone correctly. -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. +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. -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. +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. -Also, it would be nice to know the total price of the Item. +The [[Product.Collection]] will be loading its list of albums via these +methods to (in our case) static JSON back-end. -<>= - var Item = Backbone.Model.extend({ - update: function(amount) { - this.set({'quantity': amount}, {silent: true}); - this.collection.trigger('change', this); - }, - price: function() { - console.log(this.get('product').get('title'), this.get('quantity')); - return this.get('product').get('price') * this.get('quantity'); - } - }); +<>= +var ProductCollection = Backbone.Collection.extend({ + model: Product, + initialize: function(models, options) { + return this.url = options.url; + }, + comparator: function(item) { + return item.get('title'); + } +}); @ -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 [[.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 [[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. +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. -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. +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. -<>= - var ItemCollection = Backbone.Collection.extend({ - model: Item, +First, we ensure that if we don't receive an amount, we at least provide +a valid \textit{numerical} value to our code. The [[.detect()]] method +lets us find an object in our Collection using a function to compare +them; it returns the first object that matches. - getOrCreateItemForProduct: function(product) { - var i, - pid = product.get('id'), - o = this.detect(function(obj) { - return (obj.get('product').get('id') == pid); - }); - if (o) { - return o; - } - i = new Item({'product': product, 'quantity': 0}) - this.add(i, {silent: true}) - return i; - }, +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. - getTotalCount: function() { - return this.reduce(function(memo, obj) { - return obj.get('quantity') + memo; }, 0); - }, - getTotalCost: function() { - return this.reduce(function(memo, obj) { - return obj.price() + memo; }, 0); +<>= +var ItemCollection = Backbone.Collection.extend({ + model: Item, + + updateItemForProduct: function(product, amount) { + amount = amount != null ? amount : 0; + var pid = product.get('id'); + var item = this.detect(function(obj) { + return obj.get('product').get('id') === pid; + }); + if (item) { + item.update(amount); + return item; } - }); + return this.add({ + product: product, + quantity: amount + }); + }, @ +And finally, two methods to add up how many objects are in your cart, +and the total price. The first line creates a function to get the +number for a single object and add it to a memo. The second line uses +the [[.reduce()]] method, which goes through each object in the +collection and runs the function, passing the results of each run to the +next as the memo. + +<>= + getTotalCount: function() { + var addup = function(memo, obj) { + return memo + obj.get('quantity'); + }; + return this.reduce(addup, 0); + }, + + getTotalCost: function() { + var addup = function(memo, obj) { + return memo + obj.price(); + }; + return this.reduce(addup, 0); + } +}); + +@ + + \subsection {Views} Backbone Views are simple policy objects. They have a root DOM element, the contents of which they manipulate and to which they listen for events, and a model or collection they represent within that element. Views are not rigid; it's just Javascript and the DOM, -and you can hook external events as needed. +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, if you pass a model or collection to a View, that View +becomes sensitive to events \textit{within its model or collection}, and +can respond to changes automatically. This way, if you have a rich data +ecosystem, when changes to one data item results in a cascade of changes +throughout your datasets, the views will receive ``change'' events and +can update themselves accordingly. + +In a way, a View can be thought of as two separate but important +sub-programs, each based on events. The first listens to events from +the DOM, and forwards data-changing events to associated models or +collections. The second listens to events from data objects and +re-draws the View's contents when the data changes. Keeping these +separate in your mind will help you design Backbone applications +successfully. I will show how this works with the shopping cart widget. @@ -237,179 +268,205 @@ can change the class and highlight an entry on a nav bar; you can add and remove tabs from the top of the viewport as needed. <>= - var _BaseView = Backbone.View.extend({ - parent: $('#main'), - className: 'viewport', +var BaseView = Backbone.View.extend({ + parent: $('#main'), + className: 'viewport', -@ + initialize: function(options) { + Backbone.View.prototype.initialize.apply(this, arguments); + this.$el.hide(); + this.parent.append(this.el); + }, + +@ The above says that I am creating a class called \texttt{BaseView} and defining two fields. The first, 'parent', will be used by all child -views to identify into which DOM object the View's root element will -be rendered. The second defines a common class we will use for the -purpose of identifying these views to jQuery. Backbone automatically -creates a new [[DIV]] object with the class 'viewport' when a view -constructor is called. It will be our job to attach that [[DIV]] to -the DOM. In the HTML, you will see the [[DIV\#main]] object where most -of the work will be rendered. +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. -<>= - initialize: function() { - this.el = $(this.el); - this.el.hide(); - this.parent.append(this.el); - return this; - }, +As an alternative, the viewport object may already exist, in which case +you just find it with a selector, and the view attaches itself to that +DOM object from then on. In older versions of the Backbone Store, we +used to assign [[@el]] to a jQuery-wrapped version of the element; +that's no longer necessary, as Backbone provides you with its own +version automatically in [[@$el]]. -@ -%$ +The 'parent' field is something I created for my own use, since I intend +to have multiple child objects share the same piece of real-estate. The +'className' field is something Backbone automatically applies to the +generated [[DIV]] at construction time. If you pass in an existing +element at construction time for the View to use (which is not an +uncommon use case!), Backbone will \textit{not} apply the 'className' to +it; you'll have to do that yourself. -The method above ensures that the element is rendered, but not -visible, and contained within the [[DIV\#main]]. Note also that -the element is not a sacrosanct object; the Backbone.View is more a -collection of standards than a mechanism of enforcement, and so -defining it from a raw DOM object to a jQuery object will not break +I use the [[initialize]] method above to ensure that the element is +rendered, but not visible, and contained within the [[DIV#main]]. Note +also that the element is not a sacrosanct object; the Backbone.View is +more a collection of standards than a mechanism of enforcement, and so +defining it from a raw DOM object or a jQuery object will not break anything. -Next, we will define the hide and show functions: +Next, we will define the hide and show functions. <>= - hide: function() { - if (this.el.is(":visible") === false) { - return null; - } - promise = $.Deferred(_.bind(function(dfd) { - this.el.fadeOut('fast', dfd.resolve)}, this)); - return promise.promise(); - }, - - show: function() { - if (this.el.is(':visible')) { - return; - } - promise = $.Deferred(_.bind(function(dfd) { - this.el.fadeIn('fast', dfd.resolve) }, this)) - return promise.promise(); + hide: function() { + var dfd = $.Deferred(); + if (!this.el.is(':visible')) { + return dfd.resolve(); } - }); + this.el.fadeOut('fast', function() { + return dfd.resolve(); + }); + return dfd.promise(); + }, -@ + show: function() { + var dfd = $.Deferred(); + if (this.el.is(':visible')) { + return dfd.resolve(); + } + this.el.fadeIn('fast', function() { + return dfd.resolve(); + }); + return dfd.promise(); + } +}); -\textbf{Deferred} is a new feature of jQuery. It is a different -mechanism for invoking callbacks by attaching attributes and behavior -to the callback function. By using this, we can say thing like -``\textit{When} everything is hidden (when every deferred returned -from \textbf{hide} has been resolved), \textit{then} show the -appropriate viewport.'' Deferreds are incredibly powerful, and this -is a small taste of what can be done with them. +@ -Before we move on, let's take a look at the HTML we're going to use -for our one-page application: +\textbf{Deferred} is a feature of jQuery called ``promises''. It is a +different mechanism for invoking callbacks by attaching attributes and +behavior to the callback function. By using this, we can say thing like +``\textit{When} everything is hidden (when every deferred returned from +\textbf{hide} has been resolved), \textit{then} show the appropriate +viewport.'' Deferreds are incredibly powerful, and this is a small +taste of what can be done with them. + +Before we move on, let's take a look at the HTML we're going to use for +our one-page application. <>= - - - The Backbone Store - - - <> - <> - <> + + The Backbone Store + + <> + <> + <> - -
- - -
-
- - - - - + + +
+ +
+
+ + + + + -@ + +@ It's not much to look at, but already you can see where that -[[DIV\#main]] goes, as well as where we are putting our templates. -The [[DIV\#main]] will host a number of viewports, only one of -which will be visible at any given time. +[[DIV\#main]] goes, as well as where we are putting our templates. The +[[DIV\#main]] will host a number of viewports, only one of which will be +visible at any given time. -Our first view is going to be the product list view, named, well, -guess. Or just look down a few lines. +Our first view is going to be the product list view, named, well, guess. +Or just look down a few lines. -This gives us a chance to discuss one of the big confusions new -Backbone users frequently have: \textit{What is \texttt{render()} - for?}. Render is not there to show or hide the view. -\texttt{Render()} is there to \textit{change the view when the - underlying data changes}. It renders the data into a view. In our -functionality, we use the parent class's \texttt{show()} and -\texttt{hide()} methods to actually show the view. +This gives us a chance to discuss one of the big confusions new Backbone +users frequently have: \textit{What is \texttt{render()} for?}. Render +is not there to show or hide the view. \texttt{Render()} is there to +\textit{change the view when the underlying data changes}. It renders +the data into a view. In our functionality, we use the parent class's +\texttt{show()} and \texttt{hide()} methods to actually show the view. -That call to [[\_super\_]] is a Backbone idiom for calling a method on -the parent object. It is, as far as anyone knows, the only way to -invoke a superclass method if it has been redefined in a subclass. -It is rather ugly, but useful. +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. <>= - var ProductListView = _BaseView.extend({ - id: 'productlistview', - template: $("#store_index_template").html(), +var ProductListView = BaseView.extend({ + id: 'productlistview', + template: _.template($("#store_index_template").html()), - initialize: function(options) { - this.constructor.__super__.initialize.apply(this, [options]) - this.collection.bind('reset', _.bind(this.render, this)); - }, + initialize: function(options) { + BaseView.prototype.initialize.apply(this, arguments); + this.collection.bind('reset', this.render.bind(this)); + }, - render: function() { - this.el.html(_.template(this.template, - {'products': this.collection.toJSON()})) - return this; - } - }); + render: function() { + this.el.html(this.template({ + 'products': this.collection.toJSON() + }); + return this; + } +}); -@ +@ %$ -That \texttt{\_.template()} method is provided by undescore.js, and is -a full-featured, javascript-based templating method. It's not the -fastest or the most feature-complete, but it is more than adequate for -our purposes and it means we don't have to import another library. It -vaguely resembles ERB from Rails, so if you are familiar with that, -you should understand this fairly easily. +That \texttt{\_.template()} method is provided by undescore.js, and is a +full-featured, javascript-based templating method. It's not the fastest +or the most feature-complete, but it is more than adequate for our +purposes and it means we don't have to import another library. It +vaguely resembles ERB from Rails, so if you are familiar with that, you +should understand this fairly easily. It takes a template and returns a +function ready to render the template. What we're saying here is that +we want this View to automatically re-render itself every time the given +collection changes in a significant way, using the given template, into +the given element. That's what this view ``means.'' -And here is the HTML: +There are many different ways of providing templates to Backbone. The +most common, especially for small templates, is to just include it as an +inline string inside the View. The \textit{least} common, I'm afraid, +is the one I'm doing here: using the $<$script$>$ tag with an +unusual mime type to include it with the rest of the HTML. I like this +method because it means all of my HTML is in one place. + +For much larger programs, those that use features such as +\nwanchorto{http://requirejs.org/}{Require.js}, a common technique is to +keep the HTML template fragment in its own file and to import it using +Require's ``text'' plugin. + +Here is the HTML for our home page's template: <>= - -@ -%$ + +@ One of the most complicated objects in our ecosystem is the product view. It actually does something! The prefix ought to be familiar, @@ -417,24 +474,20 @@ 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. <>= - var ProductView = _BaseView.extend({ - id: 'productitemview', - template: $("#store_item_template").html(), - initialize: function(options) { - this.constructor.__super__.initialize.apply(this, [options]) - this.itemcollection = options.itemcollection; - this.item = this.itemcollection.getOrCreateItemForProduct(this.model); - return this; - }, - -@ +class ProductView extends _BaseView + className: 'productitemview' + template: $("#store_item_template").html() + initialize: (options) -> + _BaseView.prototype.initialize.apply @, [options] + @itemcollection = options.itemcollection + +@ %$ There are certain events in which we're interested: keypresses and @@ -448,12 +501,11 @@ those that jQuery's ``delegate'' understands. As of 1.5, that seems to be just about all of them. <>= - events: { - "keypress .uqf" : "updateOnEnter", - "click .uq" : "update", - }, + events: + "keypress .uqf" : "updateOnEnter" + "click .uq" : "update" -@ +@ And now we will deal with the update. This code ought to be fairly readable: the only specialness is that it's receiving an event, and @@ -470,89 +522,72 @@ cart model, which is where it belongs: \textit{knowledge about items and each item's relationship to its collection belongs in the collection}. -Look closely at the [[update()]] method. The reference [[this.\$]] is -a special Backbone object that limits selectors to objects inside the +Look closely at the [[update()]] method. The reference [[@\$]] is a +special Backbone object that limits selectors to objects inside the element of the view. Without it, jQuery would have found the first input field of class 'uqf' in the DOM, not the one for this specific -view. [[this.\$('.uqf')]] is shorthand for [[$('uqf', this.el)]], and -helps clarify what it is you're looking for. - -%' +view. [[@\$('.uqf')]] is shorthand for [[$('uqf', @el)]], and helps +clarify what it is you're looking for. <>= - update: function(e) { - e.preventDefault(); - this.item.update(parseInt(this.$('.uqf').val())); - }, - - updateOnEnter: function(e) { - if (e.keyCode == 13) { - return this.update(e); - } - }, + update: (e) -> + e.preventDefault() + @itemcollection.updateItemForProduct @model, parseInt(@$('.uqf').val()) -@ -%$ + updateOnEnter: (e) -> + @update(e) if e.keyCode == 13 + +@ The render is straightforward: <>= - render: function() { - this.el.html(_.template(this.template, this.model.toJSON())); - return this; - } - }); + render: () -> + @el.html(_.template(@template)(@model.toJSON())); + @ -@ +@ The product detail template is fairly straightforward. There is no [[underscore]] magic because there are no loops. <>= - -@ +%script#store_item_template(type= "text/x-underscore-template") + .item-detail + .item-image + %img(src="<%= large_image %>" alt="<%= title %>")/ + .item-info + .item-artist <%= artist %> + .item-title <%= title %> + .item-price $<%= price %> + .item-form + %form(action="#/cart" method="post") + %p + %label Quantity: + %input(type="text" size="2" name="quantity" value="1" class="uqf")/ + %p + %input(type="submit" value="Add to Cart" class="uq")/ + + .item-link + %a(href="<%= url %>") Buy this item on Amazon + .back-link + %a(href="#") « Back to Items +@ So, let's talk about that shopping cart thing. We've been making the point that when it changes, when you call [[item.update]] within the product detail view, any corresponding subscribing views sholud -automatically update. +automatically update. <>= - var CartWidget = Backbone.View.extend({ - el: $('.cart-info'), - template: $('#store_cart_template').html(), - - initialize: function() { - this.collection.bind('change', _.bind(this.render, this)); - }, - -@ +class CartWidget extends Backbone.View + el: $('.cart-info') + template: $('#store_cart_template').html() + + initialize: () -> + @collection.bind 'update', @render.bind @ + +@ %$ And there is the major magic. CartWidget will be initialized with the @@ -565,26 +600,23 @@ template with the new count and cost, and then wiggle it a little to show that it did changed: <>= - render: function() { - this.el.html( - _.template(this.template, { - 'count': this.collection.getTotalCount(), - 'cost': this.collection.getTotalCost() - })).animate({paddingTop: '30px'}) - .animate({paddingTop: '10px'}); - } - }); + render: () -> + tel = @$el.html _.template(@template)({ + 'count': @collection.getTotalCount() + 'cost': @collection.getTotalCost() + }) + tel.animate({paddingTop: '30px'}).animate({paddingTop: '10px'}) + @ -@ +@ And the HTML for the template is dead simple: <>= - +%script#store_cart_template(type="text/x-underscore-template") + %p Items: <%= count %> ($<%= cost %>) -@ +@ %$ Lastly, there is the [[Router]]. In Backbone, the Router is a @@ -598,23 +630,22 @@ of the [[Views]], the [[ProductCollection]], and the [[ItemCollection]]. <>= - var BackboneStore = Backbone.Router.extend({ - views: {}, - products: null, - cart: null, +class BackboneStore extends Backbone.Router + views: {} + products: null + cart: null -@ +@ There are two events we care about: view the list, and view a detail. They are routed like this: <>= - routes: { - "": "index", - "item/:id": "product", - }, + routes: + "": "index" + "item/:id": "product" -@ +@ Like most Backbone objects, the Router has an initialization feature. I create a new, empty shopping cart and corresponding cart widget, @@ -628,23 +659,21 @@ back-end server. For that, I use the jQuery deferred again, because returns the result of an [[ajax()]] call, which is a deferred. <>= - initialize: function(data) { - this.cart = new ItemCollection(); - new CartWidget({collection: this.cart}); + initialize: (data) -> + @cart = new ItemCollection() + new CartWidget + collection: @cart - this.products = new ProductCollection([], { - url: 'data/items.json'}); - this.views = { - '_index': new ProductListView({ - collection: this.products - }) - }; - $.when(this.products.fetch({reset: true})) - .then(function() { window.location.hash = ''; }); - return this; - }, + @products = new ProductCollection [], + url: 'data/items.json' + @views = + '_index': new ProductListView + collection: @products + $.when(@products.fetch({reset: true})) + .then(() -> window.location.hash = '') + @ -@ +@ %$ There are two things to route \textit{to}, but we must also route @@ -657,32 +686,30 @@ hidden) or null. The [[_.select()]] call at the end means that this method returns only an array of deferreds. <>= - hideAllViews: function () { - return _.select( - _.map(this.views, function(v) { return v.hide(); }), - function (t) { return t != null }); - }, + hideAllViews: () -> + _.select(_.map(@views, (v) -> return v.hide()), + (t) -> t != null) -@ + +@ Showing the product list view is basically hiding everything, then -showing the index: +showing the index. The function [[$$.when]] takes arguments of what to +wait for; to make it take an array of arguments, you use the +[[.apply()]] method. <>= - index: function() { - var view = this.views['_index']; - $.when(this.hideAllViews()).then( - function() { return view.show(); }); - }, + index: () -> + view = @views['_index'] + $.when.apply($, @hideAllViews()).then(() -> view.show()) -@ -%$ +@ On the other hand, showing the product detail page is a bit trickier. In order to avoid re-rendering all the time, I am going to create a view for every product in which the user shows interest, and keep it around, showing it a second time if the user wants to see it a second -time. +time. Not that we pass it the [[ItemCollection]] instance. It uses this to create a new item, which (if you recall from our discussion of @@ -692,40 +719,39 @@ item and the item collection \textit{changes}, which in turn causes the [[CartWidget]] to update automagically as well. <>= - product: function(id) { - var product, v, view; - product = this.products.detect(function(p) { return p.get('id') == (id); }) - view = ((v = this.views)['item.' + id]) || (v['item.' + id] = ( - new ProductView({model: product, - itemcollection: this.cart}).render())); - $.when(this.hideAllViews()).then( - function() { view.show(); }); - } - }); -@ + product: (id) -> + product = @products.detect (p) -> p.get('id') == (id) + view = (@views['item.' + id] ||= new ProductView( + model: product, + itemcollection: @cart + ).render()) + $.when(@hideAllViews()).then( + () -> view.show()) + +@ %$ Finally, we need to start the program <>= - $(document).ready(function() { - new BackboneStore(); - Backbone.history.start(); - }); -@ +$(document).ready () -> + new BackboneStore(); + Backbone.history.start(); +@ %$ \section{The Program} -Here's the entirety of the program: +Here's the entirety of the program. Coffeescript provides its own +namespace wrapper: -<>= -(function() { +<>= +<> -<> - -<> +<> +<> + <> <> @@ -737,9 +763,132 @@ Here's the entirety of the program: <> <> +@ -}).call(this); -@ +\section{A Little Stylus} + +Stylus is a beautiful little language that compiles down to CSS. The +original version of The Backbone Store used the same CSS provided from +the original Sammy tutorial, but I wanted to show you this one extra +tool because it's an essential part of my kit. + +If you want rounded borders, you know that writing all that code, for +older browsers as well as modern ones, and providing it to all the +different objects you want styled that way, can be time consuming. +Stylus allows you to define a function that can be called from within +any style, thus allowing you to define the style here, and attach a +set style to a semantic value in your HTML: + +<>= +rounded(radius) + -moz-border-radius-topleft: radius + -moz-border-radius-topright: radius + -moz-border-radius-bottomleft: radius + -moz-border-radius-bottomright: radius + -webkit-border-bottom-right-radius: radius + -webkit-border-top-left-radius: radius + -webkit-border-top-right-radius: radius + -webkit-border-bottom-left-radius: radius + border-bottom-right-radius: radius + border-top-left-radius: radius + border-top-right-radius: radius + border-bottom-left-radius: radius + +background_gradient(base) + background: base + background: -webkit-gradient(linear, left top, left bottom, from(lighten(base, 20%)), to(darken(base, 20%))) + background: -moz-linear-gradient(top, lighten(base, 20%), darken(base, 20%)) + +@ + +And if you look down below you'll see the [[rounded()]] function +called for the list items, which have borders. + +One of the real beauties of Stylus is that you can contains some style +definitions within others. You can see below that the header +contains an H1, and the H1 definitions will be compiled to only apply +within the context of the header. Stylus allows you to write CSS the +way you write HTML! + +<>= +body + font-family: "Lucida Grande", Lucida, Helvetica, Arial, sans-serif + background: #FFF + color: #333 + margin: 0px + padding: 0px + + +#main + position: relative + +#header + background_gradient(#999) + margin: 0px + padding: 20px + border-bottom: 1px solid #ccc + + h1 + font-family: Inconsolata, Monaco, Courier, mono + color: #FFF + margin: 0px + + .cart-info + position: absolute + top: 0px + right: 0px + text-align: right + padding: 10px + background_gradient(#555) + color: #FFF + font-size: 12px + font-weight: bold + +img + border: 0 + +.productitemview + position: absolute + top: 0 + left: 0 + +#productlistview + position: absolute + top: 0 + left: 0 + + ul + list-style: none + +.item + float:left + width: 250px + margin-right: 10px + margin-bottom: 10px + padding: 5px + rounded(5px) + border: 1px solid #ccc + text-align:center + font-size: 12px + +.item-title + font-weight: bold + +.item-artist + font-weight: bold + font-size: 14px + +.item-detail + margin: 10px 0 0 10px + + .item-image + float:left + margin-right: 10px + + .item-info + padding: 100px 10px 0px 10px + +@ And that's it. Put it all together, and you've got yourself a working Backbone Store.