% -*- Mode: noweb; noweb-code-mode: javascript-mode ; noweb-doc-mode: latex-mode -*- \documentclass{article} \usepackage{noweb} \usepackage{hyperref} \begin{document} % Generate code and documentation with: % % noweave -filter l2h -delay -x -html backbonestore.nw | htmltoc > backbonestore.html % notangle -Rstore.js backbonestore.nw > store.js % notangle -Rindex.html backbonestore.nw > index.html \section{Introduction} I've been playing with \nwanchorto{http://documentcloud.github.com/backbone/}{Backbone.js}, a small but nifty Javascript library that provides a small Model-View-Controller framework where Models can generate events that trigger View changes, and vice versa, along with a Collections models so groups of models can cause view-level events, and a Sync library that provides a basic REST architecture for propagating client-made changes back to the server. There are a number of good tutorials for Backbone (See: \nwanchorto{http://www.plexical.com/blog/2010/11/18/backbone-js-tutorial/}{Meta Cloud}, \nwanchorto{http://andyet.net/blog/2010/oct/29/building-a-single-page-app-with-backbonejs-undersc/?utm_source=twitterfeed&utm_medium=twitter}{&Yet's Tutorial}, \nwanchor{http://bennolan.com/2010/11/24/backbone-jquery-demo.html}{Backbone Mobile} (which is written in \nwanchorto{http://jashkenas.github.com/coffee-script/}{Coffee}), and \nwanchor{http://joshbohde.com/2010/11/25/backbonejs-and-django/}{Backbone and Django}. However, a couple of months ago I was attempting to learn Sammy.js, a library very similar to Backbone, and they had a nifty tutorial called \nwanchorto{http://code.quirkey.com/sammy/tutorials/json_store_part1.html}{The JsonStore}. In the spirit of The JSON Store, I present The Backbone Store. \subsection{Literate Program} A note: this article was written with the \nwanchorto{http://en.wikipedia.org/wiki/Literate_programming}{Literate Programming} toolkit \nwanchorto{http://www.cs.tufts.edu/~nr/noweb/}{Noweb}. Where you see something that looks like \<\\>, 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 1.2 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.5 and Backbone 0.3.3. \subsection{The Store} The store has three features: A list of products, a product detail page, and a ``shopping cart'' that does nothing but tally up the number of products total that you might wish to order. The main viewport flips between a list of products and a product detail; the shopping cart quantity tally is always visible. Let's start by showing you the HTML that we're going to be exploiting. As you can see, the shopping cart's primary display is already present, with zero items shoving. DOM ID ``main'' is empty. We'll fill it with templated data later. \subsection{HTML} <>= The Backbone Store <> <>
@ This is taken, more or less, straight from The JSON Store. I've included one extra thing, aside from jQuery and Backbone, and that's the \nwanchorto{https://github.com/jquery/jquery-tmpl}{jQuery Templates kit}. There is also 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}.) \section{The Program} And here's the skeleton of the program we're going to be writing: <>= (function() { <> <> <> <> <> <> <> }).call(this); @ \section{Models and Collections for the Store} Products are basically what we're selling. In Backbone, a product maps to a Backbone Model. Backbone's Model class provides a full suite of methods for setting and deleting attributes. One of things I've found useful is to expose the CID (an internal and locally unique ``client ID'' generated by Backbone) and use it to decorate DOM id's and classes. So, here, I override the Models's [[toJSON()]] and add the CID to the representation. In production, I've typically created a parent class for all of my classes, overriden [[toJSON()]], and extended that instead. <>= var Product = Backbone.Model.extend({ toJSON: function() { return _.extend(_.clone(this.attributes), {cid: this.cid}) } }); @ A store has a lot of products, so we use a Backbone Collection to keep track of them. A Collection always has a Model object associated with it; if you attempt to add an object to the Collection that is not an instance of [[Backbone.Model]], the Collection will attempt to coerce that object into it [[model]] type. Both Models and Collections have a [[toJSON()]] method. The Model creates a JSON representation of its attributes, and the collection creates a JSON representation of an array, calling [[Model.toJSON()]] for each model in contains. The other novel thing here is the comparator; Backbone uses it define the default ordering for the collection. If not defined, calling [[sort()]] on the collection raises an exception. <>= var ProductCollection = Backbone.Collection.extend({ model: Product, comparator: function(item) { return item.get('title'); } }); @ Shopping carts are a bit strange. The typical cart has items: each item represents one product and the quantity the user wants to buy. So a cart may have two items, but the user may be buying five things: one of the first item, and four of the second, and so forth. For our (simple) purpose, I'm just going to have an item that you can add amounts to, that get stored as a 'quantity'. <>= var CartItem = Backbone.Model.extend({ update: function(amount) { this.set({'quantity': this.get('quantity') + amount}); } }); @ The other feature is that, for the collection, I will want to find the CartItem not by its ID, but by the ID of the product it contains. So I have added the method [[getByProductId]]. <>= var Cart = Backbone.Collection.extend({ model: CartItem, getByProductId: function(pid) { return this.detect(function(obj) { return (obj.get('product').cid == pid); }); }, }); @ \section{Views} Backbone Views are simple policy objects. They often have a root element, the contents of which they manipulate, a model or collection they represent within that root element, events that may occur within that root element that they monitor and consequently act on. Views are not rigid; it's just Javascript and the DOM, and you can hook external events as needed. (This can be useful, for example, when doing drag-and-drop with jQueryUI to highlight valid drop zones.) More importantly, it is sensitive to events \textit{within its model or collection}, and can respond to changes automatically, without having to manually invoke the view. There are three views here: the CartView, the ProductListView, and a single ProductView. The [[CartView]] lives in the upper-right-hand corner of our screen, and just shows the quantity of items in our shopping cart. It has a default [[el]], where it will draw its quantity. This view illustrates the binding to its collection: whenever the collection is updated in some way, the [[CartView]] automagically updates itself. The programmer is now relieved of any responsibility of remembering to update the view, which is a huge win. The [[\_.bind()]] method associates the [[render]] with the instance of the [[CartView]]. The [[render()]] method is the conventional name for rendering the elements. Nothing in Backbone calls [[render()]] directly; it's up to the developer to decide how and when an object should should be rendered. This also illustrates the use of jQuery animations in Backbone. <>= var CartView = Backbone.View.extend({ el: $('.cart-info'), initialize: function() { this.collection.bind('change', _.bind(this.render, this)); }, render: function() { var sum = this.collection.reduce(function(m, n) { return m + n.get('quantity'); }, 0); this.el .find('.cart-items').text(sum).end() .animate({paddingTop: '30px'}) .animate({paddingTop: '10px'}); } }); @ %$ The [[ProductListView]] again has a root element, this time the [[#main]] DIV of our HTML, into which we're going to draw a jQuery template list of our record albums. The only tricks here are the compilation of the jQuery template when the View is instantiated, and the use of an enclosured (is that a word?) [[self]] variable to provide a hard context for the [[this]] variable within inner jQuery calls. <>= var ProductListView = Backbone.View.extend({ el: $('#main'), indexTemplate: $("#indexTmpl").template(), render: function() { var self = this; this.el.fadeOut('fast', function() { self.el.html($.tmpl(self.indexTemplate, self.model.toJSON())); self.el.fadeIn('fast'); }); return this; } }); @ The view uses a jQuery template. This is a simple, repeatable template that jQuery.Template, upon encountering an array, repeats until the array is exhausted. Note the presence of [[\${cid}]]. <>= @ %$ The most complicated object in our ecosystem is the product view. It actually does something! The prefix ought to be familiar, but note that we're again using [[#main]] as our target; we'll be replacing these contents over and over. <>= var ProductView = Backbone.View.extend({ el: $('#main'), itemTemplate: $("#itemTmpl").template(), initialize: function(options) { this.cart = options.cart; return this; }, @ We want to update the cart as needed. Remember that when we update the cart item, the CartView will be notified automagically. Later, I'll show how when we initialize and route to a product view, we pass in the model associated with it. This code ought to be fairly readable: the only specialness is that it's receiving an event, and we're ``silencing'' the call to [[cart.add()]], which means that the cart collection will not publish any events. There are only events when the item has more than zero, and that gets called on [[cart_item.update()]]. <>= update: function(e) { e.preventDefault(); var cart_item = this.cart.getByProductId(this.model.cid); if (_.isUndefined(cart_item)) { cart_item = new CartItem({product: this.model, quantity: 0}); this.cart.add(cart_item, {silent: true}); } cart_item.update(parseInt($('.uqf').val())); }, updateOnEnter: function(e) { if (e.keyCode == 13) { return this.update(e); } }, @ %$ These are the events in which we're interested: keypresses and clicks on the update button and the quantity form. (Okay, ``UQ'' isn't the best for ``update quantity''. I admit that.) Note the peculiar syntax of ``EVENT SELECTOR'': ``methodByName'' for each event. Backbone tells us that the only events it can track by itself are those that jQuery's ``delegate'' understands. As of 1.5, that seems to be just about all of them. One thing that I was not aware of until recently: if you remove and replace the [[el]] object during the lifespan of your view (including in [[initialize()]]), you must then call [[delegateEvents()]] again on the new object for these events to work. <>= events: { "keypress .uqf" : "updateOnEnter", "click .uq" : "update", }, @ And finally the render. There is no rocket science here. You've seen this before. %' <>= render: function() { var self = this; this.el.fadeOut('fast', function() { self.el.html($.tmpl(self.itemTemplate, self.model.toJSON())); self.el.fadeIn('fast'); }); return this; } }); @ The template for a ProductView is straightforward. It contains the form with the [[uq]] objects, the actions of which we intercept and operate on internally. Backbone does this automatically using jQuery's [[delegate]] method. <>= @ %' \section{The Router} The router is a fairly straightforward component. It's purpose is to pay attention to the ``\#hash'' portion of your URL and, when it changes, do something. Anything, really. [[Backbone.History]] is the event listener for the hash, so it has to be activated after the application. In many ways, a Backbone ``Controller'' is just a big View with authority over the entire Viewport. To begin with, I'm going to keep track of the ``three'' views I care about: the CartView, the ProductListView, and the ProductView. I'm going to cheat by attaching the ProductViews to their individual products, and invoke that view as necessary. <>= var BackboneStore = Backbone.Controller.extend({ _index: null, _products: null, _cart :null, @ %$ There are only two routes: home, and item: <>= routes: { "": "index", "item/:id": "item", }, @ Here's where things get interesting. There are two schools of thought over the Controller; one, that the Controller ought to be able to get all the data it needs, and two, that the Controller ought to begin with enough data to do the job sensibly. I fall into the second camp. I'm going to pass in to the [[initialize()]] method an array of objects representing all the products in the system. <>= initialize: function(data) { this._cart = new Cart(); new CartView({collection: this._cart}); this._products = new ProductCollection(data); this._index = new ProductListView({model: this._products}); return this; }, @ When we're routed to the [[index]] method, all we need to do is render the index: <>= index: function() { this._index.render(); }, @ When we are routed to a product, we need to find that product, get its view if it has one or create one if it doesn't, then call render: <>= item: function(id) { var product = this._products.getByCid(id); if (_.isUndefined(product._view)) { product._view = new ProductView({model: product, cart: this._cart}); } product._view.render(); } }); @ And that's the entirety of the application. \section{Initialization} Initialization for most single-page applications happens when the DOM is ready. So I'll do exactly that. This should be obvious, except what the Hell is that when/then construct? That's a new feature of jQuery 1.5 called Deferreds (also known as Promises or Futures). All jQuery 1.5 ajax calls are Deferreds that return data when you dereference them; [[when()]] is an instruction to wait until the ajax call is done, and [[then()]] is a chained instruction on what to do next. This is a trivial example, but when you have multiple streams of data coming in (say, you're loading independent schemas, or you have multiple, orthagonal data sets in your application, each with their own URL as per the Richardson Maturity Model), you can pass the array of ajax objects to [[when()]] and [[then()]] won't fire until they're all done. Automagic synchronization is a miracle. <>= $(document).ready(function() { var fetch_items = function() { return $.ajax({ url: 'data/items.json', data: {}, contentType: "application/json; charset=utf-8", dataType: "json" }); }; $.when(fetch_items()).then(function(data) { new BackboneStore(data); Backbone.history.start(); }); }); @ And that's it. Put it all together, and you've got yourself a working Backbone Store. This code is available at my github at \nwanchorto{https://github.com/elfsternberg/The-Backbone-Store}{The Backbone Store}. \end{document}