Working now.

This commit is contained in:
Elf M. Sternberg 2016-04-16 21:31:53 -07:00
parent 1c417adc6e
commit d0af7f22e6
6 changed files with 418 additions and 26 deletions

View File

@ -6,14 +6,14 @@ ECHO= echo
LIBS:= htdocs/lib/underscore.js htdocs/lib/jquery.js htdocs/lib/backbone.js 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 \ @if [ ! -e "./htdocs/lib" ]; then \
echo "Please do 'make setup' before continuing"; \ echo "Please do 'make setup' before continuing"; \
exit 1; \ exit 1; \
fi fi
serve: all serve: all
$(COFFEE) ./bin/autoreload ./bin/autoreload
store: all store: all
@ -39,10 +39,7 @@ docs:
mkdir -p docs mkdir -p docs
htdocs/index.html: src/backbonestore.nw htdocs/index.html: src/backbonestore.nw
$(NOTANGLE) -c -Rindex.haml src/backbonestore.nw > htdocs/index.html $(NOTANGLE) -c -Rindex.html src/backbonestore.nw > htdocs/index.html
htdocs/jsonstore.css: src/backbonestore.nw
$(NOTANGLE) -c -Rjsonstore.css src/backbonestore.nw > htdocs/jsonstore.css
htdocs/store.js: src/backbonestore.nw htdocs/store.js: src/backbonestore.nw
$(NOTANGLE) -c -Rstore.js src/backbonestore.nw > htdocs/store.js $(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 html: docs/backbonestore.html
clean: 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 distclean: clean
- rm -fr ./htdocs/lib - rm -fr ./htdocs/lib

View File

@ -1,3 +1,4 @@
#!/usr/bin/env node
var fs = require('fs'); var fs = require('fs');
var Inotify = require('inotify').Inotify; var Inotify = require('inotify').Inotify;

75
htdocs/index.html Normal file
View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>The Backbone Store</title>
<link charset="utf-8" href="jsonstore.css" rel="stylesheet" type="text/css">
<script id="store_index_template" type="text/x-underscore-tmplate">
<h1>Product Catalog</h1>
<ul>
<% for(i=0,l=products.length;i<l;++i) { p = products[i]; %>
<li class="item">
<div class="item-image">
<a href="#item/<%= p.id %>">
<img alt="<%= p.title %>" src="<%= p.image %>">
</a>
</div>
<div class="item-artist"><%= p.artist %></div>
<div class="item-title"><%= p.title %></div>
<div class="item-price">$<%= p.price %></div>
</li>
<% } %>
</ul>
</script>
<script id="store_item_template" type="text/x-underscore-template">
<div class="item-detail">
<div class="item-image">
<img alt="<%= title %>" src="<%= large_image %>">
</div>
<div class="item-info">
<div class="item-artist"><%= artist %></div>
<div class="item-title"><%= title %></div>
<div class="item-price">$<%= price %></div>
<div class="item-form"></div>
<form action="#/cart" method="post">
<p>
<label>Quantity:</label>
<input class="uqf" name="quantity" size="2" type="text" value="1">
</p>
<p>
<input class="uq" type="submit" value="Add to Cart">
</p>
</form>
<div class="item-link">
<a href="<%= url %>">Buy this item on Amazon</a>
</div>
<div class="back-link">
<a href="#">&laquo; Back to Items</a>
</div>
</div>
</div>
</script>
<script id="store_cart_template" type="text/x-underscore-template">
<p>Items: <%= count %> ($<%= cost %>)</p>
</script>
</head>
</head>
<body>
<div id="container">
<div id="header">
<h1>
The Backbone Store
</h1>
<div class="cart-info"></div>
</div>
<div id="main"></div>
</div>
<script src="lib/jquery.js" type="text/javascript"></script>
<script src="lib/underscore.js" type="text/javascript"></script>
<script src="lib/backbone.js" type="text/javascript"></script>
<script src="store.js" type="text/javascript"></script>
</body>
</html>

91
htdocs/jsonstore.css Normal file
View File

@ -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;
}

227
htdocs/store.js Normal file
View File

@ -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();
});

View File

