diff --git a/.gitignore b/.gitignore index 13afa13..ab8d13e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ .#* .DS_Store *~ +node_modules/* +bower_components/* +npm-debug.log +docs/*.html +docs/*.tex +htdocs/lib +package.yml diff --git a/bin/autoreload b/bin/autoreload new file mode 100755 index 0000000..1558b47 --- /dev/null +++ b/bin/autoreload @@ -0,0 +1,25 @@ +var fs = require('fs'); +var Inotify = require('inotify').Inotify; + +var spawn = require('child_process').spawn; + +var spew = function(data) { + return console.log(data.toString('utf8')); +}; + +var server = spawn('./node_modules/http-server/bin/http-server', ['./htdocs/']); +server.stdout.on('data', spew); + +var monitor = new Inotify(); + +var reBuild = function() { + var maker = spawn('make', ['store']); + return maker.stdout.on('data', spew); + }; + +monitor.addWatch({ + path: "./src/backbonestore.nw", + watch_for: Inotify.IN_CLOSE_WRITE, + callback: reBuild +}); + diff --git a/package.json b/package.json new file mode 100644 index 0000000..7370168 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "the-backbone-store", + "version": "3.0.1", + "description": "A comprehensive (one hopes) tutorial on a simple development platform for Backbone.", + "main": "htdocs/index.html", + "dependencies": { + "http-server": "^0.9.0" + }, + "devDependencies": { + "inotify": "^1.4.0", + "bower": "^1.7.0" + }, + "scripts": { + "test": "make serve" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/elfsternberg/The-Backbone-Store.git" + }, + "keywords": [ + "backbone", + "javascript", + "makefiles", + "node", + "tutorial" + ], + "author": "Kenneth M. \"Elf\" Sternberg ", + "license": "BSD-2-Clause", + "bugs": { + "url": "https://github.com/elfsternberg/The-Backbone-Store/issues" + }, + "homepage": "https://github.com/elfsternberg/The-Backbone-Store#readme" +} diff --git a/src/backbonestore.nw b/src/backbonestore.nw index 1ff7248..e8a82a6 100644 --- a/src/backbonestore.nw +++ b/src/backbonestore.nw @@ -116,6 +116,7 @@ var Item = Backbone.Model.extend({ return this.get('product').get('price') * this.get('quantity'); } }); + @ The methods [[.get(item)]] and [[.set(item, value)]] are at the heart of @@ -149,8 +150,9 @@ methods to (in our case) static JSON back-end. var ProductCollection = Backbone.Collection.extend({ model: Product, initialize: function(models, options) { - return this.url = options.url; + this.url = options.url; }, + comparator: function(item) { return item.get('title'); } @@ -448,23 +450,23 @@ Require's ``text'' plugin. Here is the HTML for our home page's template: <>= - + @ @@ -480,12 +482,14 @@ heirarchy, and keeping track of the ItemCollection object, so we can add and change items as needed. <>= -class ProductView extends _BaseView - className: 'productitemview' - template: $("#store_item_template").html() - initialize: (options) -> - _BaseView.prototype.initialize.apply @, [options] - @itemcollection = options.itemcollection +var ProductView = BaseView.extend({ + className: 'productitemview', + template: _.template($("#store_item_template").html()), + + initialize: function(options) { + BaseView.prototype.initialize.apply(this, [options]); + this.itemcollection = options.itemcollection; + }, @ %$ @@ -501,9 +505,10 @@ those that jQuery's ``delegate'' understands. As of 1.5, that seems to be just about all of them. <>= - events: + events: { "keypress .uqf" : "updateOnEnter" "click .uq" : "update" + }, @ @@ -530,21 +535,27 @@ view. [[@\$('.uqf')]] is shorthand for [[$('uqf', @el)]], and helps clarify what it is you're looking for. <>= - update: (e) -> - e.preventDefault() - @itemcollection.updateItemForProduct @model, parseInt(@$('.uqf').val()) + update: function(e) { + e.preventDefault(); + return this.itemcollection.updateItemForProduct(this.model, parseInt(this.$('.uqf').val())); + }, - updateOnEnter: (e) -> - @update(e) if e.keyCode == 13 + updateOnEnter: function(e) { + if (e.keyCode === 13) { + return this.update(e); + } + }, @ The render is straightforward: <>= - render: () -> - @el.html(_.template(@template)(@model.toJSON())); - @ + render: function() { + this.el.html(this.template(this.model.toJSON())); + return this; + } +}); @ @@ -552,26 +563,35 @@ 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 @@ -580,12 +600,14 @@ product detail view, any corresponding subscribing views sholud automatically update. <>= -class CartWidget extends Backbone.View - el: $('.cart-info') - template: $('#store_cart_template').html() +var CartWidget = Backbone.View.extend({ + el: $('.cart-info'), + template: _.template($('#store_cart_template').html()), - initialize: () -> - @collection.bind 'update', @render.bind @ + initialize: function() { + Backbone.View.prototype.initialize.apply(this, arguments); + this.collection.bind('update', this.render.bind(this)); + }, @ %$ @@ -600,21 +622,42 @@ template with the new count and cost, and then wiggle it a little to show that it did changed: <>= - render: () -> - tel = @$el.html _.template(@template)({ - 'count': @collection.getTotalCount() - 'cost': @collection.getTotalCost() - }) - tel.animate({paddingTop: '30px'}).animate({paddingTop: '10px'}) - @ + CartWidget.prototype.render = function() { + var tel = this.$el.html(this.template({ + 'count': this.collection.getTotalCount(), + 'cost': this.collection.getTotalCost() + })); + tel.animate({ paddingTop: '30px' }).animate({ paddingTop: '10px' }); + return this; + } +}); @ -And the HTML for the template is dead simple: +You may have noticed that every render ends in [[return this]]. There's +a reason for that. Render is supposed to be pure statement: it's not +supposed to calculate anything, nor is it supposed to mutate anything on +the Javascript side. It can and frequently does, but that's beside the +point. By returning [[this]], we can then call something immediately +afterward. + +For example, let's say you have a pop-up dialog. It starts life +hidden. You need to update the dialog, then show it: + +<>= + myDialog.render().show(); + +@ + +Because what render() return is [[this]], this code works as expected. +That's how you do chaining in HTML/Javascript. + +Back to our code. The HTML for the Cart widget template is dead simple: <>= -%script#store_cart_template(type="text/x-underscore-template") - %p Items: <%= count %> ($<%= cost %>) + @ %$ @@ -630,10 +673,10 @@ of the [[Views]], the [[ProductCollection]], and the [[ItemCollection]]. <>= -class BackboneStore extends Backbone.Router - views: {} - products: null - cart: null +var BackboneStore = Backbone.Router.extend({ + views: {}, + products: null, + cart: null, @ @@ -641,9 +684,10 @@ There are two events we care about: view the list, and view a detail. They are routed like this: <>= - routes: - "": "index" + routes: { + "": "index", "item/:id": "product" + }, @ @@ -659,19 +703,18 @@ back-end server. For that, I use the jQuery deferred again, because returns the result of an [[ajax()]] call, which is a deferred. <>= - initialize: (data) -> - @cart = new ItemCollection() - new CartWidget - collection: @cart - - @products = new ProductCollection [], - url: 'data/items.json' - @views = - '_index': new ProductListView - collection: @products - $.when(@products.fetch({reset: true})) - .then(() -> window.location.hash = '') - @ + initialize: function(data) { + Backbone.Router.prototype.initialize.apply(this, arguments); + this.cart = new ItemCollection(); + new CartWidget({ collection: this.cart }); + this.products = new ProductCollection([], { url: 'data/items.json' }); + this.views = { + '_index': new ProductListView({ collection: this.products }) + }; + $.when(this.products.fetch({ reset: true })).then(function() { + return window.location.hash = ''; + }); + }, @ %$ @@ -682,14 +725,14 @@ and the product detail, inherited from [[\_BaseView]], which has the [[hide()]] and [[show()]] methods. We want to hide all the views, then show the one invoked. First, let's hide every view we know about. [[hide()]] returns either a deferred (if the object is being -hidden) or null. The [[_.select()]] call at the end means that this +hidden) or null. The [[_.filter()]] call at the end means that this method returns only an array of deferreds. <>= - hideAllViews: () -> - _.select(_.map(@views, (v) -> return v.hide()), - (t) -> t != null) - + hideAllViews = function() { + return _.filter(_.map(this.views, function(v) { return v.hide(); }), + function(t) { return t !== null; }); + }, @ @@ -699,9 +742,12 @@ wait for; to make it take an array of arguments, you use the [[.apply()]] method. <>= - index: () -> - view = @views['_index'] - $.when.apply($, @hideAllViews()).then(() -> view.show()) + index: function() { + var view = this.views['_index']; + return $.when.apply($, this.hideAllViews()).then(function() { + return view.show(); + }); + }, @ @@ -709,7 +755,8 @@ 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. Note that the view only needs to be rendered \textit{once}, after +which we can just hide or show it on request. Not that we pass it the [[ItemCollection]] instance. It uses this to create a new item, which (if you recall from our discussion of @@ -719,14 +766,22 @@ item and the item collection \textit{changes}, which in turn causes the [[CartWidget]] to update automagically as well. <>= - 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()) + product: function(id) { + var view = this.views[id]; + if (!view) { + var product = this.products.detect(function(p) { + return p.get('id') === id; + }); + view = this.views[id] = new ProductView({ + model: product, + itemcollection: this.cart + }).render() + } + return $.when(this.hideAllViews()).then(function() { + return view.show(); + }); + } +}); @ %$ @@ -734,9 +789,11 @@ the [[CartWidget]] to update automagically as well. Finally, we need to start the program <>= -$(document).ready () -> - new BackboneStore(); - Backbone.history.start(); + $(document).ready(function() { + new BackboneStore(); + return Backbone.history.start(); + }); + @ %$ @@ -765,131 +822,6 @@ namespace wrapper: <> @ -\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.