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 .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'); return this.get('product').get('price') * this.get('quantity');
} }
}); });
@ @
The methods [[.get(item)]] and [[.set(item, value)]] are at the heart of 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({ var ProductCollection = Backbone.Collection.extend({
model: Product, model: Product,
initialize: function(models, options) { initialize: function(models, options) {
return this.url = options.url; this.url = options.url;
}, },
comparator: function(item) { comparator: function(item) {
return item.get('title'); return item.get('title');
} }
@ -448,23 +450,23 @@ Require's ``text'' plugin.
Here is the HTML for our home page's template: Here is the HTML for our home page's template:
<<product list 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> <h1>Product Catalog</h1>
<ul> <ul>
<% for(i=0,l=products.length;i<l;++i) { p = products[i]; %> <% for(i=0,l=products.length;i<l;++i) { p = products[i]; %>
<li class="item"> <li class="item">
<div class="item-image"> <div class="item-image">
<a href="#item/<%= p.id %>"> <a href="#item/<%= p.id %>">
<img alt="<%= p.title %>" src="<%= p.image %>"> <img alt="<%= p.title %>" src="<%= p.image %>">
</a> </a>
</div> </div>
<div class="item-artist"><%= p.artist %></div> <div class="item-artist"><%= p.artist %></div>
<div class="item-title"><%= p.title %></div> <div class="item-title"><%= p.title %></div>
<div class="item-price">$<%= p.price %></div> <div class="item-price">$<%= p.price %></div>
</li> </li>
<% } %> <% } %>
</ul> </ul>
</script> </script>
@ @
@ -480,12 +482,14 @@ heirarchy, and keeping track of the ItemCollection object, so we can add
and change items as needed. and change items as needed.
<<product detail view>>= <<product detail view>>=
class ProductView extends _BaseView var ProductView = BaseView.extend({
className: 'productitemview' className: 'productitemview',
template: $("#store_item_template").html() template: _.template($("#store_item_template").html()),
initialize: (options) ->
_BaseView.prototype.initialize.apply @, [options] initialize: function(options) {
@itemcollection = options.itemcollection 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. 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"
},
@ @
@ -530,21 +535,27 @@ view. [[@\$('.uqf')]] is shorthand for [[$('uqf', @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>>=
update: (e) -> update: function(e) {
e.preventDefault() e.preventDefault();
@itemcollection.updateItemForProduct @model, parseInt(@$('.uqf').val()) return this.itemcollection.updateItemForProduct(this.model, parseInt(this.$('.uqf').val()));
},
updateOnEnter: (e) -> updateOnEnter: function(e) {
@update(e) if e.keyCode == 13 if (e.keyCode === 13) {
return this.update(e);
}
},
@ @
The render is straightforward: The render is straightforward:
<<product detail view>>= <<product detail view>>=
render: () -> render: function() {
@el.html(_.template(@template)(@model.toJSON())); 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. [[underscore]] magic because there are no loops.
<<product detail template>>= <<product detail template>>=
%script#store_item_template(type= "text/x-underscore-template") <script id="store_item_template" type="text/x-underscore-template">
.item-detail <div class="item-detail">
.item-image <div class="item-image">
%img(src="<%= large_image %>" alt="<%= title %>")/ <img alt="<%= title %>" src="<%= large_image %>">
.item-info </div>
.item-artist <%= artist %> <div class="item-info">
.item-title <%= title %> <div class="item-artist"><%= artist %></div>
.item-price $<%= price %> <div class="item-title"><%= title %></div>
.item-form <div class="item-price">$<%= price %></div>
%form(action="#/cart" method="post") <div class="item-form"></div>
%p <form action="#/cart" method="post">
%label Quantity: <p>
%input(type="text" size="2" name="quantity" value="1" class="uqf")/ <label>Quantity:</label>
%p <input class="uqf" name="quantity" size="2" type="text" value="1">
%input(type="submit" value="Add to Cart" class="uq")/ </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 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. automatically update.
<<cart widget>>= <<cart widget>>=
class CartWidget extends Backbone.View var CartWidget = Backbone.View.extend({
el: $('.cart-info') el: $('.cart-info'),
template: $('#store_cart_template').html() template: _.template($('#store_cart_template').html()),
initialize: () -> initialize: function() {
@collection.bind 'update', @render.bind @ 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: show that it did changed:
<<cart widget>>= <<cart widget>>=
render: () -> CartWidget.prototype.render = function() {
tel = @$el.html _.template(@template)({ var tel = this.$el.html(this.template({
'count': @collection.getTotalCount() 'count': this.collection.getTotalCount(),
'cost': @collection.getTotalCost() 'cost': this.collection.getTotalCost()
}) }));
tel.animate({paddingTop: '30px'}).animate({paddingTop: '10px'}) 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>>= <<cart template>>=
%script#store_cart_template(type="text/x-underscore-template") <script id="store_cart_template" type="text/x-underscore-template">
%p Items: <%= count %> ($<%= cost %>) <p>Items: <%= count %> ($<%= cost %>)</p>
</script>
@ @
%$ %$
@ -630,10 +673,10 @@ of the [[Views]], the [[ProductCollection]], and the
[[ItemCollection]]. [[ItemCollection]].
<<router>>= <<router>>=
class BackboneStore extends Backbone.Router var BackboneStore = Backbone.Router.extend({
views: {} views: {},
products: null products: null,
cart: 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: They are routed like this:
<<router>>= <<router>>=
routes: routes: {
"": "index" "": "index",
"item/:id": "product" "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. returns the result of an [[ajax()]] call, which is a deferred.
<<router>>= <<router>>=
initialize: (data) -> initialize: function(data) {
@cart = new ItemCollection() Backbone.Router.prototype.initialize.apply(this, arguments);
new CartWidget this.cart = new ItemCollection();
collection: @cart new CartWidget({ collection: this.cart });
this.products = new ProductCollection([], { url: 'data/items.json' });
@products = new ProductCollection [], this.views = {
url: 'data/items.json' '_index': new ProductListView({ collection: this.products })
@views = };
'_index': new ProductListView $.when(this.products.fetch({ reset: true })).then(function() {
collection: @products return window.location.hash = '';
$.when(@products.fetch({reset: true})) });
.then(() -> 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, [[hide()]] and [[show()]] methods. We want to hide all the views,
then show the one invoked. First, let's hide every view we know then show the one invoked. First, let's hide every view we know
about. [[hide()]] returns either a deferred (if the object is being 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. method returns only an array of deferreds.
<<router>>= <<router>>=
hideAllViews: () -> hideAllViews = function() {
_.select(_.map(@views, (v) -> return v.hide()), return _.filter(_.map(this.views, function(v) { return v.hide(); }),
(t) -> t != null) function(t) { return t !== null; });
},
@ @
@ -699,9 +742,12 @@ wait for; to make it take an array of arguments, you use the
[[.apply()]] method. [[.apply()]] method.
<<router>>= <<router>>=
index: () -> index: function() {
view = @views['_index'] var view = this.views['_index'];
$.when.apply($, @hideAllViews()).then(() -> view.show()) 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 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 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 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 Not that we pass it the [[ItemCollection]] instance. It uses this to
create a new item, which (if you recall from our discussion of 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. the [[CartWidget]] to update automagically as well.
<<router>>= <<router>>=
product: (id) -> product: function(id) {
product = @products.detect (p) -> p.get('id') == (id) var view = this.views[id];
view = (@views['item.' + id] ||= new ProductView( if (!view) {
model: product, var product = this.products.detect(function(p) {
itemcollection: @cart return p.get('id') === id;
).render()) });
$.when(@hideAllViews()).then( view = this.views[id] = new ProductView({
() -> view.show()) model: product,
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 Finally, we need to start the program
<<initialization>>= <<initialization>>=
$(document).ready () -> $(document).ready(function() {
new BackboneStore(); new BackboneStore();
Backbone.history.start(); return Backbone.history.start();
});
@ @
%$ %$
@ -765,131 +822,6 @@ namespace wrapper:
<<initialization>> <<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 And that's it. Put it all together, and you've got yourself a working
Backbone Store. Backbone Store.