この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
あけましておめでとうございます、渡辺です。
2013年はクライアントサイドJavaScriptではAngular.jsとBackbone.jsがよく取り上げられていました。悲しいことにEmber.jsが無視されている状況は否めません。しかし、Ember.jsはやればやるほど魅力的なプロダクトです。もっと利用されるよう、今後も地道にEmber.jsの魅力を伝えていきたいかと思います。
さて、今回はMVCのC、すなわちControllerについて解説します。Ember.jsではModelが単数であるか複数であるかによって2種類のControllerから適切なControllerを選択します。
Ember.jsのControllerとは?
ControllerとModel – Ember.js入門(3)で簡単に説明しましたが、EmberのControllerはControllerがModelをdecorateする設計となっています。このため、すべてのControllerには対応するModelがある前提であり、ViewはControllerを通して透過的にModelのプロパティにアクセスすることができます。そして、Controller/Viewに対応するModelは対応付けられるかについては、Routeが決定します(Routingの基本 – Ember.js入門(2))。
例えば、OrderRouteによってModelがOrderControllerに対応付けられ、ModelのitemNameプロパティをOrderControllerのプロパティとしてアクセスできるワケです。
App.Order = Ember.Object.extend({
itemName: null,
price: 0,
quantity: 0,
cost: function() {
return this.get('price') * this.get('quantity');
}.property('price', 'quantity')
});
App.OrderRoute = Ember.Route.extend({
model: function() {
return App.Order.create({itemName: 'Book', price: 980, quantity: 2});
}
});
<script type="text/x-handlebars" data-template-name="orders">
<ul>
{{#each}}
<li>{{#link-to 'order'}}{{itemName}}{{/link-to}}</li>
{{/each}}
</ul>
</script>
なお、OrderControllerは暗黙的に生成されます(後述)。
複数のModel
アプリケーションではOrderControllerのように単一のModelを扱うViewの他に、複数のOrderを扱うケースが頻出します。このため、Ember.jsではModelが単数の場合と複数の場合で、それぞれ便利に扱えるような工夫がされています。配列としてのModelをControllerに対して紐付けたい場合は、Routeで配列を返すだけです。
例えば、OrdersRouteによってModelの配列をOrdersControllerに対応付けることができます。
App.OrdersRoute = Ember.Route.extend({
model: function() {
return [
App.Order.create({itemName: 'Book', price: 980, quantity: 2}),
App.Order.create({itemName: 'DVD', price: 9800, quantity: 1})
];
}
});
<script type="text/x-handlebars" data-template-name="order">
<h2>{{itemName}}</h2>
<p>price:{{price}}</p>
<p>quantity:{{quantity}}</p>
<p>cost:{{cost}}</p>
</script>
なお、OrderControllerは暗黙的に生成されます(後述)。また、Route/Controller/Viewの命名規約には注意してください。
暗黙的なController
非常に好みが分かれる部分ではありますが、Ember.jsは暗黙的なオブジェクトを多く生成します。Routerでorderというrouteを定義したならば、OrderRouteとOrderController、そしてorderという名前のview(テンプレート)が対応します。同様にordersというrouteにはOrdersRouteとOrdersController、そしてordersという名前のview(テンプレート)が対応します。
route名 | route | controller | view(テンプレート) |
---|---|---|---|
order | OrderRoute | OrderController | order |
orders | OrdersRoute | OrdersController | orders |
なお、現実としては、Route/Controllerを暗黙的な状態(デフォルト状態)で利用するケースはまずありません。このため、暗黙的なRoute/Controllerはフレームワークに生成させず、空であっても明示的にRoute/Controllerを定義する方がよいでしょう。
ObjectControllerとArrayController
Emberには2種類のControllerとしての基底クラスがあります。ひとつは単数のModelを扱う場合に利用するObjectController
であり、もうひとつは複数のModelを扱う場合に利用するArrayController
です。
ObjectController
ObjectControllerはEmberのコントローラ層に相当する部分で、単一のオブジェクトをModelとして内包します。先に説明したように、ObjectControllerは対応するModelのプロパティに対し、透過的にアクセス可能です。言い換えれば、Controllerに対するget/setメソッドはControllerに再定義されていない限り、Modelのプロパティに対して作用します。
なお、Routeで単数のModelが対応付けられた場合、暗黙的に生成されるControllerはObjectControllerのサブクラスになります。
ArrayController
ArrayControllerはEmberのコントローラ層に相当する部分で、複数のオブジェクトをModelとして内包します。ArrayControllerの特徴は、それ自体が配列のように振る舞うということです。
ArrayControllerは、ArrayProxyのサブクラスです(Controllerはmixin)。したがって、それ自体がiterableであり、eachヘルパーなどで反復処理を行えます。勿論、サイズを取得するlengthプロパティを持ちます。さらに、filter, sort, reduceなど今時な配列系操作を行うことができます。
なお、Routeで配列Modelが対応付けられた場合、暗黙的に生成されるControllerはArrayControllerのサブクラスになります。
Computed Property
ArrayControllerでは配列Modelにアクセスできるため、配列の状態に対するComputed Property(Computed Properties – Ember.js入門(11))を定義できます。
例えば、配列の要素数がゼロの場合にtrueとなるisEmptyは次のように定義できるでしょう。
App.OrdersController = Ember.ArrayController.extend({
isEmpty: function() {
return this.get('length') == 0;
}.property('model')
});
また、配列の各要素に対する変更をObserveするComputed Propertyを定義し、合計金額を計算させることもできます。
App.OrdersController = Ember.ArrayController.extend({
totalCost: function() {
return this.reduce(function(value, o) {return value + o.get('cost') }, 0);
}.property('model@cost')
});
eachヘルパーでのプロパティへのアクセス
次のようにeachヘルパーを利用した場合、反復処理される各オブジェクトのプロパティにアクセスできます。
<ul>
{{#each}}
<li>{{#link-to 'order'}}{{itemName}}{{/link-to}}</li>
{{/each}}
</ul>
このようにテンプレートの各部分で、どのオブジェクトがコンテキストとなっているかを意識する事がEmber.jsでは重要な考え方となっているので覚えておきましょう。
まとめ
EmberにはObjectControllerとArrayControllerという2種類のControllerがあり、Modelが単数か複数かによって使い分けます。特にArrayControllerでは集約やフィルタリングなど高度な機能をControllerに実装することができることが強みです。Computed Propertyとあわせて使いこなしていきましょう。また、Emberでは暗黙的にControllerなどが生成されます。しかし、可能な限り明示的に宣言し、どちらのControllerが利用されているかを意識した方がハマりにくいと思います。
最後にサンプルコード全体を記載します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ObjectController and ArrayController - Ember.js</title>
</head>
<body>
<script type="text/x-handlebars" data-template-name="orders">
{{#if isEmpty}}No Orders{{/if}}
<ul>
{{#each}}
<li>{{#link-to 'order'}}{{itemName}}{{/link-to}}</li>
{{/each}}
</ul>
<p>Total: {{totalCost}}</p>
</script>
<script type="text/x-handlebars" data-template-name="order">
<h2>{{itemName}}</h2>
<p>price:{{price}}</p>
<p>quantity:{{quantity}}</p>
<p>cost:{{cost}}</p>
</script>
<script type="text/javascript" src="../../ember/1.2.0/jquery-1.10.2.js"></script>
<script type="text/javascript" src="../../ember/1.2.0/handlebars-v1.1.2.js"></script>
<script type="text/javascript" src="../../ember/1.2.0/ember-1.2.0.js"></script>
<script type="text/javascript">
window.App = Ember.Application.create();
App.Order = Ember.Object.extend({
itemName: null,
price: 0,
quantity: 0,
cost: function() {
return this.get('price') * this.get('quantity');
}.property('price', 'quantity')
});
App.Router.map(function() {
this.route("orders", { path: "/" });
this.route("order");
});
App.OrdersRoute = Ember.Route.extend({
model: function() {
return [
App.Order.create({itemName: 'Book', price: 980, quantity: 2}),
App.Order.create({itemName: 'DVD', price: 9800, quantity: 1})
];
}
});
App.OrdersController = Ember.ArrayController.extend({
isEmpty: function() {
return this.get('length') == 0;
}.property('model'),
totalCost: function() {
return this.reduce(function(value, o) {return value + o.get('cost') }, 0);
}.property('model@cost')
});
App.OrderRoute = Ember.Route.extend({
model: function() {
return App.Order.create({itemName: 'Book', price: 980, quantity: 2});
}
});
App.OrderController = Ember.ObjectController.extend({
});
</script>
</body>
</html>