前回はショッピングカートを作成を通じて、
今回取り扱うEmber.
前回からの差分は次のとおりです。
- カートの中身が保存される
- 注文画面で商品の個数を変更できる
- 注文を確定できる
前準備
さて、
このバージョンからテンプレート記述言語がHandlebarsからHTMLBarsに変更になりました。HTMLBarsはHnadlebarsを元にEmber.
詳しい変更点とEmber.
本稿でもこの手順にしたがってEmber.
ということで、
Ember.js のバージョンアップ
特に重要な点は次の2つです。
handlebars.
の代わりにjs ember-template-compiler.
を読み込むjs 今まで
handlebars.
をscriptタグで読み込んで利用していましたが、js Ember. js 1. 10. 0からはHTMLBarsを同梱した ember-template-compiler.
を利用しますjs [1]。 ember.
の代わりにjs ember.
を読み込むdebug. js 開発用のデバッグ出力機能が充実した
ember.
と、debug. js 本番用にデバッグ処理をなくしてパフォーマンスを向上させた ember.
があります。Ember.prod. js js 1. 9.1まで開発用のファイル名は ember.
だったのですが、js 開発用・ 本番用の区別をしやすくするために名前が変更されました。
さて、
- まずは、
必要なファイルをダウンロードして libs
ディレクトリに保存します。
index.
のscriptタグを以下のように記述します。html <script src="libs/
jquery-2. ></script> <script src="libs/1.3. js" ember-template-compiler. ></script> <script src="libs/js" ember. ></script>debug. js"
以上で準備完了です。では、
カートの保存
ここまでのショッピングカートだとブラウザをリロードするとカートの中身が失われてしまっていました。これでは安心して買い物ができません。そこでカートの中身をlocalStorageに保存することで、
方針は次のとおりです。
- 商品をカートに入れたタイミングで商品IDをlocalStorageに保存する
- 画面を表示したタイミングでlocalStorageから商品のIDを読み出しカートの中身を復元する
商品IDを保存する
まずはCartController
にカートの中身を保存するメソッドを追加します。
App.CartController = Ember.ArrayController.extend({
// ...
save: function() {
var ids = JSON.stringify(this.mapBy('id'));
localStorage.setItem('cart-product-ids', ids);
}
}
localStorageに保存できる値は文字列のみなので、JSON.
しているのがポイントです。
続いて、ProductsRoute
のaddCart
アクションでsave()
メソッドを呼びます。
App.ProductsRoute = Ember.Route.extend({
// ...
actions: {
addCart: function(product) {
this.controllerFor('cart').pushObject(product);
this.controllerFor('cart').save();
this.transitionTo('cart');
}
}
}
これでカートに追加した商品のIDをlocalStorageに保存できるようになりました。まだカートの内容を復元する処理を入れていないので画面からの確認はできませんが、
いくつかの商品をカートに入れた状態で次のコードをコンソールに打ち込んでみてください。
localStorage.getItem('cart-product-ids');
選択した商品のIDが表示されたでしょうか?
さて、ProductsRoute
の中にだけ存在しているので、CartController
から商品一覧を取得できません。
そこで、Ember.
というEmber.
Ember.Object
「商品」
App.Product = Ember.Object.extend({
id: null,
name: null,
price: 0,
url: null
});
今まで特に解説していませんでしたが、extend()
メソッドを使うことで既存のクラスを継承して新しいクラスを定義できます。その際、
App.Product.reopenClass({
data: [],
all: function() {
return App.Product.data;
},
find: function(id) {
return this.all().findBy('id', Number(id));
}
});
reopenClass()
メソッドを使うと、
上記の例では、App.
というプロパティとApp.
・App.
というメソッドが定義されます。
App.Product.data.pushObjects([
App.Product.create({
id: 1,
name: 'ステッカー',
price: 6.0,
url: 'http://devswag.com/products/ember-sticker'
}),
App.Product.create({
id: 2,
name: 'Tシャツ',
price: 22.0,
url: 'http://devswag.com/products/ember-js-tshirt'
}),
App.Product.create({
id: 3,
name: 'ぬいぐるみ',
price: 10.0,
url: 'http://devswag.com/products/ember-mascot-tomster'
})
]);
商品データを用意します。ここで登場したpushObjects()
メソッドは、
また、create()
メソッドを使うことでインスタンスを生成できます。
これで商品データの準備と商品を問い合わせるメソッドが完成しました。App.
を使うと商品全件を、App.
を使うとIDを元に1件の商品を取得できます。
ProductsRoute
のmodel()
メソッドでもApp.
メソッドを使うようにしましょう。
App.ProductsRoute = Ember.Route.extend({
model: function() {
return App.Product.all();
},
// ...
});
カートを復元する
次はカートを復元する処理を組み込みます。CartController
にカートの中身を復元するメソッドを追加します。
App.CartController = Ember.ArrayController.extend({
// ...
restore: function() {
var idsString = localStorage.getItem('cart-product-ids');
var ids;
if (idsString) {
ids = JSON.parse(idsString);
} else {
ids = [];
}
var products = ids.map(function(id) {
return App.Product.find(id);
});
products = products.compact();
this.set('model', products);
}
});
商品IDはJSON.
を使って文字列で保存してあるため、JSON.
を使って配列に変換しています。
あとは、restore()
メソッドを画面表示時に実行できればカートの中身を復元できそうですね。Ember.initializer
を利用します。
App.initializer({
name: 'restore-cart',
initialize: function(container, app) {
container.lookup('controller:cart').restore();
}
});
このinitializerについて解説します。
name
initializerの名前です。複数のinitializer同士の間で実行順序を制御したいときに利用できます。
initialize
アプリケーション初期化時に実行される関数です。実行したい処理を記述します。
container
( initialize
の第一引数)Ember.
jsのDIコンテナです。 lookup
メソッドに「タイプ:名前」 の形式で問い合わせると指定のオブジェクト取得できます。ここでは CartController
のインスタンスを取得しています。app
( initialize
の第二引数)Ember.
jsアプリケーションのインスタンスです。 App
と同じオブジェクトが取得できます。
以上、
ここまでで、
まとめて表示する
カートの中身を保存できるようになりましたが、Ember.
を使って、
App.OrderLine = Ember.Object.extend({
product: null,
count: 0,
price: function() {
return this.get('product.price') * this.get('count');
}.property('product.price', 'count')
});
クラスを定義する際には、
続いてCartController
では、Product
を直接参照する代わりにこのOrderLine
を利用して選択した商品を管理することにします。CartController
のmodel
を変更することになるため、mapBy()
やpushObject()
など商品の配列に対して操作していた部分がそのままでは使えなくなります。CartController
全体を書き直すことにしましょう。
App.CartController = Ember.ArrayController.extend({
totalPrice: function() {
return this.mapBy('price').reduce(function(total, price) {
return total + price;
}, 0);
}.property('@each.price'),
addProduct: function(product) {
var orderLine = this.get('model').findBy('product', product);
if (!orderLine) {
orderLine = App.OrderLine.create({
product: product
});
this.pushObject(orderLine);
}
orderLine.set('count', orderLine.get('count') + 1);
save: function() {
var ids = [];
this.forEach(function(orderLine) {
var i;
var count = orderLine.get('count');
var productId = orderLine.get('product.id');
for (i = 0; i < count; i++) {
ids.push(productId);
}
});
localStorage.setItem('cart-product-ids', JSON.stringify(ids));
},
restore: function() {
var idsString = localStorage.getItem('cart-product-ids');
var ids;
if (idsString) {
ids = JSON.parse(idsString);
} else {
ids = [];
}
var products = ids.map(function(id) {
return App.Product.find(id);
});
products = products.compact();
products.forEach(function(product) {
this.addProduct(product);
}, this);
}
});
同様に、ProductsRoute
でCartController
のpushObject()
メソッドを呼んでいた部分もaddProduct()
メソッドを使うように変更しましょう。
App.ProductsRoute = Ember.Route.extend({
// ...
actions: {
addCart: function(product) {
this.controllerFor('cart').addProduct(product);
this.controllerFor('cart').save();
this.transitionTo('cart');
}
}
});
cart
テンプレートも同様に変更します。
<h1>注文確認</h1>
<table>
<tr>
<th>商品名</th>
<th>価格(USD)</th>
<th>個数</th>
<th>小計</th>
</tr>
{{#each orderLine in model}}
<tr>
<td>{{orderLine.product.name}}</td>
<td>{{orderLine.product.price}}</td>
<td>{{orderLine.count}}</td>
<td>${{orderLine.price}}</td>
</tr>
{{/each}}
</table>
<hr>
合計金額: ${{totalPrice}} USD
見やすさのために<li>
の代わりに<table>
を使うようにしました。次のCSSを追記してもう少しだけ見栄えを整えておきましょう。
th, td {
padding: 4px 16px;
}
td {
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
text-align: right;
}
これでカートが明細行で表示されるようになりました。
商品の個数変更
さて、
cart
テンプレートにボタンを追加します。
{{#each orderLine in model}}
~{{/each}}
中の<tr>
タグの中にボタンを追加してください。
<td>
<button {{action "increment" orderLine}}>+1</button>
<button {{action "decrement" orderLine}}>-1</button>
</td>
これに対応するactions
をCartController
に定義します。
App.CartController = Ember.ArrayController.extend({
// ...
actions: {
increment: function(orderLine) {
orderLine.incrementProperty('count');
},
decrement: function(orderLine) {
orderLine.decrementProperty('count');
}
}
});
incrementProperty()
・decrementProperty()
メソッドはEmber.
が提供するメソッドです。指定したプロパティの値を増減できます。
さて、
必要な箇所すべてでCartController
のsave()
を呼び出せば再びカートの中身が保存されるようになりますが、save()
メソッドを呼び出すのは煩雑なうえに忘れがちになってしまいます。
Observer
Ember.Observer
という機能があり、Observer
を使って、
CartController
のsave()
メソッドに次の記述を追加します。
save: function() {
// ...
}.observes('@each.count')
このobserves()
メソッドがポイントです。これまでsave()
メソッドは必要になったタイミングで適宜実行していましたが、observes()
メソッドを使うとsave()
メソッドがオブザーバーとしてオブジェクトに設定されます。
observes()
メソッドの引数には、
ここでひとつ注意したいことがあります。カートの内容を復元する際にmodel
に明細を追加しているため、 このままではカート復元中にsave()
メソッドが実行されてしまいます。
これを防ぐため、
App.CartController = Ember.ArrayController.extend({
isReady: false,
// ...
save: function() {
if (!this.get('isReady')) {
return;
}
// ...
}.observes('@each.count')
restore: function() {
// ...
this.set('isReady', true);
}
// ...
});
CartController
にisReady
というプロパティを追加し、
購入
さて、
とは言っても、
まずはcart
テンプレートに購入ボタンを追加します。
<button {{action "submit"}}>購入する</button>
このボタンに対応するactions
をCartController
に追記します。
App.CartController = Ember.ArrayController.extend({
// ...
actions: {
// ...
submit: function() {
var params = [];
this.get('model').forEach(function(orderLine) {
params.push({
product_id: orderLine.get('product.id'),
count: orderLine.get('count')
});
});
alert('購入が完了しました。(リクエストパラメータ: ' + JSON.stringify(params) + ')');
this.get('model').clear();
this.transitionToRoute('products');
}
}
});
alert()
を呼ぶ際に、
[
{"product_id": 1, "count": 1},
...
]
アプリケーションサーバを用意したとすると、
ここまでで、
今回のアプリケーションは最終的にこのようになりました。
まとめ
前回・
またEmber.