From d0af7f22e6f290218047a2937fa43b936c66ed3c Mon Sep 17 00:00:00 2001 From: "Elf M. Sternberg" Date: Sat, 16 Apr 2016 21:31:53 -0700 Subject: [PATCH] Working now. --- Makefile | 11 +-- bin/autoreload | 1 + htdocs/index.html | 75 ++++++++++++++ htdocs/jsonstore.css | 91 +++++++++++++++++ htdocs/store.js | 227 +++++++++++++++++++++++++++++++++++++++++++ src/backbonestore.nw | 39 ++++---- 6 files changed, 418 insertions(+), 26 deletions(-) create mode 100644 htdocs/index.html create mode 100644 htdocs/jsonstore.css create mode 100644 htdocs/store.js diff --git a/Makefile b/Makefile index baed8bf..4f493e0 100644 --- a/Makefile +++ b/Makefile @@ -6,14 +6,14 @@ ECHO= echo LIBS:= htdocs/lib/underscore.js htdocs/lib/jquery.js htdocs/lib/backbone.js -all: htdocs/index.html htdocs/store.js htdocs/jsonstore.css htdocs/data/items.json +all: htdocs/index.html htdocs/store.js htdocs/data/items.json @if [ ! -e "./htdocs/lib" ]; then \ echo "Please do 'make setup' before continuing"; \ exit 1; \ fi serve: all - $(COFFEE) ./bin/autoreload + ./bin/autoreload store: all @@ -39,10 +39,7 @@ docs: mkdir -p docs htdocs/index.html: src/backbonestore.nw - $(NOTANGLE) -c -Rindex.haml src/backbonestore.nw > htdocs/index.html - -htdocs/jsonstore.css: src/backbonestore.nw - $(NOTANGLE) -c -Rjsonstore.css src/backbonestore.nw > htdocs/jsonstore.css + $(NOTANGLE) -c -Rindex.html src/backbonestore.nw > htdocs/index.html htdocs/store.js: src/backbonestore.nw $(NOTANGLE) -c -Rstore.js src/backbonestore.nw > htdocs/store.js @@ -67,7 +64,7 @@ docs/backbonestore.html: docs src/backbonestore.nw html: docs/backbonestore.html clean: - - rm -f htdocs/*.* docs/*.tex docs/*.dvi docs/*.aux docs/*.toc docs/*.log docs/*.out + - rm -f htdocs/*.js htdocs/*.html docs/*.tex docs/*.dvi docs/*.aux docs/*.toc docs/*.log docs/*.out distclean: clean - rm -fr ./htdocs/lib diff --git a/bin/autoreload b/bin/autoreload index 1558b47..428acf0 100755 --- a/bin/autoreload +++ b/bin/autoreload @@ -1,3 +1,4 @@ +#!/usr/bin/env node var fs = require('fs'); var Inotify = require('inotify').Inotify; diff --git a/htdocs/index.html b/htdocs/index.html new file mode 100644 index 0000000..87754e0 --- /dev/null +++ b/htdocs/index.html @@ -0,0 +1,75 @@ + + + + The Backbone Store + + + + + + + + + + +
+ +
+
+ + + + + + + diff --git a/htdocs/jsonstore.css b/htdocs/jsonstore.css new file mode 100644 index 0000000..1cfec6b --- /dev/null +++ b/htdocs/jsonstore.css @@ -0,0 +1,91 @@ +body { + font-family: "Lucida Grande", Lucida, Helvetica, Arial, sans-serif; + background: #fff; + color: #333; + margin: 0px; + padding: 0px; +} +#main { + position: relative; +} +#header { + background: #999; + background: -webkit-gradient(linear, left top, left bottom, from(#adadad), to(#7a7a7a)); + background: -moz-linear-gradient(top, #adadad, #7a7a7a); + margin: 0px; + padding: 20px; + border-bottom: 1px solid #ccc; +} +#header h1 { + font-family: Inconsolata, Monaco, Courier, mono; + color: #fff; + margin: 0px; +} +#header .cart-info { + position: absolute; + top: 0px; + right: 0px; + text-align: right; + padding: 10px; + background: #555; + background: -webkit-gradient(linear, left top, left bottom, from(#777), to(#444)); + background: -moz-linear-gradient(top, #777, #444); + 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; +} +#productlistview ul { + list-style: none; +} +.item { + float: left; + width: 250px; + margin-right: 10px; + margin-bottom: 10px; + padding: 5px; + -moz-border-radius-topleft: 5px; + -moz-border-radius-topright: 5px; + -moz-border-radius-bottomleft: 5px; + -moz-border-radius-bottomright: 5px; + -webkit-border-bottom-right-radius: 5px; + -webkit-border-top-left-radius: 5px; + -webkit-border-top-right-radius: 5px; + -webkit-border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + border-bottom-left-radius: 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-detail .item-image { + float: left; + margin-right: 10px; +} +.item-detail .item-info { + padding: 100px 10px 0px 10px; +} diff --git a/htdocs/store.js b/htdocs/store.js new file mode 100644 index 0000000..43da3b0 --- /dev/null +++ b/htdocs/store.js @@ -0,0 +1,227 @@ +var Product = Backbone.Model.extend({}); + +var Item = Backbone.Model.extend({ + update: function(amount) { + if (amount === this.get('quantity')) { + return this; + } + this.set({quantity: amount}, {silent: true}); + this.collection.trigger('update', this); + return this; + }, + + price: function() { + return this.get('product').get('price') * this.get('quantity'); + } +}); + + +var ProductCollection = Backbone.Collection.extend({ + model: Product, + initialize: function(models, options) { + this.url = options.url; + }, + + comparator: function(item) { + return item.get('title'); + } +}); + + +var ItemCollection = Backbone.Collection.extend({ + model: Item, + + updateItemForProduct: function(product, amount) { + amount = amount != null ? amount : 0; + var pid = product.get('id'); + var item = this.detect(function(obj) { + return obj.get('product').get('id') === pid; + }); + if (item) { + item.update(amount); + return item; + } + return this.add({ + product: product, + quantity: amount + }); + }, + + getTotalCount: function() { + var addup = function(memo, obj) { + return memo + obj.get('quantity'); + }; + return this.reduce(addup, 0); + }, + + getTotalCost: function() { + var addup = function(memo, obj) { + return memo + obj.price(); + }; + return this.reduce(addup, 0); + } +}); + + +var BaseView = Backbone.View.extend({ + parent: $('#main'), + className: 'viewport', + + initialize: function(options) { + Backbone.View.prototype.initialize.apply(this, arguments); + this.$el.hide(); + this.parent.append(this.el); + }, + + hide: function() { + var dfd = $.Deferred(); + if (!this.$el.is(':visible')) { + return dfd.resolve(); + } + this.$el.fadeOut('fast', function() { + return dfd.resolve(); + }); + return dfd.promise(); + }, + + show: function() { + var dfd = $.Deferred(); + if (this.$el.is(':visible')) { + return dfd.resolve(); + } + this.$el.fadeIn('fast', function() { + return dfd.resolve(); + }); + return dfd.promise(); + } +}); + + +var ProductListView = BaseView.extend({ + id: 'productlistview', + template: _.template($("#store_index_template").html()), + + initialize: function(options) { + BaseView.prototype.initialize.apply(this, arguments); + this.collection.bind('reset', this.render.bind(this)); + }, + + render: function() { + this.$el.html(this.template({ + 'products': this.collection.toJSON() + })); + return this; + } +}); + + +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; + }, + + events: { + "keypress .uqf" : "updateOnEnter", + "click .uq" : "update" + }, + + update: function(e) { + e.preventDefault(); + return this.itemcollection.updateItemForProduct(this.model, parseInt(this.$('.uqf').val())); + }, + + updateOnEnter: function(e) { + if (e.keyCode === 13) { + this.update(e); + } + }, + + render: function() { + this.$el.html(this.template(this.model.toJSON())); + return this; + } +}); + + +var CartWidget = Backbone.View.extend({ + el: $('.cart-info'), + template: _.template($('#store_cart_template').html()), + + initialize: function() { + Backbone.View.prototype.initialize.apply(this, arguments); + this.collection.bind('update', this.render.bind(this)); + }, + + 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; + } +}); + + +var BackboneStore = Backbone.Router.extend({ + views: {}, + products: null, + cart: null, + + routes: { + "": "index", + "item/:id": "product" + }, + + 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 = ''; + }); + }, + + hideAllViews: function() { + return _.filter(_.map(this.views, function(v) { return v.hide(); }), + function(t) { return t !== null; }); + }, + + index: function() { + var view = this.views['_index']; + return $.when.apply($, this.hideAllViews()).then(function() { + return 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(); + }); + } +}); + + + $(document).ready(function() { + new BackboneStore(); + return Backbone.history.start(); + }); + diff --git a/src/backbonestore.nw b/src/backbonestore.nw index e8a82a6..c532435 100644 --- a/src/backbonestore.nw +++ b/src/backbonestore.nw @@ -106,10 +106,11 @@ var Product = Backbone.Model.extend({}); var Item = Backbone.Model.extend({ update: function(amount) { if (amount === this.get('quantity')) { - return; + return this; } this.set({quantity: amount}, {silent: true}); - return this.collection.trigger('update', this); + this.collection.trigger('update', this); + return this; }, price: function() { @@ -295,9 +296,9 @@ be rendered. As an alternative, the viewport object may already exist, in which case you just find it with a selector, and the view attaches itself to that DOM object from then on. In older versions of the Backbone Store, we -used to assign [[@el]] to a jQuery-wrapped version of the element; +used to assign [[this.el]] to a jQuery-wrapped version of the element; that's no longer necessary, as Backbone provides you with its own -version automatically in [[@$el]]. +version automatically in [[this.$el]]. The 'parent' field is something I created for my own use, since I intend to have multiple child objects share the same piece of real-estate. The @@ -319,10 +320,10 @@ Next, we will define the hide and show functions. <>= hide: function() { var dfd = $.Deferred(); - if (!this.el.is(':visible')) { + if (!this.$el.is(':visible')) { return dfd.resolve(); } - this.el.fadeOut('fast', function() { + this.$el.fadeOut('fast', function() { return dfd.resolve(); }); return dfd.promise(); @@ -330,10 +331,10 @@ Next, we will define the hide and show functions. show: function() { var dfd = $.Deferred(); - if (this.el.is(':visible')) { + if (this.$el.is(':visible')) { return dfd.resolve(); } - this.el.fadeIn('fast', function() { + this.$el.fadeIn('fast', function() { return dfd.resolve(); }); return dfd.promise(); @@ -414,9 +415,9 @@ var ProductListView = BaseView.extend({ }, render: function() { - this.el.html(this.template({ + this.$el.html(this.template({ 'products': this.collection.toJSON() - }); + })); return this; } }); @@ -506,7 +507,7 @@ to be just about all of them. <>= events: { - "keypress .uqf" : "updateOnEnter" + "keypress .uqf" : "updateOnEnter", "click .uq" : "update" }, @@ -527,11 +528,11 @@ cart model, which is where it belongs: \textit{knowledge about items and each item's relationship to its collection belongs in the collection}. -Look closely at the [[update()]] method. The reference [[@\$]] is a +Look closely at the [[update()]] method. The reference [[this.$]] is a special Backbone object that limits selectors to objects inside the element of the view. Without it, jQuery would have found the first input field of class 'uqf' in the DOM, not the one for this specific -view. [[@\$('.uqf')]] is shorthand for [[$('uqf', @el)]], and helps +view. [[this.$('.uqf')]] is shorthand for [[$('uqf', this.el)]], and helps clarify what it is you're looking for. <>= @@ -542,7 +543,7 @@ clarify what it is you're looking for. updateOnEnter: function(e) { if (e.keyCode === 13) { - return this.update(e); + this.update(e); } }, @@ -552,7 +553,7 @@ The render is straightforward: <>= render: function() { - this.el.html(this.template(this.model.toJSON())); + this.$el.html(this.template(this.model.toJSON())); return this; } }); @@ -622,7 +623,7 @@ template with the new count and cost, and then wiggle it a little to show that it did changed: <>= - CartWidget.prototype.render = function() { + render: function() { var tel = this.$el.html(this.template({ 'count': this.collection.getTotalCount(), 'cost': this.collection.getTotalCost() @@ -729,7 +730,7 @@ hidden) or null. The [[_.filter()]] call at the end means that this method returns only an array of deferreds. <>= - hideAllViews = function() { + hideAllViews: function() { return _.filter(_.map(this.views, function(v) { return v.hide(); }), function(t) { return t !== null; }); }, @@ -775,7 +776,7 @@ the [[CartWidget]] to update automagically as well. view = this.views[id] = new ProductView({ model: product, itemcollection: this.cart - }).render() + }).render(); } return $.when(this.hideAllViews()).then(function() { return view.show(); @@ -802,7 +803,7 @@ Finally, we need to start the program Here's the entirety of the program. Coffeescript provides its own namespace wrapper: -<>= +<>= <> <>