ObjectControllerとArrayController – Ember.js入門(12)

2014.01.02

この記事は公開されてから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のプロパティとしてアクセスできるワケです。

order-2

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に対応付けることができます。

orders-2

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にはObjectControllerArrayControllerという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>