@ -106,10 +106,11 @@ var Product = Backbone.Model.extend({});
var Item = Backbone.Model.extend({ var Item = Backbone.Model.extend({
update: function(amount) { update: function(amount) {
if (amount === this.get('quantity')) { if (amount === this.get('quantity')) {
return; return this;
} }
this.set({quantity: amount}, {silent: true}); this.set({quantity: amount}, {silent: true});
return this.collection.trigger('update', this); this.collection.trigger('update', this);
return this;
}, },
price: function() { price: function() {
@ -295,9 +296,9 @@ be rendered.
As an alternative, the viewport object may already exist, in which case 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 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 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 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 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 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.
<<base view>>= <<base view>>=
hide: function() { hide: function() {
var dfd = $.Deferred(); var dfd = $.Deferred();
if (!this.el.is(':visible')) { if (!this.$el.is(':visible')) {
return dfd.resolve(); return dfd.resolve();
} }
this.el.fadeOut('fast', function() { this.$el.fadeOut('fast', function() {
return dfd.resolve(); return dfd.resolve();
}); });
return dfd.promise(); return dfd.promise();
@ -330,10 +331,10 @@ Next, we will define the hide and show functions.
show: function() { show: function() {
var dfd = $.Deferred(); var dfd = $.Deferred();
if (this.el.is(':visible')) { if (this.$el.is(':visible')) {
return dfd.resolve(); return dfd.resolve();
} }
this.el.fadeIn('fast', function() { this.$el.fadeIn('fast', function() {
return dfd.resolve(); return dfd.resolve();
}); });
return dfd.promise(); return dfd.promise();
@ -414,9 +415,9 @@ var ProductListView = BaseView.extend({
}, },
render: function() { render: function() {
this.el.html(this.template({ this.$el.html(this.template({
'products': this.collection.toJSON() 'products': this.collection.toJSON()
}); }));
return this; return this;
} }
}); });
@ -506,7 +507,7 @@ to be just about all of them.
<<product detail view>>= <<product detail view>>=
events: { events: {
"keypress .uqf" : "updateOnEnter" "keypress .uqf" : "updateOnEnter",
"click .uq" : "update" "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 and each item's relationship to its collection belongs in the
collection}. 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 special Backbone object that limits selectors to objects inside the
element of the view. Without it, jQuery would have found the first 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 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. clarify what it is you're looking for.
<<product detail view>>= <<product detail view>>=
@ -542,7 +543,7 @@ clarify what it is you're looking for.
updateOnEnter: function(e) { updateOnEnter: function(e) {
if (e.keyCode === 13) { if (e.keyCode === 13) {
return this.update(e); this.update(e);
} }
}, },
@ -552,7 +553,7 @@ The render is straightforward:
<<product detail view>>= <<product detail view>>=
render: function() { render: function() {
this.el.html(this.template(this.model.toJSON())); this.$el.html(this.template(this.model.toJSON()));
return this; 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: show that it did changed:
<<cart widget>>= <<cart widget>>=
CartWidget.prototype.render = function() { render: function() {
var tel = this.$el.html(this.template({ var tel = this.$el.html(this.template({
'count': this.collection.getTotalCount(), 'count': this.collection.getTotalCount(),
'cost': this.collection.getTotalCost() '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. method returns only an array of deferreds.
<<router>>= <<router>>=
hideAllViews = function() { hideAllViews: function() {
return _.filter(_.map(this.views, function(v) { return v.hide(); }), return _.filter(_.map(this.views, function(v) { return v.hide(); }),
function(t) { return t !== null; }); function(t) { return t !== null; });
}, },
@ -775,7 +776,7 @@ the [[CartWidget]] to update automagically as well.
view = this.views[id] = new ProductView({ view = this.views[id] = new ProductView({
model: product, model: product,
itemcollection: this.cart itemcollection: this.cart
}).render() }).render();
} }
return $.when(this.hideAllViews()).then(function() { return $.when(this.hideAllViews()).then(function() {
return view.show(); 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 Here's the entirety of the program. Coffeescript provides its own
namespace wrapper: namespace wrapper:
<<store.coffee>>= <<store.js>>=
<<models>> <<models>>
<<product collection>> <<product collection>>