Rough draft of 3.0 done.

This commit is contained in:
Elf M. Sternberg 2016-04-16 21:19:08 -07:00
parent 1f6c1e0b46
commit 1c417adc6e
4 changed files with 228 additions and 231 deletions

7
.gitignore vendored
View File

@ -4,3 +4,10 @@
.#*
.DS_Store
*~
node_modules/*
bower_components/*
npm-debug.log
docs/*.html
docs/*.tex
htdocs/lib
package.yml

25
bin/autoreload Executable file
View File

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

33
package.json Normal file
View File

@ -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 <elf.sternberg@gmail.com>",
"license": "BSD-2-Clause",
"bugs": {
"url": "https://github.com/elfsternberg/The-Backbone-Store/issues"
},
"homepage": "https://github.com/elfsternberg/The-Backbone-Store#readme"
}

View File

@ -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,7 +450,7 @@ Require's ``text'' plugin.
Here is the HTML for our home page's template:
<<product list template>>=
<script id="store_index_template" type="text/x-underscore-tmplate">
<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]; %>
@ -464,7 +466,7 @@ Here is the HTML for our home page's template:
</li>
<% } %>
</ul>
</script>
</script>
@
@ -480,12 +482,14 @@ heirarchy, and keeping track of the ItemCollection object, so we can add
and change items as needed.
<<product detail view>>=
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.
<<product detail view>>=
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.
<<product detail view>>=
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:
<<product detail view>>=
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.
<<product detail template>>=
%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")/
<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>
.item-link
%a(href="<%= url %>") Buy this item on Amazon
.back-link
%a(href="#") &laquo; 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.
<<cart widget>>=
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:
<<cart widget>>=
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:
<<example>>=
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:
<<cart template>>=
%script#store_cart_template(type="text/x-underscore-template")
%p Items: <%= count %> ($<%= cost %>)
<script id="store_cart_template" type="text/x-underscore-template">
<p>Items: <%= count %> ($<%= cost %>)</p>
</script>
@
%$
@ -630,10 +673,10 @@ of the [[Views]], the [[ProductCollection]], and the
[[ItemCollection]].
<<router>>=
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:
<<router>>=
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.
<<router>>=
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.
<<router>>=
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.
<<router>>=
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.
<<router>>=
product: (id) ->
product = @products.detect (p) -> p.get('id') == (id)
view = (@views['item.' + id] ||= new ProductView(
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: @cart
).render())
$.when(@hideAllViews()).then(
() -> view.show())
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
<<initialization>>=
$(document).ready () ->
$(document).ready(function() {
new BackboneStore();
Backbone.history.start();
return Backbone.history.start();
});
@
%$
@ -765,131 +822,6 @@ namespace wrapper:
<<initialization>>
@
\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:
<<jsonstore.styl>>=
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!
<<jsonstore.styl>>=
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.