Rough draft of 3.0 done.
This commit is contained in:
parent
1f6c1e0b46
commit
1c417adc6e
|
@ -4,3 +4,10 @@
|
||||||
.#*
|
.#*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*~
|
*~
|
||||||
|
node_modules/*
|
||||||
|
bower_components/*
|
||||||
|
npm-debug.log
|
||||||
|
docs/*.html
|
||||||
|
docs/*.tex
|
||||||
|
htdocs/lib
|
||||||
|
package.yml
|
||||||
|
|
|
@ -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
|
||||||
|
});
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -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,7 +450,7 @@ 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]; %>
|
||||||
|
@ -464,7 +466,7 @@ Here is the HTML for our home page's template:
|
||||||
</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="#">« Back to Items</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
.item-link
|
|
||||||
%a(href="<%= url %>") Buy this item on Amazon
|
|
||||||
.back-link
|
|
||||||
%a(href="#") « 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) {
|
||||||
|
var product = this.products.detect(function(p) {
|
||||||
|
return p.get('id') === id;
|
||||||
|
});
|
||||||
|
view = this.views[id] = new ProductView({
|
||||||
model: product,
|
model: product,
|
||||||
itemcollection: @cart
|
itemcollection: this.cart
|
||||||
).render())
|
}).render()
|
||||||
$.when(@hideAllViews()).then(
|
}
|
||||||
() -> view.show())
|
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.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue