这是一个最少实现的购物网站
1、index.html
<!DOCTYPE html>
<html>
<head>
<link href='bootstrap.css' rel='stylesheet' charset="utf-8" />
<link href='application.css' rel='stylesheet' charset="utf-8" />
<script src='jquery.js'></script>
<script src='accounting.js'></script>
<script src='markdown.js'></script>
<script src='handlebars.js'></script>
<script src='ember.js'></script>
<script src='ember-data.js'></script>
<script src='app.js'></script>
</head>
<body>
<!-- 这里是顶级模版,使用了bootstrap的标记 主要展示了导航栏 页脚的内容 -->
<script type='text/x-handlebars' data-template-name='application' charset="utf-8">
<div class='navbar navbar-default'>
<div class='container'>
{{#link-to 'index' class='navbar-brand'}}<img src='images/logo.png' alt='logo' height='34' width='224' />{{/link-to}}
<ul class='nav navbar-nav navbar-right'>
{{#link-to 'index' tagName='li'}}Home主页{{/link-to}}
{{#link-to 'products' tagName='li'}}Products产品{{/link-to}}
{{#link-to 'contacts' tagName='li'}}Contacts联系{{/link-to}}
</ul>
</div>
</div>
<div class='container'>
{{outlet}}
</div>
<footer class='container'>
<hr />
<p class='pull-left'>© 2013 The Flint & Flame</p>
<p class='pull-right'>{{#link-to 'credits'}}Credits{{/link-to}}</p>
</footer>
</script>
<!--这里的index 模版的内容 -->
<script type='text/x-handlebars' data-template-name='index'>
<div class='jumbotron'>
<h1>Welcome to The Flint & Flame!</h1>
<p class='tagline'>
<img {{bind-attr src='logo'}} alt='Logo' />
Everything you need to make it through the winter.
</p>
<p>
{{#link-to 'products' class='btn btn-primary btn-lg'}}
Browse All {{productsCount}} Items »
{{/link-to}}
</p>
</div>
<div class='row'>
{{#each onSale}}
{{product-details product=this classNames='col-sm-4'}}
{{/each}}
</div>
<p class='pull-right text-muted'>Rendered on {{time}}</p>
</script>
<!-- 这里是contacts/index 模版的额你容 -->
<script type='text/x-handlebars' data-template-name='contacts/index'>
<div class='row'>
<img {{bind-attr src='avatar'}} alt='Avatar' class='img-thumbnail col-sm-4'/>
<div class='col-sm-8'>
<h1>About The Fire Spirits</h1>
<p>Contact {{contactName}} for more info!</p>
<p>Current Status: {{open}}.</p>
</div>
</div>
</script>
<script type='text/x-handlebars' data-template-name='credits'>
<h1>Thanks for the Help!</h1>
<p>This site would not be possible without the hardworking Ember Core Team!</p>
</script>
<script type='text/x-handlebars' data-template-name='products'>
<div class='row'>
<div class='col-sm-3'>
<div class='list-group'>
{{#link-to 'products.deals' classNames='list-group-item'}}Deals{{/link-to}}
{{#link-to 'products.onsale' classNames='list-group-item'}}On Sale{{/link-to}}
</div>
<div class='list-group'>
{{#each}}
{{#link-to 'product' this classNames='list-group-item'}}
{{title}}
{{/link-to}}
{{/each}}
</div>
</div>
<div class='col-sm-9'>
{{outlet}}
</div>
</div>
</script>
<script type='text/x-handlebars' data-template-name='product'>
<div class='row'>
<div class='col-sm-7'>
<h2>{{title}}</h2>
<h3 class='text-success'>${{money price}}</h3>
<p class='text-muted'>{{description}}</p>
<p class='text-muted'>This Product has a {{rating}} star rating!</p>
<p>Finely crafted by {{#link-to 'contact' crafter}}{{crafter.name}}{{/link-to}}.</p>
{{render 'reviews' reviews}}
{{#if isNotReviewed}}
<div class='new-review'>
<h3>Review {{title}}</h3>
{{#if review.text}}
<p class='text-muted'>{{review.text}}</p>
{{/if}}
{{textarea valueBinding=review.text}}
{{view Ember.Select content=ratings value=review.rating}}
<button {{action 'createReview'}} class='btn-primary'>Review</button>
</div>
{{/if}}
</div>
<div class='col-sm-5'>
<img {{bind-attr src='image'}} class='img-thumbnail img-rounded'/>
</div>
</div>
{{contact-details contact=crafter className='row'}}
</script>
<script type='text/x-handlebars' data-template-name='products/index'>
<h1>Products!</h1>
<ul class='list-unstyled'>
{{#each}}
{{product-details product=this classNames='row' tagName='li'}}
{{/each}}
</ul>
</script>
<script type='text/x-handlebars' data-template-name='contacts'>
<div class='row'>
<div class='col-sm-9'>
{{outlet}}
</div>
<div class='col-sm-3'>
<div class='list-group'>
{{#each}}
{{#link-to 'contact' this classNames='list-group-item'}}
{{name}}
{{/link-to}}
{{/each}}
</div>
</div>
</div>
</script>
<script type='text/x-handlebars' data-template-name='contact'>
<div class='row'>
<div class='col-sm-5'>
<img {{bind-attr src='avatar' alt='name'}} class='img-thumbnail img-rounded'/>
</div>
<div class='col-sm-7'>
<h2>{{name}}</h2>
<p>{{about}}</p>
{{render 'contact/products' products}}
</div>
</div>
</script>
<script type='text/x-handlebars' data-template-name='contact/products'>
<h3>Products here3</h3>
<ul>
{{#each}}
<li>{{#link-to 'product' this}}{{title}}{{/link-to}}</li>
{{/each}}
</ul>
</script>
<script type='text/x-handlebars' data-template-name='products/onsale'>
<h1>Products On Sale</h1>
<ul class='list-unstyled'>
{{#each}}
{{product-details product=this classNames='row' tagName='li'}}
{{/each}}
</ul>
</script>
<script type='text/x-handlebars' data-template-name='products/deals'>
<h1>Todays Product Deals!</h1>
<ul class='list-unstyled'>
{{#each}}
{{product-details product=this classNames='row' tagName='li'}}
{{/each}}
</ul>
</script>
<script type='text/x-handlebars' data-template-name='components/product-details'>
<img {{bind-attr src='product.image'}} class='img-thumbnail col-sm-5' />
<div class='col-sm-7'>
<h2>{{product.title}}</h2>
<p class='product-description'>{{product.description}}</p>
<p>{{#link-to 'product' product class='btn btn-success'}}
Buy for ${{money product.price}}{{/link-to}}
</p>
{{#if hasReviews}}
<p class='text-muted'>Read all reviews ({{reviewsCount}}).</p>
{{/if}}
</div>
</script>
<script type='text/x-handlebars' data-template-name='components/contact-details'>
<div class='col-sm-9'>
<h2>About {{contact.name}}</h2>
<p>{{contact.about}}</p>
{{#if isProductive}}
<p class='text-muted'>Responsible for {{productsCount}} amazing products!</p>
{{/if}}
</div>
<div class='col-sm-3'>
<img {{bind-attr src='contact.avatar' alt='contact.name'}} class='img-thumbnail img-rounded'/>
</div>
</script>
<script type='text/x-handlebars' data-template-name='reviews'>
<h3>Reviews</h3>
<ul>
{{#each}}
{{#view 'App.ReviewView' tagName='li' length=text.length}}
<div class='content'>{{markdown text}}</div>
<span class='expand text-success'>
Read {{#if view.isExpanded}}Less{{else}}More{{/if}}
</span>
{{/view}}
{{else}}
<li><p class='text-muted'><em>No reviews yet. Be the first to write one!</em></p></li>
{{/each}}
</ul>
</script>
</body>
</html>
app.js
/* 定义创建命名空间 */
var App = Ember.Application.create({
LOG_TRANSITIONS: true
});
/* 定义路由 */
App.Router.map(function() {
this.route('credits', { path: '/thanks' });
this.resource('products', function() {
this.resource('product', { path: '/:product_id' });
this.route('onsale');
this.route('deals');
});
this.resource('contacts', function() {
this.resource('contact', { path: '/:contact_id' });
});
});
Ember.Handlebars.registerBoundHelper('markdown', function(text) {
return new Handlebars.SafeString(markdown.toHTML(text));
});
Ember.Handlebars.registerBoundHelper('money', function(value) {
return accounting.formatMoney(value/100);
});
/* index 控制器 */
App.IndexController = Ember.ArrayController.extend({
productsCount: Ember.computed.alias('length'),
logo: 'images/logo-small.png',
time: function() {
return (new Date()).toDateString();
}.property(),
onSale: function() {
return this.filterBy('isOnSale').slice(0,3);
}.property('@each.isOnSale')
});
/* contactsindex 控制器 */
App.ContactsIndexController = Ember.Controller.extend({
contactName: 'Anostagia',
avatar: 'images/avatar.png',
open: function() {
return ((new Date()).getDay() === 0) ? "Closed" : "Open";
}.property()
});
/* Products 控制器 */
App.ProductsController = Ember.ArrayController.extend({
sortProperties: ['title']
});
/* Products 控制器 */
App.ContactsController = Ember.ArrayController.extend({
sortProperties: ['name'],
contactsCount: Ember.computed.alias('length')
});
/* Products 控制器 */
App.ReviewsController = Ember.ArrayController.extend({
sortProperties: ['reviewedAt'],
sortAscending: false
});
/* Products 控制器 */
App.ContactProductsController = Ember.ArrayController.extend({
sortProperties: ['title']
});
/* Products 控制器 */
App.ProductController = Ember.ObjectController.extend({
ratings: [1,2,3,4,5],
isNotReviewed: Ember.computed.alias('review.isNew'),
review: function(){
return this.store.createRecord('review',{
product: this.get('model')
});
}.property('model'),
actions: {
createReview: function(){
var controller = this;
this.get('review').set('reviewedAt', new Date());
this.get('review').save().then(function(review){
controller.get('model.reviews')
.addObject(review);
});
}
}
});
/* 模型 */
App.ProductsRoute = Ember.Route.extend({
model: function() {
return this.store.findAll('product');
}
});
/* 模型 */
App.ContactsRoute = Ember.Route.extend({
model: function() {
return this.store.findAll('contact');
}
});
/* 模型 */
App.IndexRoute = Ember.Route.extend({
model: function(){
return this.store.findAll('product');
}
});
/* 模型 */
App.ProductsIndexRoute = Ember.Route.extend({
model: function(){
return this.store.findAll('product');
}
});
/* 模型 */
App.ProductsOnsaleRoute = Ember.Route.extend({
model: function(){
return this.modelFor('products').filterBy('isOnSale');
}
});
/* 模型 */
App.ProductsDealsRoute = Ember.Route.extend({
model: function(){
return this.modelFor('products').filter(function(product){
return product.get('price') < 500;
});
}
});
/* 视图 */
App.ReviewView = Ember.View.extend({
isExpanded: false,
classNameBindings: ['isExpanded', 'readMore'],
click: function(){
this.toggleProperty('isExpanded');
},
readMore: function(){
return this.get('length') > 140;
}.property('length')
});
/* 视图 */
App.ProductDetailsComponent = Ember.Component.extend({
reviewsCount: Ember.computed.alias('product.reviews.length'),
hasReviews: function(){
return this.get('reviewsCount') > 0;
}.property('reviewsCount')
});
App.ContactDetailsComponent = Ember.Component.extend({
productsCount: Ember.computed.alias('contact.products.length'),
isProductive: function() {
return this.get('productsCount') > 3;
}.property('productsCount')
});
/* 视图 */
App.ProductView = Ember.View.extend({
isOnSale: Ember.computed.alias('controller.isOnSale'),
classNameBindings: ['isOnSale']
});
/* 适配器 */
App.ApplicationAdapter = DS.FixtureAdapter.extend();
App.Product = DS.Model.extend({
title: DS.attr('string'),
price: DS.attr('number'),
description: DS.attr('string'),
isOnSale: DS.attr('boolean'),
image: DS.attr('string'),
reviews: DS.hasMany('review', { async: true }),
crafter: DS.belongsTo('contact', { async: true }),
rating: function() {
if(this.get('reviews.length') === 0) { return 0; }
return this.get('reviews').reduce(function(previousValue, review) {
return previousValue + review.get('rating');
}, 0) / this.get('reviews.length');
}.property('reviews.@each.rating')
});
/* 数据 */
App.Product.FIXTURES = [
{ id: 1,
title: 'Flint',
price: 99,
description: 'Flint is a hard, sedimentary cryptocrystalline form of the mineral quartz, categorized as a variety of chert.',
isOnSale: true,
image: 'images/products/flint.png',
reviews: [100,101],
crafter: 200
},
{
id: 2,
title: 'Kindling',
price: 249,
description: 'Easily combustible small sticks or twigs used for starting a fire.',
isOnSale: false,
image: 'images/products/kindling.png',
reviews: [],
crafter: 201
},
{
id: 3,
title: 'Matches',
price: 499,
description: 'One end is coated with a material that can be ignited by frictional heat generated by striking the match against a suitable surface.',
isOnSale: true,
reviews: [],
image: 'images/products/matches.png',
crafter: 201
},
{
id: 4,
title: 'Bow Drill',
price: 999,
description: 'The bow drill is an ancient tool. While it was usually used to make fire, it was also used for primitive woodworking and dentistry.',
isOnSale: false,
reviews: [],
image: 'images/products/bow-drill.png',
crafter: 200
},
{
id: 5,
title: 'Tinder',
price: 499,
description: 'Tinder is easily combustible material used to ignite fires by rudimentary methods.',
isOnSale: true,
reviews: [],
image: 'images/products/tinder.png',
crafter: 201
},
{
id: 6,
title: 'Birch Bark Shaving',
price: 999,
description: 'Fresh and easily combustable',
isOnSale: true,
reviews: [],
image: 'images/products/birch.png',
crafter: 201
}
];
/* model data */
App.Contact = DS.Model.extend({
name: DS.attr('string'),
about: DS.attr('string'),
avatar: DS.attr('string'),
products: DS.hasMany('product', { async: true })
});
/* 数据 */
App.Contact.FIXTURES = [
{
id: 200,
name: 'Giamia',
about: 'Although Giamia came from a humble spark of lightning, he quickly grew to be a great craftsman, providing all the warming instruments needed by those close to him.',
avatar: 'images/contacts/giamia.png',
products: [1,4]
},
{
id: 201,
name: 'Anostagia',
about: 'Knowing there was a need for it, Anostagia drew on her experience and spearheaded the Flint & Flame storefront. In addition to coding the site, she also creates a few products available in the store.',
avatar: 'images/contacts/anostagia.png',
products: [2,3,5,6]
}
];
App.Review = DS.Model.extend({
text: DS.attr('string'),
reviewedAt: DS.attr('date'),
product: DS.belongsTo('product'),
rating: DS.attr('number')
});
App.Review.FIXTURES = [
{
id: 100,
reviewedAt: new Date('12/10/2013').getTime(),
text: "Started a fire in no time!",
rating: 4
},
{
id: 101,
reviewedAt: new Date('12/12/2013').getTime(),
text: "Not the brightest flame, but warm!",
rating: 5
},
{
id: 102,
reviewedAt: new Date('12/30/2013').getTime(),
text: "This is some amazing Flint! It lasts **forever** and works even when damp! I still remember the first day when I was only a little fire sprite and got one of these in my flame stalking for treemas. My eyes lit up the moment I tried it! Here's just a few uses for it:\n\n* Create a fire using just a knife and kindling!\n* Works even after jumping in a lake (although, that's suicide for me)\n* Small enough to fit in a pocket -- if you happen to wear pants\n\n\nYears later I'm still using the _same one_. That's the biggest advantage of this -- it doesn't run out easily like matches. As long as you have something to strike it against, **you can start a fire anywhere** you have something to burn!",
rating: 5
}
];