\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 I've been learning how to use \nwanchorto{http://documentcloud.github.com/backbone/}{Backbone.js}, a nifty little library for organizing your client-side Javascript into a classic Model-View-Controller paradigm while trying (and to some extent, succeeding) in trying to burden you, the user, with as little additional learning as possible. I consider this a good thing; the overhead of learning a library and its accompaning DSL represent additional cognitive loads that developers can better use elsewhere. Keeping as much as possible within familiar paradigms is not just useful, it's necessary as our programs get bigger. The tutorial for Backbone is woefully lacking in specifics, and the example program, Todo, doesn't really have much chops in teaching you the ins and outs of Backbone, especially not its new Controller and History modules. But in the announcement for Backbone.Controller, Jeremy Ashkenas hid a clue: There's another library, Sammy.js, that does something similar, and they do have a 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, and online store written entirely in JSON and operating entirely within a single page. Let's start by showing you the HTML that we're going to be exploiting: <>= 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}. We'll discuss those in a minute. There's a simplified JSON file that comes in the download; it contains six record albums that the store sells. (Unlike the JSON store, these albums don't exist; the covers were generated during a round of \nwanchorto{http://elfs.livejournal.com/756709.html}{The Album Cover Game}.) There are two views, the index and the item. So, using [[Backbone.Controller]], we're going to route the following: <>= routes: { "": "index", "item/:id": "item", }, @ Unlike Sammy, Backbone mostly only routes GET commands. Routes are to routes to views; everything else happens more or less under the covers. As Backbone is running, the [[Backbone.History]] module is listening to the hash object, waiting for it to change so that it can trigger a ``route'' event, in which case the function named as the value in the route is called. There are a few things I want to track: the index view, the individual product views, and the shopping cart. <>= _index: null, _products: null, _cart :null, @ Using backbone, I have a list of products. So, I should declare those. The basic product is just a model, with nothing to show for it; the list of products is a [[Backbone.Collection]], with one feature, the [[comparator]], which sorts the albums in order by album title. <>= var Product = Backbone.Model.extend({}); var ProductCollection = Backbone.Collection.extend({ model: Product, comparator: function(item) { return item.get('title'); } }); @ That's not very exciting. Let's show this list, with a View. Let's call it IndexView: <>= var IndexView = Backbone.View.extend({ el: $('#main'), indexTemplate: $("#indexTmpl").template(), render: function() { var sg = this; this.el.fadeOut('fast', function() { sg.el.empty(); $.tmpl(sg.indexTemplate, sg.model.toArray()).appendTo(sg.el); sg.el.fadeIn('fast'); }); return this; } }); @ This code defines a [[Backbone.View]] object, in which the parent element is \#main, the render function fades out the existing elements in that position and replaces them with the contents of a rendered jQuery template, and then fades the element back in. The index template looks like this: <>= @ There's some \nwanchorto{http://en.wikipedia.org/wiki/Law_of_Demeter}{Demeter violations} going on here, in that I have to know about the [[attributes]] of a Backbone model, something that's normally hidden within the class. But this is good enough for our purposes. The above is a jQuery template, and the [[\$\{\}]] syntax is what's used to dereference variables within a template. (As an aside, I think that the [[set]] and [[get]] methods of [[Backbone.Model]] are a poor access mechanism. I understand why they're there, and I can only hope that someday \nwanchorto{http://ejohn.org/blog/javascript-getters-and-setters/}{Javascript Getter and Setters} become so well-established as to make [[set]] and [[get]] irrelevant.) Now, we can render the index view: <>= index: function() { this._index.render(); }, @ At this point, well, we need an application. A controller. And we need to initialize it, and call it. Here's what it looks like (some of this, you've already seen): <>= var Workspace = Backbone.Controller.extend({ <> <> <> <> <> }); workspace = new Workspace(); Backbone.history.start(); @ There are two things left in our workspace, that we haven't defined. The intialization, and the product render. Initialization consists of getting our product list, creating a shopping cart to hold ``desired'' products (and in quantity!), and creating the index view. (Product views, we'll discuss in a moment). <>= initialize: function() { var ws = this; if (this._index === null) { $.ajax({ url: 'data/items.json', dataType: 'json', data: {}, success: function(data) { ws._cart = new Cart(); new CartView({model: ws._cart}); ws._products = new ProductCollection(data); ws._index = new IndexView({model: ws._products}); Backbone.history.loadUrl(); } }); return this; } return this; }, @ We haven't defined the Cart yet, but that's all right. We'll get to it.) But here you see a lot of what's already existent being used: we get a ProductCollection, and an IndexView. That last line is curious. It's an instruction to Backbone to look at the URL; if the user navigated to something other than the home page, it's to use the routes defined to go there. Users can now bookmark places in your site other than the home page. Yes, the bookmark will be funny and have at least one \nwanchorto{http://en.wiktionary.org/wiki/octothorpe}{octothorpe} in it, but it will work. Let's deal with the shopping cart: <>= var CartItem = Backbone.Model.extend({ update: function(amount) { this.set({'quantity': this.get('quantity') + amount}); } }); var Cart = Backbone.Collection.extend({ model: CartItem, getByPid: function(pid) { return this.detect(function(obj) { return (obj.get('product').cid == pid); }); }, }); @ A little rocket science here: A [[Cart]] contains [[CartItems]]. Each ``item'' represents a quantity of a [[Product]]. (I know, that always struck me as odd, but that's how most online stores do it.) [[CartItem]] has an update method that allows you to add more (but not remove any-- hey, the Sammy store wasn't any smarter, and this is For Demonstration Purposes Only), and we use the [[set]] method to make sure that a ``change'' event is triggered. The [[Cart]], in turn, has a method, getByPid (``Product ID''), which is meant to assist other objects in finding the [[CartItem]] associated with a specific product. Here, I'm just using the Backbone default client id. The cart is represented by a little tag in the upper right-hand corner of the view; it never goes away, and its count is always the total number of [[Products]] (not [[CartItem]]s) ordered. So the [[CartView]] needs to update whenever a [[CartItem]] is added or updated. And we want a nifty little animation to go with it: <>= var CartView = Backbone.View.extend({ el: $('.cart-info'), initialize: function() { this.model.bind('change', _.bind(this.render, this)); }, render: function() { var sum = this.model.reduce(function(m, n) { return m + n.get('quantity'); }, 0); this.el .find('.cart-items').text(sum).end() .animate({paddingTop: '30px'}) .animate({paddingTop: '10px'}); } }); @ A couple of things here: the render is rebound to [[this]] to make sure it renders in the context of the view. I found that that was not always happening. Note the use of [[reduce]], a nifty method from [[underscore.js]] that allows you to build a result out an array using an anonymous function. This reduce, obviously, sums up the total quantity of items in the cart. Also, jQuery enthusiasts could learn (I certainly did!) from the [[.find()]] and [[.end()]] methods, which push a child object onto the stack to be animated, and then pop it off after the operation has been applied. The biggest thing left is the [[ProductView]]. It's skeleton looks like this: <>= var ProductView = Backbone.View.extend({ el: $('#main'), itemTemplate: $("#itemTmpl").template(), <> initialize: function(options) { this.cart = options.cart; }, <> <> }); @ First, we find the element we're going to work with, and the template. I expect the ProductView to be where we'll add items to the cart, so the initializer here expects to have a handle on the cart. And the template: <>= @ One extra item: note the octothorpe used as the target link for ``Home''. I kept thinking an empty link or just ``/'' would be appropriate, but no, it's an octothorpe. Rendering the product is not difficult: <>= render: function() { var sg = this; this.el.fadeOut('fast', function() { sg.el.empty(); $.tmpl(sg.itemTemplate, sg.model).appendTo(sg.el); sg.el.fadeIn('fast'); }); return this; } @ That looks familiar. Updating the product, however, is a whole 'nother story. Note that each product has a form associated with it. We need to intercept any form update events and manipulate our shopping cart. We have two objects that can do that: the input field, and the submit button. I need to intercept those events: <>= events: { "keypress .uqf" : "updateOnEnter", "click .uq" : "update", }, @ Backbone uses a curious definition of an event with an ``event selector'', followed by a target method of the View class. Backbone is also limited about what events can be used here, as the following events cannot be wrapped by jQuery's delegate method and do not work: ``focus'', ``blur'', ``change'', ``submit'', and ``reset''. The update then becomes straightforward. We're in a view for a specific product; we must see if the customer has a [[CartItem]] for that product in the [[Cart]], and add or update it as needed. Like so: <>= update: function(e) { e.preventDefault(); var cart_item = this.cart.getByPid(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); } }, @ We [[preventDefault]] to keep the traditional meaning of the submit button from triggering. When the [[CartItem]] is updated, it triggers a ``change'' event, and the [[CartView]] will update itself automatically. I added the ``silent'' option to keep the ``change'' event from triggering twice when adding a new [[CartItem]] to the [[Cart]]. And now I'm down to one last thing. I haven't defined that product render call in the application controller. The one thing I don't want to do is have [[ProductViews]] for every product, if I don't need them. So I want to build them as-needed, but keep them, and associate them with the local [[Product]], so they can be recalled whenever we want. The underscore function [[isUndefined]] is excellent for this. <>= item: function(id) { if (_.isUndefined(this._products.getByCid(id)._view)) { this._products.getByCid(id)._view = new ProductView({model: this._products.getByCid(id), cart: this._cart}); } this._products.getByCid(id)._view.render(); } @ And now my store looks like <>= <> <> <> <> <> <> @ As always, this code is available at github. \end{document}