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
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

View File

@ -1,3 +1,4 @@
#!/usr/bin/env node
var fs = require('fs');
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({
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.
<<base view>>=
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.
<<product detail view>>=
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.
<<product detail view>>=
@ -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:
<<product detail view>>=
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:
<<cart widget>>=
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.
<<router>>=
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:
<<store.coffee>>=
<<store.js>>=
<<models>>
<<product collection>>