diff --git a/Makefile b/Makefile index 4d4bd12..d76c15a 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,31 @@ -.SUFFIXES: .nw .js .pdf .html .tex +.SUFFIXES: .nw .js .pdf .html .tex .haml .css .stylus NOTANGLE= notangle NOWEAVE= noweave ECHO= /bin/echo +STYLUS= stylus +HAML= haml +COFFEE= coffee -all: index.html store.js +all: index.html store.js jsonstore.css -.nw.html: - $(NOWEAVE) -filter l2h -delay -x -index -autodefs c -html $*.nw > $*.html +index.html: index.haml + $(HAML) --unix-newlines --no-escape-attrs --double-quote-attribute $*.haml > $*.html + +index.haml: backbonestore.nw + $(NOTANGLE) -c -R$@ $< > $*.haml + +jsonstore.css: jsonstore.styl + $(STYLUS) $*.styl + +jsonstore.styl: backbonestore.nw + $(NOTANGLE) -c -R$@ $< > $@ + +store.js: store.coffee + $(COFFEE) --compile $< + +store.coffee: backbonestore.nw + $(NOTANGLE) -c -R$@ $< > $@ .nw.tex: $(NOWEAVE) -x -delay $*.nw > $*.tex #$ @@ -19,47 +37,6 @@ all: index.html store.js xelatex *$.tex; \ done -.nw.js: - @ $(ECHO) $(NOTANGLE) -c -R$@ $< - @ - $(NOTANGLE) -c -R$@ $< > $*.nw-js-tmp - @ if [ -s "$*.nw-js-tmp" ]; then \ - mv $*.nw-js-tmp $@; \ - else \ - echo "$@ not found in $<"; \ - rm $*.nw-js-tmp; \ - fi - -jsonstore.styl: backbonestore.nw - @ $(ECHO) $(NOTANGLE) -c -R$@ $< - @ - $(NOTANGLE) -c -R$@ $< > $*.nw-styl-tmp - @ if [ -s "$*.nw-styl-tmp" ]; then \ - mv $*.nw-styl-tmp $@; \ - else \ - echo "$@ not found in $<"; \ - rm $*.nw-styl-tmp; \ - fi - -store.js: backbonestore.nw - @ $(ECHO) $(NOTANGLE) -c -R$@ $< - @ - $(NOTANGLE) -c -R$@ $< > $*.nw-html-tmp - @ if [ -s "$*.nw-html-tmp" ]; then \ - mv $*.nw-html-tmp $@; \ - else \ - echo "$@ not found in $<"; \ - rm $*.nw-tmp; \ - fi - -index.html: - @ $(ECHO) $(NOTANGLE) -c -R$@ $< - @ - $(NOTANGLE) -c -R$@ $< > $*.nw-html-tmp - @ if [ -s "$*.nw-html-tmp" ]; then \ - mv $*.nw-html-tmp $@; \ - else \ - echo "$@ not found in $<"; \ - rm $*.nw-tmp; \ - fi - - clean: - rm -f *.tex *.dvi *.aux *.toc *.log *.out *.html *.js diff --git a/backbonestore.nw b/backbonestore.nw index 8e4dd51..834db60 100644 --- a/backbonestore.nw +++ b/backbonestore.nw @@ -319,7 +319,7 @@ Before we move on, let's take a look at the HAML we're going to use for our one-page application. The code below compiles beautifully into the same HTML seen in the original Backbone Store. -<>= +<>= !!! 5 %html{:xmlns => "http://www.w3.org/1999/xhtml"} %head @@ -688,7 +688,7 @@ $(document).ready () -> Here's the entirety of the program. Coffeescript provides its own namespace wrapper: -<>= +<>= <> <> @@ -779,11 +779,16 @@ body img border: 0 +#productlistview + ul + list-style: none + .item float:left width: 250px - margin-right: 3px - padding: 2px + margin-right: 10px + margin-bottom: 10px + padding: 5px rounded(5px) border: 1px solid #ccc text-align:center diff --git a/index.haml b/index.haml index 6db9d82..f736192 100644 --- a/index.haml +++ b/index.haml @@ -3,7 +3,6 @@ %head %title The Backbone Store %link{:charset => "utf-8", :href => "jsonstore.css", :rel => "stylesheet", :type => "text/css"}/ - %script#store_index_template(type="text/x-underscore-tmplate") %h1 Product Catalog %ul @@ -12,20 +11,20 @@ .item-image %a{:href => "#item/<%= p.id %>"} %img{:src => "<%= p.image %>", :alt => "<%= p.title %>"}/ - .item-artist <%= p.artist %> - .item-title <%= p.title %> - .item-price $<%= p.price %> + .item-artist <%= p.artist %> + .item-title <%= p.title %> + .item-price $<%= p.price %> <% } %> %script#store_item_template(type= "text/x-underscore-template") - .item-detail + .item-detail .item-image %img(src="<%= large_image %>" alt="<%= title %>")/ - .item-info + .item-info .item-artist <%= artist %> .item-title <%= title %> .item-price $<%= price %> - .item-form + .item-form %form(action="#/cart" method="post") %p %label Quantity: @@ -37,17 +36,17 @@ %a(href="<%= url %>") Buy this item on Amazon .back-link %a(href="#") « Back to Items - %script#store_cart_template(type="text/x-underscore-template") %p Items: <%= count %> ($<%= cost %>) + %body #container #header %h1 The Backbone Store .cart-info - #main + #main %script{:src => "jquery-1.6.2.min.js", :type => "text/javascript"} %script{:src => "underscore.js", :type => "text/javascript"} %script{:src => "backbone.js", :type => "text/javascript"} diff --git a/index.html b/index.html index c0926f3..add6bdb 100644 --- a/index.html +++ b/index.html @@ -13,10 +13,10 @@ <%= p.title %> +
<%= p.artist %>
+
<%= p.title %>
+
$<%= p.price %>
-
<%= p.artist %>
-
<%= p.title %>
-
$<%= p.price %>
<% } %> @@ -49,6 +49,7 @@ +
diff --git a/jsonstore.css b/jsonstore.css index 47465e0..80b30cd 100644 --- a/jsonstore.css +++ b/jsonstore.css @@ -29,11 +29,15 @@ body { img { border: 0; } +#productlistview ul { + list-style: none; +} .item { float: left; width: 250px; - margin-right: 3px; - padding: 2px; + margin-right: 10px; + margin-bottom: 10px; + padding: 5px; -moz-border-radius-topleft: 5px; -moz-border-radius-topright: 5px; -moz-border-radius-bottomleft: 5px; diff --git a/jsonstore.styl b/jsonstore.styl new file mode 100644 index 0000000..e0e72a8 --- /dev/null +++ b/jsonstore.styl @@ -0,0 +1,76 @@ +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 + +body + font-family: "Lucida Grande", Lucida, Helvetica, Arial, sans-serif + background: #FFF + color: #333 + margin: 0px + padding: 0px + + +#header + background: #C97E41 + margin: 0px + padding: 20px + + 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: #714625 + color: #FFF + font-size: 12px + font-weight: bold + +img + border: 0 + +#productlistview + 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 + + .item-image + float:left + + .item-info + padding: 100px 10px 0px 10px + diff --git a/store.coffee b/store.coffee index 455ae3a..aa97be2 100644 --- a/store.coffee +++ b/store.coffee @@ -10,10 +10,13 @@ class ProductCollection extends Backbone.Collection comparator: (item) -> item.get('title') - class Item extends Backbone.Model update: (amount) -> - @set {quantity: @get('quantity') + amount} + @set + quantity: @get('quantity') + + price: () -> + @get('product').get('price') * @get('quantity') class ItemCollection extends Backbone.Collection model: Item @@ -34,10 +37,9 @@ class ItemCollection extends Backbone.Collection @reduce addup, 0 getTotalCost: () -> - addup = (memo, obj) -> - (obj.get('product').get('price') * - obj.get('quantity')) + memo - @reduce addup, 0 + addup = (memo, obj) ->obj.price() + memo + @reduce(addup, 0); + class _BaseView extends Backbone.View parent: $('#main') @@ -52,11 +54,7 @@ class _BaseView extends Backbone.View hide: () -> if not @el.is(':visible') return null - - # Make a note here about how the => operator replaces the need for - # _.bind() promise = $.Deferred (dfd) => @el.fadeOut('fast', dfd.resolve) - @trigger 'hide', @ promise.promise() show: () -> @@ -64,7 +62,6 @@ class _BaseView extends Backbone.View return promise = $.Deferred (dfd) => @el.fadeIn('fast', dfd.resolve) - @trigger 'show', @ promise.promise() @@ -80,6 +77,7 @@ class ProductListView extends _BaseView @el.html(_.template(@template, {'products': @collection.toJSON()})) @ + class ProductView extends _BaseView id: 'productitemview' template: $("#store_item_template").html() @@ -95,7 +93,7 @@ class ProductView extends _BaseView update: (e) -> e.preventDefault() - @item.update parseInt($('.uqf').val()) + @item.update parseInt(@$('.uqf').val()) updateOnEnter: (e) -> if (e.keyCode == 13) @@ -105,6 +103,7 @@ class ProductView extends _BaseView @el.html(_.template(@template, @model.toJSON())); @ + class CartWidget extends Backbone.View el: $('.cart-info') template: $('#store_cart_template').html() @@ -119,6 +118,7 @@ class CartWidget extends Backbone.View tel.animate({paddingTop: '30px'}).animate({paddingTop: '10px'}) @ + class BackboneStore extends Backbone.Router views: {} products: null @@ -146,6 +146,7 @@ class BackboneStore extends Backbone.Router _.select(_.map(@views, (v) -> return v.hide()), (t) -> t != null) + index: () -> view = @views['_index'] $.when(@hideAllViews()).then(() -> view.show()) @@ -159,6 +160,7 @@ class BackboneStore extends Backbone.Router $.when(@hideAllViews()).then( () -> view.show()) + $(document).ready () -> new BackboneStore(); Backbone.history.start(); diff --git a/store.js b/store.js index 5184881..8e92e4f 100644 --- a/store.js +++ b/store.js @@ -1,203 +1,264 @@ (function() { - - var Product = Backbone.Model.extend({}) - - var ProductCollection = Backbone.Collection.extend({ - model: Product, - - initialize: function(models, options) { - this.url = options.url; - }, - - comparator: function(item) { - return item.get('title'); - } - }); - - var Item = Backbone.Model.extend({ - update: function(amount) { - this.set({'quantity': this.get('quantity') + amount}); - } - }); - - var ItemCollection = Backbone.Collection.extend({ - model: Item, - getOrCreateItemForProduct: function(product) { - var i, - pid = product.get('id'), - o = this.detect(function(obj) { - return (obj.get('product').get('id') == pid); - }); - if (o) { - return o; - } - i = new Item({'product': product, 'quantity': 0}) - this.add(i, {silent: true}) - return i; - }, - getTotalCount: function() { - return this.reduce(function(memo, obj) { - return obj.get('quantity') + memo; }, 0); - }, - getTotalCost: function() { - return this.reduce(function(memo, obj) { - return (obj.get('product').get('price') * - obj.get('quantity')) + memo; }, 0); - - } - }); - - var _BaseView = Backbone.View.extend({ - parent: $('#main'), - className: 'viewport', - - initialize: function() { - this.el = $(this.el); - this.el.hide(); - this.parent.append(this.el); - return this; - }, - - hide: function() { - if (this.el.is(":visible") === false) { - return null; - } - promise = $.Deferred(_.bind(function(dfd) { - this.el.fadeOut('fast', dfd.resolve)}, this)).promise(); - this.trigger('hide', this); - return promise; - }, - - show: function() { - if (this.el.is(':visible')) { - return; - } - promise = $.Deferred(_.bind(function(dfd) { - this.el.fadeIn('fast', dfd.resolve) }, this)).promise(); - - this.trigger('show', this); - return promise; - } - }); - - var ProductListView = _BaseView.extend({ - id: 'productlistview', - template: $("#store_index_template").html(), - - initialize: function(options) { - this.constructor.__super__.initialize.apply(this, [options]) - this.collection.bind('reset', _.bind(this.render, this)); - }, - - render: function() { - this.el.html(_.template(this.template, - {'products': this.collection.toJSON()})) - return this; - } - }); - - var ProductView = _BaseView.extend({ - id: 'productitemview', - template: $("#store_item_template").html(), - initialize: function(options) { - this.constructor.__super__.initialize.apply(this, [options]) - this.itemcollection = options.itemcollection; - this.item = this.itemcollection.getOrCreateItemForProduct(this.model); - return this; - }, - - events: { - "keypress .uqf" : "updateOnEnter", - "click .uq" : "update", - }, - - update: function(e) { - e.preventDefault(); - this.item.update(parseInt($('.uqf').val())); - }, - - updateOnEnter: function(e) { - if (e.keyCode == 13) { - return this.update(e); - } - }, - - render: function() { - this.el.html(_.template(this.template, this.model.toJSON())); - return this; - } - }); - - var CartWidget = Backbone.View.extend({ - el: $('.cart-info'), - template: $('#store_cart_template').html(), - - initialize: function() { - this.collection.bind('change', _.bind(this.render, this)); - }, - - render: function() { - console.log(arguments); - this.el.html( - _.template(this.template, { - 'count': this.collection.getTotalCount(), - 'cost': this.collection.getTotalCost() - })).animate({paddingTop: '30px'}) - .animate({paddingTop: '10px'}); - } - }); - - var BackboneStore = Backbone.Router.extend({ - views: {}, - products: null, - cart: null, - - routes: { - "": "index", - "item/:id": "product", - }, - - initialize: function(data) { - 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() { window.location.hash = ''; }); - return this; - }, - - hideAllViews: function () { - return _.select( - _.map(this.views, function(v) { return v.hide(); }), - function (t) { return t != null }); - }, - - index: function() { - var view = this.views['_index']; - $.when(this.hideAllViews()).then( - function() { return view.show(); }); - }, - - product: function(id) { - var product, v, view; - product = this.products.detect(function(p) { return p.get('id') == (id); }) - view = ((v = this.views)['item.' + id]) || (v['item.' + id] = ( - new ProductView({model: product, - itemcollection: this.cart}).render())); - $.when(this.hideAllViews()).then( - function() { view.show(); }); - } - }); - - $(document).ready(function() { - new BackboneStore(); - Backbone.history.start(); - }); + var BackboneStore, CartWidget, Item, ItemCollection, Product, ProductCollection, ProductListView, ProductView, _BaseView; + var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { + for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } + function ctor() { this.constructor = child; } + ctor.prototype = parent.prototype; + child.prototype = new ctor; + child.__super__ = parent.prototype; + return child; + }, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + Product = (function() { + __extends(Product, Backbone.Model); + function Product() { + Product.__super__.constructor.apply(this, arguments); + } + return Product; + })(); + ProductCollection = (function() { + __extends(ProductCollection, Backbone.Collection); + function ProductCollection() { + ProductCollection.__super__.constructor.apply(this, arguments); + } + ProductCollection.prototype.model = Product; + ProductCollection.prototype.initialize = function(models, options) { + this.url = options.url; + return this; + }; + ProductCollection.prototype.comparator = function(item) { + return item.get('title'); + }; + return ProductCollection; + })(); + Item = (function() { + __extends(Item, Backbone.Model); + function Item() { + Item.__super__.constructor.apply(this, arguments); + } + Item.prototype.update = function(amount) { + return this.set({ + quantity: this.get('quantity') + }); + }; + Item.prototype.price = function() { + return this.get('product').get('price') * this.get('quantity'); + }; + return Item; + })(); + ItemCollection = (function() { + __extends(ItemCollection, Backbone.Collection); + function ItemCollection() { + ItemCollection.__super__.constructor.apply(this, arguments); + } + ItemCollection.prototype.model = Item; + ItemCollection.prototype.getOrCreateItemForProduct = function(product) { + var i, pid; + pid = product.get('id'); + i = this.detect(function(obj) { + return obj.get('product').get('id') === pid; + }); + if (i) { + return i; + } + i = new Item({ + product: product, + quantity: 0 + }); + this.add(i, { + silent: true + }); + return i; + }; + ItemCollection.prototype.getTotalCount = function() { + var addup; + addup = function(memo, obj) { + return obj.get('quantity') + memo; + }; + return this.reduce(addup, 0); + }; + ItemCollection.prototype.getTotalCost = function() { + var addup; + addup = function(memo, obj) { + return obj.price() + memo; + }; + return this.reduce(addup, 0); + }; + return ItemCollection; + })(); + _BaseView = (function() { + __extends(_BaseView, Backbone.View); + function _BaseView() { + _BaseView.__super__.constructor.apply(this, arguments); + } + _BaseView.prototype.parent = $('#main'); + _BaseView.prototype.className = 'viewport'; + _BaseView.prototype.initialize = function() { + this.el = $(this.el); + this.el.hide(); + this.parent.append(this.el); + return this; + }; + _BaseView.prototype.hide = function() { + var promise; + if (!this.el.is(':visible')) { + return null; + } + promise = $.Deferred(__bind(function(dfd) { + return this.el.fadeOut('fast', dfd.resolve); + }, this)); + return promise.promise(); + }; + _BaseView.prototype.show = function() { + var promise; + if (this.el.is(':visible')) { + return; + } + promise = $.Deferred(__bind(function(dfd) { + return this.el.fadeIn('fast', dfd.resolve); + }, this)); + return promise.promise(); + }; + return _BaseView; + })(); + ProductListView = (function() { + __extends(ProductListView, _BaseView); + function ProductListView() { + ProductListView.__super__.constructor.apply(this, arguments); + } + ProductListView.prototype.id = 'productlistview'; + ProductListView.prototype.template = $("#store_index_template").html(); + ProductListView.prototype.initialize = function(options) { + this.constructor.__super__.initialize.apply(this, [options]); + return this.collection.bind('reset', _.bind(this.render, this)); + }; + ProductListView.prototype.render = function() { + this.el.html(_.template(this.template, { + 'products': this.collection.toJSON() + })); + return this; + }; + return ProductListView; + })(); + ProductView = (function() { + __extends(ProductView, _BaseView); + function ProductView() { + ProductView.__super__.constructor.apply(this, arguments); + } + ProductView.prototype.id = 'productitemview'; + ProductView.prototype.template = $("#store_item_template").html(); + ProductView.prototype.initialize = function(options) { + this.constructor.__super__.initialize.apply(this, [options]); + this.itemcollection = options.itemcollection; + this.item = this.itemcollection.getOrCreateItemForProduct(this.model); + return this; + }; + ProductView.prototype.events = { + "keypress .uqf": "updateOnEnter", + "click .uq": "update" + }; + ProductView.prototype.update = function(e) { + e.preventDefault(); + return this.item.update(parseInt(this.$('.uqf').val())); + }; + ProductView.prototype.updateOnEnter = function(e) { + if (e.keyCode === 13) { + return this.update(e); + } + }; + ProductView.prototype.render = function() { + this.el.html(_.template(this.template, this.model.toJSON())); + return this; + }; + return ProductView; + })(); + CartWidget = (function() { + __extends(CartWidget, Backbone.View); + function CartWidget() { + CartWidget.__super__.constructor.apply(this, arguments); + } + CartWidget.prototype.el = $('.cart-info'); + CartWidget.prototype.template = $('#store_cart_template').html(); + CartWidget.prototype.initialize = function() { + return this.collection.bind('change', _.bind(this.render, this)); + }; + CartWidget.prototype.render = function() { + var tel; + tel = this.el.html(_.template(this.template, { + 'count': this.collection.getTotalCount(), + 'cost': this.collection.getTotalCost() + })); + tel.animate({ + paddingTop: '30px' + }).animate({ + paddingTop: '10px' + }); + return this; + }; + return CartWidget; + })(); + BackboneStore = (function() { + __extends(BackboneStore, Backbone.Router); + function BackboneStore() { + BackboneStore.__super__.constructor.apply(this, arguments); + } + BackboneStore.prototype.views = {}; + BackboneStore.prototype.products = null; + BackboneStore.prototype.cart = null; + BackboneStore.prototype.routes = { + "": "index", + "item/:id": "product" + }; + BackboneStore.prototype.initialize = function(data) { + 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 = ''; + }); + return this; + }; + BackboneStore.prototype.hideAllViews = function() { + return _.select(_.map(this.views, function(v) { + return v.hide(); + }), function(t) { + return t !== null; + }); + }; + BackboneStore.prototype.index = function() { + var view; + view = this.views['_index']; + return $.when(this.hideAllViews()).then(function() { + return view.show(); + }); + }; + BackboneStore.prototype.product = function(id) { + var product, view, _base, _name; + product = this.products.detect(function(p) { + return p.get('id') === id; + }); + view = ((_base = this.views)[_name = 'item.' + id] || (_base[_name] = new ProductView({ + model: product, + itemcollection: this.cart + }).render())); + return $.when(this.hideAllViews()).then(function() { + return view.show(); + }); + }; + return BackboneStore; + })(); + $(document).ready(function() { + new BackboneStore(); + return Backbone.history.start(); + }); }).call(this);