├── .gitignore ├── Makefile ├── README.md ├── bin └── autoreload ├── docs └── backbonestore.pdf ├── htdocs ├── data │ └── items.json ├── images │ ├── AdventuresInOdyssey.jpg │ ├── AdventuresInOdyssey_t.jpg │ ├── AmericanAttorneys.jpg │ ├── AmericanAttorneys_t.jpg │ ├── BritishCivilLightTransport.jpg │ ├── BritishCivilLightTransport_t.jpg │ ├── PeriodsofMentalAssimilation.jpg │ ├── PeriodsofMentalAssimilation_t.jpg │ ├── Pulaski.jpg │ ├── Pulaski_t.jpg │ ├── StealthMonkeyVirus.png │ ├── StealthMonkeyVirus_t.jpg │ ├── SumsofMagnolia.jpg │ └── SumsofMagnolia_t.jpg ├── index.html ├── jsonstore.css └── store.js ├── package.json └── src └── backbonestore.nw /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *# 4 | .#* 5 | .DS_Store 6 | *~ 7 | node_modules/* 8 | bower_components/* 9 | npm-debug.log 10 | docs/*.html 11 | docs/*.tex 12 | htdocs/lib 13 | package.yml 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: setup store serve 2 | 3 | NOTANGLE= notangle 4 | NOWEAVE= noweave 5 | ECHO= echo 6 | 7 | LIBS:= htdocs/lib/underscore.js htdocs/lib/jquery.js htdocs/lib/backbone.js 8 | 9 | all: htdocs/index.html htdocs/store.js htdocs/data/items.json 10 | @if [ ! -e "./htdocs/lib" ]; then \ 11 | echo "Please do 'make setup' before continuing"; \ 12 | exit 1; \ 13 | fi 14 | 15 | serve: all 16 | ./bin/autoreload 17 | 18 | store: all 19 | 20 | htdocs/lib: 21 | mkdir -p htdocs/lib 22 | 23 | htdocs/lib/underscore.js: htdocs/lib 24 | cp bower_components/underscore/underscore.js htdocs/lib 25 | 26 | htdocs/lib/jquery.js: htdocs/lib 27 | cp bower_components/jquery/dist/jquery.js htdocs/lib 28 | 29 | htdocs/lib/backbone.js: 30 | cp bower_components/backbone/backbone.js htdocs/lib 31 | 32 | install: 33 | npm install 34 | ./node_modules/bower/bin/bower install jquery underscore backbone 35 | 36 | setup: install $(LIBS) 37 | 38 | docs: 39 | mkdir -p docs 40 | 41 | htdocs/index.html: src/backbonestore.nw 42 | $(NOTANGLE) -c -Rindex.html src/backbonestore.nw > htdocs/index.html 43 | 44 | htdocs/store.js: src/backbonestore.nw 45 | $(NOTANGLE) -c -Rstore.js src/backbonestore.nw > htdocs/store.js 46 | 47 | docs/backbonestore.tex: docs src/backbonestore.nw 48 | ${NOWEAVE} -x -delay src/backbonestore.nw > docs/backbonestore.tex 49 | 50 | docs/backbonestore.pdf: docs/backbonestore.tex 51 | xelatex docs/backbonestore.tex; \ 52 | while grep -s 'Rerun to get cross-references right' ./backbonestore.log; \ 53 | do \ 54 | xelatex docs/backbonestore.tex; \ 55 | done 56 | mv backbonestore.pdf docs 57 | rm -f ./backbonestore.log ./backbonestore.aux ./backbonestore.out 58 | 59 | pdf: docs/backbonestore.pdf 60 | 61 | docs/backbonestore.html: docs src/backbonestore.nw 62 | $(NOWEAVE) -filter l2h -delay -x -autodefs c -html src/backbonestore.nw > docs/backbonestore.html 63 | 64 | html: docs/backbonestore.html 65 | 66 | clean: 67 | - rm -f htdocs/*.js htdocs/*.html docs/*.tex docs/*.dvi docs/*.aux docs/*.toc docs/*.log docs/*.out 68 | 69 | distclean: clean 70 | - rm -fr ./htdocs/lib 71 | 72 | realclean: distclean 73 | - rm -fr docs 74 | 75 | 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | The Backbone Store is a tutorial and demonstration application for 4 | BackboneJS, a javascript framework for managing data-driven websites. 5 | 6 | ## Installation 7 | 8 | After checking out the source code, type 9 | 10 | $ make setup all serve 11 | 12 | This will automatically run the NPM and Bower install scripts, placing 13 | the correct libraries into the target tree, build the actual application 14 | from the original source material, and start a local server. 15 | 16 | ## Requirements 17 | 18 | The build tool relies upon GNU Make and node-js. It also uses the NoWeb 19 | Literate Programming documentation tools, and building the documentation 20 | from source requires Xelatex be installed as well. 21 | 22 | The command 'make serve' probably only works under a fairly modern 23 | Linux, as it is dependent upon the kernel's inotify facility. 24 | 25 | ## Branches 26 | 27 | There are two major development branches for The Backbone Store. 28 | 29 | Branch 'master' uses HTML, CSS, and Javascript. 30 | 31 | Branch 'modern' uses HAML, Stylus, and Coffee. 32 | 33 | ## Changelog 34 | 35 | ### Changes from 2.0 36 | 37 | Version 3.0 has the following notable changes: 38 | * Replace __super__ with prototype 39 | * Replace Backbone-generated internal IDs with supplied IDs 40 | * Updates the use of Deferred 41 | * Updates to the current Underscore Template mechanism 42 | 43 | ### Changes from 1.0 44 | 45 | Version 2.0 has the following notable changes: 46 | * Use of jQuery animations 47 | * Better Styling 48 | * Proper event management. Version 1.0 was just doin' it WRONG. 49 | 50 | ## Copyright 51 | 52 | Store.js is entirely my own work, and is Copyright (c) 2010 Elf 53 | M. Sternberg. Included libraries are covered by their respective 54 | copyright holders, and are used with permission of the licenses 55 | included. Store.js is intended for educational purposes only, rather 56 | than to be working code, and is hereby licensed under the Creative 57 | Commons Attribution Non-Commercial Share Alike (by-nc-sa) licence. 58 | 59 | The images contained herein are derivative works of photographs 60 | licensed under Creative Commons licences for non-commercial purposes. 61 | 62 | ## Contribution 63 | 64 | Please look in backbonestore.nw for the base code. Backbonestore.nw 65 | is produced using the Noweb Literate Programming toolkit by Norman 66 | Ramsey (http://www.cs.tufts.edu/~nr/noweb/). 67 | -------------------------------------------------------------------------------- /bin/autoreload: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require('fs'); 3 | var Inotify = require('inotify').Inotify; 4 | 5 | var spawn = require('child_process').spawn; 6 | 7 | var spew = function(data) { 8 | return console.log(data.toString('utf8')); 9 | }; 10 | 11 | var server = spawn('./node_modules/http-server/bin/http-server', ['./htdocs/']); 12 | server.stdout.on('data', spew); 13 | 14 | var monitor = new Inotify(); 15 | 16 | var reBuild = function() { 17 | var maker = spawn('make', ['store']); 18 | return maker.stdout.on('data', spew); 19 | }; 20 | 21 | monitor.addWatch({ 22 | path: "./src/backbonestore.nw", 23 | watch_for: Inotify.IN_CLOSE_WRITE, 24 | callback: reBuild 25 | }); 26 | 27 | -------------------------------------------------------------------------------- /docs/backbonestore.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/docs/backbonestore.pdf -------------------------------------------------------------------------------- /htdocs/data/items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "unless-you-have-been-drinking", 4 | "title": "Unless You Have Been Drinking", 5 | "artist": "Adventures in Odyssey", 6 | "image": "images/AdventuresInOdyssey_t.jpg", 7 | "large_image": "images/AdventuresInOdyssey.jpg", 8 | "price": 9.98, 9 | "url": "http://www.amazon.com/Door-Religious-Knives/dp/B001FGW0UQ/?tag=quirkey-20" 10 | }, 11 | { 12 | "id": "leave-to-do-my-utmost", 13 | "title": "Leave To Do My Utmost", 14 | "artist": "American Attorneys", 15 | "image": "images/AmericanAttorneys_t.jpg", 16 | "large_image": "images/AmericanAttorneys.jpg", 17 | "price": 13.98, 18 | "url": "http://www.amazon.com/gp/product/B002GNOMJE?ie=UTF8&tag=quirkeycom-20&linkCode=as2&camp=1789&creative=390957&creativeASIN=B002GNOMJE" 19 | }, 20 | { 21 | "id": "the-dead-sleep-encircled-by-the-living", 22 | "title": "The Dead Sleep Encircled by The Living", 23 | "artist": "British Civil Light Transport", 24 | "image": "images/BritishCivilLightTransport_t.jpg", 25 | "large_image": "images/BritishCivilLightTransport.jpg", 26 | "price": 13.98, 27 | "url": "http://www.amazon.com/Bitte-Orca-Dirty-Projectors/dp/B0026T4RTI/ref=pd_sim_m_12?tag=quirkey-20" 28 | }, 29 | { 30 | "id": "periods-of-mental-assimilation", 31 | "title": "Periods of Mental Assimilation", 32 | "artist": "Grigory Szondy", 33 | "image": "images/PeriodsofMentalAssimilation_t.jpg", 34 | "large_image": "images/PeriodsofMentalAssimilation.jpg", 35 | "price": 13.99, 36 | "url": "http://www.amazon.com/Pains-Being-Pure-Heart/dp/B001LGXIDS/ref=pd_sim_m_44?tag=quirkey-20" 37 | }, 38 | { 39 | "id": "keenly-developed-moral-bankruptcy", 40 | "title": "Keenly Developed Moral Bankruptcy", 41 | "artist": "Stealth Monkey Virus", 42 | "image": "images/StealthMonkeyVirus_t.jpg", 43 | "large_image": "images/StealthMonkeyVirus.png", 44 | "price": 13.99, 45 | "url": "http://www.amazon.com/Pains-Being-Pure-Heart/dp/B001LGXIDS/ref=pd_sim_m_44?tag=quirkey-20" 46 | }, 47 | { 48 | "id": "my-mistresss-sparrow-is-dead", 49 | "title": "My Mistress's Sparrow is Dead", 50 | "artist": "Sums of Mongolia", 51 | "image": "images/SumsofMagnolia_t.jpg", 52 | "large_image": "images/SumsofMagnolia.jpg", 53 | "price": 13.99, 54 | "url": "http://www.amazon.com/Pains-Being-Pure-Heart/dp/B001LGXIDS/ref=pd_sim_m_44?tag=quirkey-20" 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /htdocs/images/AdventuresInOdyssey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/AdventuresInOdyssey.jpg -------------------------------------------------------------------------------- /htdocs/images/AdventuresInOdyssey_t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/AdventuresInOdyssey_t.jpg -------------------------------------------------------------------------------- /htdocs/images/AmericanAttorneys.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/AmericanAttorneys.jpg -------------------------------------------------------------------------------- /htdocs/images/AmericanAttorneys_t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/AmericanAttorneys_t.jpg -------------------------------------------------------------------------------- /htdocs/images/BritishCivilLightTransport.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/BritishCivilLightTransport.jpg -------------------------------------------------------------------------------- /htdocs/images/BritishCivilLightTransport_t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/BritishCivilLightTransport_t.jpg -------------------------------------------------------------------------------- /htdocs/images/PeriodsofMentalAssimilation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/PeriodsofMentalAssimilation.jpg -------------------------------------------------------------------------------- /htdocs/images/PeriodsofMentalAssimilation_t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/PeriodsofMentalAssimilation_t.jpg -------------------------------------------------------------------------------- /htdocs/images/Pulaski.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/Pulaski.jpg -------------------------------------------------------------------------------- /htdocs/images/Pulaski_t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/Pulaski_t.jpg -------------------------------------------------------------------------------- /htdocs/images/StealthMonkeyVirus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/StealthMonkeyVirus.png -------------------------------------------------------------------------------- /htdocs/images/StealthMonkeyVirus_t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/StealthMonkeyVirus_t.jpg -------------------------------------------------------------------------------- /htdocs/images/SumsofMagnolia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/SumsofMagnolia.jpg -------------------------------------------------------------------------------- /htdocs/images/SumsofMagnolia_t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfsternberg/The-Backbone-Store/82af8f6a443664a91af2edd707060b09e357e5d4/htdocs/images/SumsofMagnolia_t.jpg -------------------------------------------------------------------------------- /htdocs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Backbone Store 5 | 6 | 23 | 24 | 52 | 53 | 56 | 57 | 58 | 59 | 60 |
61 | 67 |
68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /htdocs/jsonstore.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Lucida Grande", Lucida, Helvetica, Arial, sans-serif; 3 | background: #fff; 4 | color: #333; 5 | margin: 0px; 6 | padding: 0px; 7 | } 8 | #main { 9 | position: relative; 10 | } 11 | #header { 12 | background: #999; 13 | background: -webkit-gradient(linear, left top, left bottom, from(#adadad), to(#7a7a7a)); 14 | background: -moz-linear-gradient(top, #adadad, #7a7a7a); 15 | margin: 0px; 16 | padding: 20px; 17 | border-bottom: 1px solid #ccc; 18 | } 19 | #header h1 { 20 | font-family: Inconsolata, Monaco, Courier, mono; 21 | color: #fff; 22 | margin: 0px; 23 | } 24 | #header .cart-info { 25 | position: absolute; 26 | top: 0px; 27 | right: 0px; 28 | text-align: right; 29 | padding: 10px; 30 | background: #555; 31 | background: -webkit-gradient(linear, left top, left bottom, from(#777), to(#444)); 32 | background: -moz-linear-gradient(top, #777, #444); 33 | color: #fff; 34 | font-size: 12px; 35 | font-weight: bold; 36 | } 37 | img { 38 | border: 0; 39 | } 40 | .productitemview { 41 | position: absolute; 42 | top: 0; 43 | left: 0; 44 | } 45 | #productlistview { 46 | position: absolute; 47 | top: 0; 48 | left: 0; 49 | } 50 | #productlistview ul { 51 | list-style: none; 52 | } 53 | .item { 54 | float: left; 55 | width: 250px; 56 | margin-right: 10px; 57 | margin-bottom: 10px; 58 | padding: 5px; 59 | -moz-border-radius-topleft: 5px; 60 | -moz-border-radius-topright: 5px; 61 | -moz-border-radius-bottomleft: 5px; 62 | -moz-border-radius-bottomright: 5px; 63 | -webkit-border-bottom-right-radius: 5px; 64 | -webkit-border-top-left-radius: 5px; 65 | -webkit-border-top-right-radius: 5px; 66 | -webkit-border-bottom-left-radius: 5px; 67 | border-bottom-right-radius: 5px; 68 | border-top-left-radius: 5px; 69 | border-top-right-radius: 5px; 70 | border-bottom-left-radius: 5px; 71 | border: 1px solid #ccc; 72 | text-align: center; 73 | font-size: 12px; 74 | } 75 | .item-title { 76 | font-weight: bold; 77 | } 78 | .item-artist { 79 | font-weight: bold; 80 | font-size: 14px; 81 | } 82 | .item-detail { 83 | margin: 10px 0 0 10px; 84 | } 85 | .item-detail .item-image { 86 | float: left; 87 | margin-right: 10px; 88 | } 89 | .item-detail .item-info { 90 | padding: 100px 10px 0px 10px; 91 | } 92 | -------------------------------------------------------------------------------- /htdocs/store.js: -------------------------------------------------------------------------------- 1 | var Product = Backbone.Model.extend({}); 2 | 3 | var Item = Backbone.Model.extend({ 4 | update: function(amount) { 5 | if (amount === this.get('quantity')) { 6 | return this; 7 | } 8 | this.set({quantity: amount}, {silent: true}); 9 | this.collection.trigger('update', this); 10 | return this; 11 | }, 12 | 13 | price: function() { 14 | return this.get('product').get('price') * this.get('quantity'); 15 | } 16 | }); 17 | 18 | 19 | var ProductCollection = Backbone.Collection.extend({ 20 | model: Product, 21 | initialize: function(models, options) { 22 | this.url = options.url; 23 | }, 24 | 25 | comparator: function(item) { 26 | return item.get('title'); 27 | } 28 | }); 29 | 30 | 31 | var ItemCollection = Backbone.Collection.extend({ 32 | model: Item, 33 | 34 | updateItemForProduct: function(product, amount) { 35 | amount = amount != null ? amount : 0; 36 | var pid = product.get('id'); 37 | var item = this.detect(function(obj) { 38 | return obj.get('product').get('id') === pid; 39 | }); 40 | if (item) { 41 | item.update(amount); 42 | return item; 43 | } 44 | return this.add({ 45 | product: product, 46 | quantity: amount 47 | }); 48 | }, 49 | 50 | getTotalCount: function() { 51 | var addup = function(memo, obj) { 52 | return memo + obj.get('quantity'); 53 | }; 54 | return this.reduce(addup, 0); 55 | }, 56 | 57 | getTotalCost: function() { 58 | var addup = function(memo, obj) { 59 | return memo + obj.price(); 60 | }; 61 | return this.reduce(addup, 0); 62 | } 63 | }); 64 | 65 | 66 | var BaseView = Backbone.View.extend({ 67 | parent: $('#main'), 68 | className: 'viewport', 69 | 70 | initialize: function(options) { 71 | Backbone.View.prototype.initialize.apply(this, arguments); 72 | this.$el.hide(); 73 | this.parent.append(this.el); 74 | }, 75 | 76 | hide: function() { 77 | var dfd = $.Deferred(); 78 | if (!this.$el.is(':visible')) { 79 | return dfd.resolve(); 80 | } 81 | this.$el.fadeOut('fast', function() { 82 | return dfd.resolve(); 83 | }); 84 | return dfd.promise(); 85 | }, 86 | 87 | show: function() { 88 | var dfd = $.Deferred(); 89 | if (this.$el.is(':visible')) { 90 | return dfd.resolve(); 91 | } 92 | this.$el.fadeIn('fast', function() { 93 | return dfd.resolve(); 94 | }); 95 | return dfd.promise(); 96 | } 97 | }); 98 | 99 | 100 | var ProductListView = BaseView.extend({ 101 | id: 'productlistview', 102 | template: _.template($("#store_index_template").html()), 103 | 104 | initialize: function(options) { 105 | BaseView.prototype.initialize.apply(this, arguments); 106 | this.collection.bind('reset', this.render.bind(this)); 107 | }, 108 | 109 | render: function() { 110 | this.$el.html(this.template({ 111 | 'products': this.collection.toJSON() 112 | })); 113 | return this; 114 | } 115 | }); 116 | 117 | 118 | var ProductView = BaseView.extend({ 119 | className: 'productitemview', 120 | template: _.template($("#store_item_template").html()), 121 | 122 | initialize: function(options) { 123 | BaseView.prototype.initialize.apply(this, [options]); 124 | this.itemcollection = options.itemcollection; 125 | }, 126 | 127 | events: { 128 | "keypress .uqf" : "updateOnEnter", 129 | "click .uq" : "update" 130 | }, 131 | 132 | update: function(e) { 133 | e.preventDefault(); 134 | return this.itemcollection.updateItemForProduct(this.model, parseInt(this.$('.uqf').val())); 135 | }, 136 | 137 | updateOnEnter: function(e) { 138 | if (e.keyCode === 13) { 139 | this.update(e); 140 | } 141 | }, 142 | 143 | render: function() { 144 | this.$el.html(this.template(this.model.toJSON())); 145 | return this; 146 | } 147 | }); 148 | 149 | 150 | var CartWidget = Backbone.View.extend({ 151 | el: $('.cart-info'), 152 | template: _.template($('#store_cart_template').html()), 153 | 154 | initialize: function() { 155 | Backbone.View.prototype.initialize.apply(this, arguments); 156 | this.collection.bind('update', this.render.bind(this)); 157 | }, 158 | 159 | render: function() { 160 | var tel = this.$el.html(this.template({ 161 | 'count': this.collection.getTotalCount(), 162 | 'cost': this.collection.getTotalCost() 163 | })); 164 | tel.animate({ paddingTop: '30px' }).animate({ paddingTop: '10px' }); 165 | return this; 166 | } 167 | }); 168 | 169 | 170 | var BackboneStore = Backbone.Router.extend({ 171 | views: {}, 172 | products: null, 173 | cart: null, 174 | 175 | routes: { 176 | "": "index", 177 | "item/:id": "product" 178 | }, 179 | 180 | initialize: function(data) { 181 | Backbone.Router.prototype.initialize.apply(this, arguments); 182 | this.cart = new ItemCollection(); 183 | new CartWidget({ collection: this.cart }); 184 | this.products = new ProductCollection([], { url: 'data/items.json' }); 185 | this.views = { 186 | '_index': new ProductListView({ collection: this.products }) 187 | }; 188 | $.when(this.products.fetch({ reset: true })).then(function() { 189 | return window.location.hash = ''; 190 | }); 191 | }, 192 | 193 | hideAllViews: function() { 194 | return _.filter(_.map(this.views, function(v) { return v.hide(); }), 195 | function(t) { return t !== null; }); 196 | }, 197 | 198 | index: function() { 199 | var view = this.views['_index']; 200 | return $.when.apply($, this.hideAllViews()).then(function() { 201 | return view.show(); 202 | }); 203 | }, 204 | 205 | product: function(id) { 206 | var view = this.views[id]; 207 | if (!view) { 208 | var product = this.products.detect(function(p) { 209 | return p.get('id') === id; 210 | }); 211 | view = this.views[id] = new ProductView({ 212 | model: product, 213 | itemcollection: this.cart 214 | }).render(); 215 | } 216 | return $.when(this.hideAllViews()).then(function() { 217 | return view.show(); 218 | }); 219 | } 220 | }); 221 | 222 | 223 | $(document).ready(function() { 224 | new BackboneStore(); 225 | return Backbone.history.start(); 226 | }); 227 | 228 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "the-backbone-store", 3 | "version": "3.0.1", 4 | "description": "A comprehensive (one hopes) tutorial on a simple development platform for Backbone.", 5 | "main": "htdocs/index.html", 6 | "dependencies": { 7 | "http-server": "^0.9.0" 8 | }, 9 | "devDependencies": { 10 | "inotify": "^1.4.0", 11 | "bower": "^1.7.0" 12 | }, 13 | "scripts": { 14 | "test": "make serve" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/elfsternberg/The-Backbone-Store.git" 19 | }, 20 | "keywords": [ 21 | "backbone", 22 | "javascript", 23 | "makefiles", 24 | "node", 25 | "tutorial" 26 | ], 27 | "author": "Kenneth M. \"Elf\" Sternberg ", 28 | "license": "BSD-2-Clause", 29 | "bugs": { 30 | "url": "https://github.com/elfsternberg/The-Backbone-Store/issues" 31 | }, 32 | "homepage": "https://github.com/elfsternberg/The-Backbone-Store#readme" 33 | } 34 | -------------------------------------------------------------------------------- /src/backbonestore.nw: -------------------------------------------------------------------------------- 1 | % -*- Mode: poly-noweb+javascript -*- 2 | \documentclass{article} 3 | \usepackage{noweb} 4 | \usepackage[T1]{fontenc} 5 | \usepackage{hyperref} 6 | \usepackage{fontspec, xunicode, xltxtra} 7 | \setromanfont{Georgia} 8 | \begin{document} 9 | 10 | % Generate code and documentation with: 11 | % 12 | % noweave -filter l2h -delay -x -html backbonestore.nw | htmltoc > backbonestore.html 13 | % notangle -Rstore.js backbonestore.nw > store.js 14 | % notangle -Rindex.html backbonestore.nw > index.html 15 | 16 | \section{Introduction} 17 | 18 | This is version 3.0 of \textbf{The Backbone Store}, a brief tutorial on 19 | using [[backbone.js]]. The version you are currently reading has been 20 | tested with the latest versions of the supporting software as of April, 21 | 2016. 22 | 23 | \nwanchorto{http://documentcloud.github.com/backbone/}{Backbone.js} is 24 | a popular Model-View-Controller (MVC) library that provides a 25 | framework for creating data-rich, single-page web applications. It 26 | provides (1) a two-layer scheme for separating data from presentation, 27 | (2) a means of automatically synchronizing data with a server in a 28 | RESTful manner, and (3) a mechanism for making some views bookmarkable 29 | and navigable. 30 | 31 | There are a number of other good tutorials for Backbone (See: 32 | \nwanchorto{http://www.plexical.com/blog/2010/11/18/backbone-js-tutorial/}{Meta 33 | Cloud}, 34 | \nwanchorto{http://andyet.net/blog/2010/oct/29/building-a-single-page-app-with-backbonejs-undersc/?utm_source=twitterfeed&utm_medium=twitter}{\&Yet's 35 | Tutorial}, 36 | \nwanchorto{http://bennolan.com/2010/11/24/backbone-jquery-demo.html}{Backbone 37 | Mobile} (which is written in 38 | \nwanchorto{http://jashkenas.github.com/coffee-script/}{Coffee}), and 39 | \nwanchorto{http://joshbohde.com/2010/11/25/backbonejs-and-django/}{Backbone 40 | and Django}. However, a couple of months ago I was attempting to 41 | learn Sammy.js, a library very similar to Backbone, and they had a 42 | nifty tutorial called 43 | \nwanchorto{http://code.quirkey.com/sammy/tutorials/json_store_part1.html}{The 44 | JsonStore}. 45 | 46 | In the spirit of The JSON Store, I present The Backbone Store. 47 | 48 | \subsection{Literate Program} 49 | 50 | A note: this article was written with the 51 | \nwanchorto{http://en.wikipedia.org/wiki/Literate_programming}{Literate 52 | Programming} toolkit 53 | \nwanchorto{http://www.cs.tufts.edu/~nr/noweb/}{Noweb}. Where you see 54 | something that looks like $\langle\langle$this$\rangle\rangle$, it's a 55 | placeholder for code described elsewhere in the document. Placeholders 56 | with an equal sign at the end of them indicate the place where that code 57 | is defined. The link (U->) indicates that the code you're seeing is 58 | used later in the document, and (<-U) indicates it was used earlier but 59 | is being defined here. 60 | 61 | \subsection{Revision} 62 | 63 | This is version 3.0 of \textit{The Backbone Store}. It includes several 64 | significant updates, including the use of both NPM and Bower to build 65 | the final application. 66 | 67 | \subsection{The Store: What We're Going to Build} 68 | 69 | To demonstrate the basics of Backbone, I'm going to create a simple 70 | one-page application, a store for record albums, with two unique views: 71 | a list of all products and a product detail view. I will also put a 72 | shopping cart widget on the page that shows the user how many products 73 | have been has dropped into the cart. I'll use some simple animations to 74 | transition between the catalog and the product detail pages. 75 | 76 | \subsection{Models, Collections, and Controllers} 77 | 78 | Backbone's data layer provides two classes, [[Collection]] and 79 | [[Model]]. 80 | 81 | Every web application has data, often tabular data. Addressing tabular 82 | data usually involves three parts: The \textit{table}, \textit{row}, and 83 | \textit{column}. In Backbone, these are represented by the 84 | [[Collection]], the [[Model]], and the [[attribute]]. The 85 | [[Collection]] often has a URL indicating the back-end source of the 86 | table; the [[Model]] may have a URL indicating its specific row in the 87 | table, as a way of efficiently saving itserlf back to the table. 88 | 89 | To use the Model, you inherit from it using Backbone's own [[.extend()]] 90 | class method, adding or replacing methods in the child object as needed. 91 | For our purposes, we have two models: [[Product]] represents something 92 | we wish to sell, and [[Item]] represents something currently in the 93 | customer's shopping cart. 94 | 95 | The Product literally has nothing to modify. It already provides all 96 | the methods we need. 97 | 98 | Shopping carts are a little odd; the convention is that [[Item]] is not a 99 | single instance of the product, but instead has a reference to the 100 | product, and a count of how many the buyer wants. To that end, I am 101 | adding two methods that extend Item: [[.update()]], which changes the 102 | current quantity, and [[.price()]], which calculates the product's price 103 | times the quantity: 104 | 105 | <>= 106 | var Product = Backbone.Model.extend({}); 107 | 108 | var Item = Backbone.Model.extend({ 109 | update: function(amount) { 110 | if (amount === this.get('quantity')) { 111 | return this; 112 | } 113 | this.set({quantity: amount}, {silent: true}); 114 | this.collection.trigger('update', this); 115 | return this; 116 | }, 117 | 118 | price: function() { 119 | return this.get('product').get('price') * this.get('quantity'); 120 | } 121 | }); 122 | 123 | @ 124 | 125 | The methods [[.get(item)]] and [[.set(item, value)]] are at the heart of 126 | Backbone.Model. They're how you set individual attributes on the object 127 | being manipulated. Notice how I can 'get' the product, which is a 128 | Backbone.Model, and then 'get' its price. This is called a 129 | \textit{chain}, and is fairly common in Javascript. 130 | 131 | The big secret to Backbone is that it supplies an advanced event 132 | management toolkit. Changing a model triggers various events, none of 133 | which matter here in this context so I silence the event, but then I 134 | tell the Item's Backbone.Collection that the Model has changed. For 135 | this program, it is the collection as a whole whose value matters, 136 | because that collection as a whole represents our shopping cart. Events 137 | are the primary way in which Backbone objects interact, so understanding 138 | them is key to using Backbone correctly. 139 | 140 | Collections, like Models, are just objects you can (and often must) 141 | extend to support your application's needs. Just as a Model has 142 | \texttt{.get()} and \texttt{.set()}, a Collection has [[.add(item)]] and 143 | [[.remove(id)]] as methods. Collections have a lot more than that. 144 | 145 | Both Models and Collections also have [[.fetch()]] and [[.save()]]. If 146 | either has a URL, these methods allow the collection to represent data 147 | on the server, and to save that data back to the server. The default 148 | method is a simple JSON object representing either a Model's attributes, 149 | or a JSON list of the Collection's models' attributes. 150 | 151 | The [[Product.Collection]] will be loading its list of albums via these 152 | methods to (in our case) static JSON back-end. Backbone provides a 153 | mechanism for fetching JSON (and you can override the [[.parse()]] 154 | method to handle XML, CSV, or whatever strikes your fancy); to use the 155 | default [[.fetch()]] method, capture and set the Collection's [[.url]] 156 | field: 157 | 158 | <>= 159 | var ProductCollection = Backbone.Collection.extend({ 160 | model: Product, 161 | initialize: function(models, options) { 162 | this.url = options.url; 163 | }, 164 | 165 | comparator: function(product) { 166 | return product.get('title'); 167 | } 168 | }); 169 | 170 | @ 171 | 172 | The [[.model]] attribute tells the [[ProductCollection]] that if 173 | [[.add()]] or [[.fetch()]] are called and the contents are plain JSON, 174 | a new [[Product]] Model should be initialized with the JSON data and 175 | that will be used as a new object for the Collection. 176 | 177 | The [[.comparator()]] method specifies the per-model value by which the 178 | Collection should be sorted. Sorting happens automatically whenever the 179 | Collection receives an event indicating its contents have been altered. 180 | 181 | The [[ItemCollection]] doesn't have a URL, but we do have several helper 182 | methods to add. We don't want to add Items; instead, we want to add 183 | products as needed, then update the count as requested. If the product 184 | is already in our system, we don't want to create duplicates. 185 | 186 | First, we ensure that if we don't receive an amount, we at least provide 187 | a valid \textit{numerical} value to our code. The [[.detect()]] method 188 | lets us find an object in our Collection using a function to compare 189 | them; it returns the first object that matches. 190 | 191 | If we find the object, we update it and return. If we don't, we create 192 | a new one, exploiting the fact that, since we specified the Collection's 193 | Model above, it will automatically be created as a Model in the 194 | Collection at the end of this call. In either case, we return the new 195 | Item to be handled further by the calling code. 196 | 197 | <>= 198 | var ItemCollection = Backbone.Collection.extend({ 199 | model: Item, 200 | 201 | updateItemForProduct: function(product, amount) { 202 | amount = amount != null ? amount : 0; 203 | var pid = product.get('id'); 204 | var item = this.detect(function(obj) { 205 | return obj.get('product').get('id') === pid; 206 | }); 207 | if (item) { 208 | item.update(amount); 209 | return item; 210 | } 211 | return this.add({ 212 | product: product, 213 | quantity: amount 214 | }); 215 | }, 216 | 217 | @ 218 | 219 | And finally, two methods to add up how many objects are in your cart, 220 | and the total price. The first line creates a function to get the 221 | number for a single object and add it to a memo. The second line uses 222 | the [[.reduce()]] method, which goes through each object in the 223 | collection and runs the function, passing the results of each run to the 224 | next as the memo. 225 | 226 | <>= 227 | getTotalCount: function() { 228 | var addup = function(memo, obj) { 229 | return memo + obj.get('quantity'); 230 | }; 231 | return this.reduce(addup, 0); 232 | }, 233 | 234 | getTotalCost: function() { 235 | var addup = function(memo, obj) { 236 | return memo + obj.price(); 237 | }; 238 | return this.reduce(addup, 0); 239 | } 240 | }); 241 | 242 | @ 243 | 244 | \subsection {Views} 245 | 246 | Backbone Views are simple policy objects. They have a root DOM 247 | element, the contents of which they manipulate and to which they 248 | listen for events, and a model or collection they represent within 249 | that element. Views are not rigid; it's just Javascript and the DOM, 250 | and you can hook external events as needed. 251 | 252 | More importantly, if you pass a model or collection to a View, that View 253 | becomes sensitive to events \textit{within its model or collection}, and 254 | can respond to changes automatically. This way, if you have a rich data 255 | ecosystem, when changes to one data item results in a cascade of changes 256 | throughout your datasets, the views will receive ``change'' events and 257 | can update themselves accordingly. 258 | 259 | In a way, a View can be thought of as two separate but important 260 | sub-programs, each based on events. The first listens to events from 261 | the DOM, and forwards data-changing events to associated models or 262 | collections. The second listens to events from data objects and 263 | re-draws the View's contents when the data changes. Keeping these 264 | separate in your mind will help you design Backbone applications 265 | successfully. 266 | 267 | I will show how this works with the shopping cart widget. 268 | 269 | To achieve the [[fadeIn/fadeOut]] animations and enforce consistency, 270 | I'm going to do some basic object-oriented programming. I'm going to 271 | create a base class that contains knowledge about the main area into 272 | which all views are rendered, and that manages these transitions. 273 | 274 | With this technique, you can do lots of navigation-related tricks: you 275 | can highlight where the user is in breadcrumb-style navigation; you 276 | can change the class and highlight an entry on a nav bar; you can add 277 | and remove tabs from the top of the viewport as needed. 278 | 279 | <>= 280 | var BaseView = Backbone.View.extend({ 281 | parent: $('#main'), 282 | className: 'viewport', 283 | 284 | initialize: function(options) { 285 | Backbone.View.prototype.initialize.apply(this, arguments); 286 | this.$el.hide(); 287 | this.parent.append(this.el); 288 | }, 289 | 290 | @ 291 | 292 | The above says that I am creating a class called \texttt{BaseView} and 293 | defining two fields. The first, 'parent', will be used by all child 294 | views to identify into which DOM object the View root element will be 295 | rendered. The second defines a common class we will use for the purpose 296 | of identifying these views to jQuery. Backbone automatically creates a 297 | new [[DIV]] object with the class 'viewport' when a view constructor is 298 | called. It will be our job to attach that [[DIV]] to the DOM. In the 299 | HTML, you will see the [[DIV#main]] object where most of the work will 300 | be rendered. 301 | 302 | As an alternative, the viewport object may already exist, in which case 303 | you just find it with a selector, and the view attaches itself to that 304 | DOM object from then on. In older versions of the Backbone Store, we 305 | used to assign [[this.el]] to a jQuery-wrapped version of the element; 306 | that's no longer necessary, as Backbone provides you with its own 307 | version automatically in [[this.$el]]. 308 | 309 | The 'parent' field is something I created for my own use, since I intend 310 | to have multiple child objects share the same piece of real-estate. The 311 | 'className' field is something Backbone automatically applies to the 312 | generated [[DIV]] at construction time. If you pass in an existing 313 | element at construction time for the View to use (which is not an 314 | uncommon use case!), Backbone will \textit{not} apply the 'className' to 315 | it; you'll have to do that yourself. 316 | 317 | I use the [[initialize]] method above to ensure that the element is 318 | rendered, but not visible, and contained within the [[DIV#main]]. Note 319 | also that the element is not a sacrosanct object; the Backbone.View is 320 | more a collection of standards than a mechanism of enforcement, and so 321 | defining it from a raw DOM object or a jQuery object will not break 322 | anything. 323 | 324 | Next, we will define the hide and show functions. 325 | 326 | <>= 327 | hide: function() { 328 | var dfd = $.Deferred(); 329 | if (!this.$el.is(':visible')) { 330 | return dfd.resolve(); 331 | } 332 | this.$el.fadeOut('fast', function() { 333 | return dfd.resolve(); 334 | }); 335 | return dfd.promise(); 336 | }, 337 | 338 | show: function() { 339 | var dfd = $.Deferred(); 340 | if (this.$el.is(':visible')) { 341 | return dfd.resolve(); 342 | } 343 | this.$el.fadeIn('fast', function() { 344 | return dfd.resolve(); 345 | }); 346 | return dfd.promise(); 347 | } 348 | }); 349 | 350 | @ 351 | 352 | \textbf{Deferred} is a feature of jQuery called ``promises''. It is a 353 | different mechanism for invoking callbacks by attaching attributes and 354 | behavior to the callback function. By using this, we can say thing like 355 | ``\textit{When} everything is hidden (when every deferred returned from 356 | \textbf{hide} has been resolved), \textit{then} show the appropriate 357 | viewport.'' Deferreds are incredibly powerful, and this is a small 358 | taste of what can be done with them. 359 | 360 | Before we move on, let's take a look at the HTML we're going to use for 361 | our one-page application. 362 | 363 | <>= 364 | 365 | 366 | 367 | The Backbone Store 368 | 369 | <> 370 | <> 371 | <> 372 | 373 | 374 | 375 |
376 | 382 |
383 |
384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | @ 392 | 393 | It's not much to look at, but already you can see where that 394 | [[DIV\#main]] goes, as well as where we are putting our templates. The 395 | [[DIV\#main]] will host a number of viewports, only one of which will be 396 | visible at any given time. 397 | 398 | Our first view is going to be the product list view, named, well, guess. 399 | Or just look down a few lines. 400 | 401 | This gives us a chance to discuss one of the big confusions new Backbone 402 | users frequently have: \textit{What is \texttt{render()} for?}. Render 403 | is not there to show or hide the view. \texttt{Render()} is there to 404 | \textit{change the view when the underlying data changes}. It renders 405 | the data into a view. In our functionality, we use the parent class's 406 | \texttt{show()} and \texttt{hide()} methods to actually show the view. 407 | 408 | That call to [[.prototype]] is a Javascript idiom for calling a method 409 | on the parent object. It is, as far as anyone knows, the only way to 410 | invoke a superclass method if it has been redefined in a subclass. It 411 | is rather ugly, but useful. 412 | 413 | <>= 414 | var ProductListView = BaseView.extend({ 415 | id: 'productlistview', 416 | template: _.template($("#store_index_template").html()), 417 | 418 | initialize: function(options) { 419 | BaseView.prototype.initialize.apply(this, arguments); 420 | this.collection.bind('reset', this.render.bind(this)); 421 | }, 422 | 423 | render: function() { 424 | this.$el.html(this.template({ 425 | 'products': this.collection.toJSON() 426 | })); 427 | return this; 428 | } 429 | }); 430 | 431 | @ 432 | %$ 433 | 434 | That \texttt{\_.template()} method is provided by undescore.js, and is a 435 | full-featured, javascript-based templating method. It's not the fastest 436 | or the most feature-complete, but it is more than adequate for our 437 | purposes and it means we don't have to import another library. It 438 | vaguely resembles ERB from Rails, so if you are familiar with that, you 439 | should understand this fairly easily. It takes a template and returns a 440 | function ready to render the template. What we're saying here is that 441 | we want this View to automatically re-render itself every time the given 442 | collection changes in a significant way, using the given template, into 443 | the given element. That's what this view ``means.'' 444 | 445 | There are many different ways of providing templates to Backbone. The 446 | most common, especially for small templates, is to just include it as an 447 | inline string inside the View. The \textit{least} common, I'm afraid, 448 | is the one I'm doing here: using the $<$script$>$ tag with an 449 | unusual mime type to include it with the rest of the HTML. I like this 450 | method because it means all of my HTML is in one place. 451 | 452 | For much larger programs, those that use features such as 453 | \nwanchorto{http://requirejs.org/}{Require.js}, a common technique is to 454 | keep the HTML template fragment in its own file and to import it using 455 | Require's ``text'' plugin. 456 | 457 | Here is the HTML for our home page's template: 458 | 459 | <>= 460 | 477 | 478 | @ 479 | 480 | One of the most complicated objects in our ecosystem is the product 481 | view. It actually does something! The prefix ought to be familiar, 482 | but note that we are again using [[\#main]] as our target; we will be 483 | showing and hiding the various [[DIV]] objects in [[\#main]] again and 484 | again. 485 | 486 | The only trickiness here is twofold: the means by which one calls the 487 | method of a parent class from a child class via Backbone's class 488 | heirarchy, and keeping track of the ItemCollection object, so we can add 489 | and change items as needed. 490 | 491 | <>= 492 | var ProductView = BaseView.extend({ 493 | className: 'productitemview', 494 | template: _.template($("#store_item_template").html()), 495 | 496 | initialize: function(options) { 497 | BaseView.prototype.initialize.apply(this, [options]); 498 | this.itemcollection = options.itemcollection; 499 | }, 500 | 501 | @ 502 | %$ 503 | 504 | There are certain events in which we're interested: keypresses and 505 | clicks on the update button and the quantity form. (Okay, ``UQ'' 506 | isn't the best for ``update quantity''. I admit that.) Note the 507 | peculiar syntax of ``EVENT SELECTOR'': ``methodByName'' for each 508 | event. 509 | 510 | Backbone tells us that the only events it can track by itself are 511 | those that jQuery's ``delegate'' understands. As of 1.5, that seems 512 | to be just about all of them. 513 | 514 | <>= 515 | events: { 516 | "keypress .uqf" : "updateOnEnter", 517 | "click .uq" : "update" 518 | }, 519 | 520 | @ 521 | 522 | And now we will deal with the update. This code ought to be fairly 523 | readable: the only specialness is that it's receiving an event, and 524 | we're ``silencing'' the call to [[cart.add()]], which means that the 525 | cart collection will not publish any events. There are only events 526 | when the item has more than zero, and that gets called on 527 | [[cart_item.update()]]. 528 | 529 | In the original tutorial, this code had a lot of responsibility for 530 | managing the shopping cart, looking into it and seeing if it had an 531 | item for this product, and there was lots of accessing the model to 532 | get its id and so forth. All of that has been put into the shopping 533 | cart model, which is where it belongs: \textit{knowledge about items 534 | and each item's relationship to its collection belongs in the 535 | collection}. 536 | 537 | Look closely at the [[update()]] method. The reference [[this.$]] is a 538 | special Backbone object that limits selectors to objects inside the 539 | element of the view. Without it, jQuery would have found the first 540 | input field of class 'uqf' in the DOM, not the one for this specific 541 | view. [[this.$('.uqf')]] is shorthand for [[$('uqf', this.el)]], and helps 542 | clarify what it is you're looking for. 543 | 544 | <>= 545 | update: function(e) { 546 | e.preventDefault(); 547 | return this.itemcollection.updateItemForProduct(this.model, parseInt(this.$('.uqf').val())); 548 | }, 549 | 550 | updateOnEnter: function(e) { 551 | if (e.keyCode === 13) { 552 | this.update(e); 553 | } 554 | }, 555 | 556 | @ 557 | 558 | The render is straightforward: 559 | 560 | <>= 561 | render: function() { 562 | this.$el.html(this.template(this.model.toJSON())); 563 | return this; 564 | } 565 | }); 566 | 567 | @ 568 | 569 | The product detail template is fairly straightforward. There is no 570 | [[underscore]] magic because there are no loops. 571 | 572 | <>= 573 | 601 | 602 | @ 603 | 604 | So, let's talk about that shopping cart thing. We've been making the 605 | point that when it changes, when you call [[item.update]] within the 606 | product detail view, any corresponding subscribing views sholud 607 | automatically update. 608 | 609 | <>= 610 | var CartWidget = Backbone.View.extend({ 611 | el: $('.cart-info'), 612 | template: _.template($('#store_cart_template').html()), 613 | 614 | initialize: function() { 615 | Backbone.View.prototype.initialize.apply(this, arguments); 616 | this.collection.bind('update', this.render.bind(this)); 617 | }, 618 | 619 | @ 620 | %$ 621 | 622 | And there is the major magic. CartWidget will be initialized with the 623 | ItemCollection; when there is any change in the collection, the widget 624 | will receive the 'change' event, which will automatically trigger the 625 | call to the widget's [[render()]] method. 626 | 627 | The render method will refill that widget's HTML with a re-rendered 628 | template with the new count and cost, and then wiggle it a little to 629 | show that it did changed: 630 | 631 | <>= 632 | render: function() { 633 | var tel = this.$el.html(this.template({ 634 | 'count': this.collection.getTotalCount(), 635 | 'cost': this.collection.getTotalCost() 636 | })); 637 | tel.animate({ paddingTop: '30px' }).animate({ paddingTop: '10px' }); 638 | return this; 639 | } 640 | }); 641 | 642 | @ 643 | 644 | You may have noticed that every render ends in [[return this]]. There's 645 | a reason for that. Render is supposed to be pure statement: it's not 646 | supposed to calculate anything, nor is it supposed to mutate anything on 647 | the Javascript side. It can and frequently does, but that's beside the 648 | point. By returning [[this]], we can then call something immediately 649 | afterward. 650 | 651 | For example, let's say you have a pop-up dialog. It starts life 652 | hidden. You need to update the dialog, then show it: 653 | 654 | <>= 655 | myDialog.render().show(); 656 | 657 | @ 658 | 659 | Because what render() return is [[this]], this code works as expected. 660 | That's how you do chaining in HTML/Javascript. 661 | 662 | Back to our code. The HTML for the Cart widget template is dead simple: 663 | 664 | <>= 665 | 668 | 669 | @ 670 | %$ 671 | 672 | Lastly, there is the [[Router]]. In Backbone, the Router is a 673 | specialized View for invoking other views. It listens for one 674 | specific event: when the [[window.location.hash]] object, the part of 675 | the URL after the hash symbol, changes. When the hash changes, the 676 | Router invokes an event handler. The Router, since its purpose is to 677 | control the major components of the one-page display, is also a good 678 | place to keep all the major components of the sytem. We'll keep track 679 | of the [[Views]], the [[ProductCollection]], and the 680 | [[ItemCollection]]. 681 | 682 | <>= 683 | var BackboneStore = Backbone.Router.extend({ 684 | views: {}, 685 | products: null, 686 | cart: null, 687 | 688 | @ 689 | 690 | There are two events we care about: view the list, and view a detail. 691 | They are routed like this: 692 | 693 | <>= 694 | routes: { 695 | "": "index", 696 | "item/:id": "product" 697 | }, 698 | 699 | @ 700 | 701 | Like most Backbone objects, the Router has an initialization feature. 702 | I create a new, empty shopping cart and corresponding cart widget, 703 | which doesn't render because it's empty. I then create a new 704 | [[ProductCollection]] and and corresponding [[ProductListView]]. 705 | These are all processes that happen immediately. 706 | 707 | What does not happen immediately is the [[fetch()]] of data from the 708 | back-end server. For that, I use the jQuery deferred again, because 709 | [[fetch()]] ultimately returns the results of [[sync()]], which 710 | returns the result of an [[ajax()]] call, which is a deferred. 711 | 712 | <>= 713 | initialize: function(data) { 714 | Backbone.Router.prototype.initialize.apply(this, arguments); 715 | this.cart = new ItemCollection(); 716 | new CartWidget({ collection: this.cart }); 717 | this.products = new ProductCollection([], { url: 'data/items.json' }); 718 | this.views = { 719 | '_index': new ProductListView({ collection: this.products }) 720 | }; 721 | $.when(this.products.fetch({ reset: true })).then(function() { 722 | return window.location.hash = ''; 723 | }); 724 | }, 725 | 726 | @ 727 | %$ 728 | 729 | There are two things to route \textit{to}, but we must also route 730 | \textit{from}. Remember that our two major views, the product list 731 | and the product detail, inherited from [[\_BaseView]], which has the 732 | [[hide()]] and [[show()]] methods. We want to hide all the views, 733 | then show the one invoked. First, let's hide every view we know 734 | about. [[hide()]] returns either a deferred (if the object is being 735 | hidden) or null. The [[_.filter()]] call at the end means that this 736 | method returns only an array of deferreds. 737 | 738 | <>= 739 | hideAllViews: function() { 740 | return _.filter(_.map(this.views, function(v) { return v.hide(); }), 741 | function(t) { return t !== null; }); 742 | }, 743 | 744 | @ 745 | 746 | Showing the product list view is basically hiding everything, then 747 | showing the index. The function [[$$.when]] takes arguments of what to 748 | wait for; to make it take an array of arguments, you use the 749 | [[.apply()]] method. 750 | 751 | <>= 752 | index: function() { 753 | var view = this.views['_index']; 754 | return $.when.apply($, this.hideAllViews()).then(function() { 755 | return view.show(); 756 | }); 757 | }, 758 | 759 | @ 760 | 761 | On the other hand, showing the product detail page is a bit trickier. 762 | In order to avoid re-rendering all the time, I am going to create a 763 | view for every product in which the user shows interest, and keep it 764 | around, showing it a second time if the user wants to see it a second 765 | time. Note that the view only needs to be rendered \textit{once}, after 766 | which we can just hide or show it on request. 767 | 768 | Not that we pass it the [[ItemCollection]] instance. It uses this to 769 | create a new item, which (if you recall from our discussion of 770 | [[getOrCreateItemForProduct()]]) is automagically put into the 771 | collection as needed. Which means all we need to do is update this 772 | item and the item collection \textit{changes}, which in turn causes 773 | the [[CartWidget]] to update automagically as well. 774 | 775 | <>= 776 | product: function(id) { 777 | var view = this.views[id]; 778 | if (!view) { 779 | var product = this.products.detect(function(p) { 780 | return p.get('id') === id; 781 | }); 782 | view = this.views[id] = new ProductView({ 783 | model: product, 784 | itemcollection: this.cart 785 | }).render(); 786 | } 787 | return $.when(this.hideAllViews()).then(function() { 788 | return view.show(); 789 | }); 790 | } 791 | }); 792 | 793 | @ 794 | %$ 795 | 796 | Finally, we need to start the program 797 | 798 | <>= 799 | $(document).ready(function() { 800 | new BackboneStore(); 801 | return Backbone.history.start(); 802 | }); 803 | 804 | @ 805 | %$ 806 | 807 | \section{The Program} 808 | 809 | Here's the entirety of the program. Coffeescript provides its own 810 | namespace wrapper: 811 | 812 | <>= 813 | <> 814 | 815 | <> 816 | 817 | <> 818 | 819 | <> 820 | 821 | <> 822 | 823 | <> 824 | 825 | <> 826 | 827 | <> 828 | 829 | <> 830 | @ 831 | 832 | And that's it. Put it all together, and you've got yourself a working 833 | Backbone Store. 834 | 835 | This code is available at my github at 836 | \nwanchorto{https://github.com/elfsternberg/The-Backbone-Store}{The 837 | Backbone Store}. 838 | 839 | \end{document} 840 | --------------------------------------------------------------------------------