ObjectControllerとArrayController – Ember.js入門(12)
あけましておめでとうございます、渡辺です。
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>