この記事は、knockout.js Advent Calendar 2015の7日目の記事です。 先に6日目に目を通すことを推奨しています。
knockout , knockout-es5 , knockout.punches環境を想定しています。
knockoutでも、あるpropertyに依存した別のpropertyを定義することができます。
例えば、商品の小計を考えてみましょう。
ここでの小計は単価 × 数量
とします。
つまり、小計
は単価
と数量
に依存しています。
これをバニラのjavascriptで書くと、
function Order(){
this.unitPrice=150;
this.quantity=3;
Object.defineProperty(this,'price',{
get:function(){
return this.unitPrice * this.quantity;
}
});
}
このようになります。
これについての説明は、MDNなどを参照するといいです。
では、knockoutではどう書けばいいんでしょうか?
knockoutもまったく同じでもOKです。
もちろん、このpropertyにさらに依存したpropertyも定義できます。
なお、knockout es5では、knockout用にカスタムしたko.definePropertyという機構も提供しています。
Object.defineProperty
ではなく、あえてko.defineProperty
を採用する利点は、
Object.defineProperty
の場合は、propertyへアクセスする度に値が再計算されるのに対して、
ko.defineProperty
は、依存先の値が変化するまでは、計算された値をキャッシュとして保持することで、余計な再計算を防ぐことができます。
複雑な計算や、膨大な計算量がある場合には、ko.definePropertyを使うべきでしょう。
では、これまでの復習とより実践的な例として、次のようなものを考えてみます。
とあるFruitShopの商品注文を想定します。このShopでは、下記の要件をクリアする必要があります。
- Shopでは複数のItem(商品)を扱っている。
- Itemは、name(商品名)とprice(単価)の情報を持っている。
- Shopでは、1つのOrder(注文)を受け付けている。
- Orderは、複数のOrderDetail(注文内訳)を持っていて、そのtotalPrice(合計価格)の情報を持っている。
- OrderDetailは Itemとquantity(個数)とprice(小計)の情報を持っている。
これらを実現するサンプルがこちらです。
<h1>Fruit Shopping Order</h1>
<div>商品リスト:
<table>
<thead>
<tr>
<th>商品名</th>
<th>単価</th>
<th>注文</th>
</tr>
</thead>
<tbody data-bind="foreach:items">
<tr>
<td>{{name}}</td>
<td>{{price}}</td>
<td>
<button type="button" data-bind="click:$parent.order.add">注文</button>
</td>
</tr>
</tbody>
</table>
</div>
<br>
<div>注文票:
<table data-bind="with:order">
<thead>
<tr>
<th>商品名</th>
<th>単価</th>
<th>数量</th>
<th>小計</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach:details">
<tr>
<td>{{item.name}}</td>
<td>{{item.price}}</td>
<td><input type="number" data-bind="value:quantity" min="1" style="width:3em;" /></td>
<td>{{price}}</td>
<td><button type="button" data-bind="on.click:$parent.details.splice($index(),1)">キャンセル</button></td>
</tr>
</tbody>
<tbody data-bind="if:!details.length">
<tr>
<td colspan="5" style="text-align:center">Empty</td>
</tr>
</tbody>
<tfooter>
<tr>
<td colspan="3">合計</td>
<td>{{totalPrice}}</td>
</tr>
</tfooter>
</table>
</div>
function Item(name,price){
this.name = name;
this.price = price;
ko.track(this);
}
function Shop(){
this.items = [
new Item("apple",60),
new Item("banana",25),
new Item("cinnamon",80),
new Item("dragonfruit",120)
];
this.order = new Order();
ko.track(this);
}
function OrderDetail(item,quantity){
this.item=item;
this.quantity=quantity;
ko.track(this);
Object.defineProperty(this,'price',{get:function(){
return this.item.price * this.quantity
}});
}
function Order(){
this.details = [];
ko.track(this);
Object.defineProperty(this,'totalPrice',{get:function(){
var total = 0;
this.details.forEach(function(detail){
total += detail.price;
});
return total;
}});
}
Order.prototype.add=function(item,event){
var quantity = 1;
this.details.push(new OrderDetail(item,quantity));
}
var vm = new Shop();
ko.punches.enableAll();
ko.applyBindings(vm);
蛇足ですが、そろそろ型チェックが恋しくなってきたのではないでしょうか? ぜひTypeScriptで書きましょう。
class Item{
constructor(
public name:string,
public price:number
){
ko.track(this);
}
}
class Shop{
public items:Item[]=[];
public order=new Order();
constructor(){
this.items = [
new Item("apple",60),
new Item("banana",25),
new Item("cinnamon",80),
new Item("dragonfruit",120)
];
ko.track(this);
}
}
class OrderDetail{
public price:number;
constructor(
public item:Item,
public quantity:number
){
ko.track(this);
}
public get price():number{
return this.item.price * this.quantity
}
}
class Order{
public totalPrice:number;
public details:OrderDetail[] = [];
constructor(){
ko.track(this);
}
public get totalPrice():number{
var total = 0;
this.details.forEach((detail)=>{
total += detail.price;
});
return total;
}
public add(item:Item,event){
var quantity = 1;
this.details.push(new OrderDetail(item,quantity));
}
}
var vm = new Shop();
ko.punches.enableAll();
ko.applyBindings(vm);