前回はユーザのインタラクションを受け取る方法を解説しました。今回はこれまでの連載内容を踏まえつつ、Ember.jsが提供する汎用的な機能を解説します。
今回取り扱うEmber.jsアプリケーションはこちらです。
「商品を選択してカートに入れ、まとめて購入できる」というアプリケーションを作りながら、汎用的な機能をひとつずつ紹介していきます。
前準備
本稿の対象バージョンはこちらです。
第3回の記事を参考にして必要なファイルを作成してください。
ファイルの作成が完了したら、今回のアプリケーションを作成するために画面と初期データの準備を行います。
まずはルーティングとデータを準備します。
次はテンプレートを準備します。
さて、ここまでで基本となる画面を準備しました。
ではこれからカートを動くようにしていきましょう。
ArrayController
まずは、選択した商品を保持しておくための置き場所を作成します。
ここで初めて登場したArrayController
について説明します。Ember.ArrayController
はEmber.Controller
の仲間ですが、その名の通り配列を扱うことを得意とします。
ArrayControllerには配列操作のメソッドが数多く定義されており、model
プロパティに設定された配列に対して様々な操作ができます(具体的なメソッドについては後述します)。
JavaScriptネイティブのArrayに定義されているメソッドと比較した時の大きな違いは、ArrayControllerはEmber.jsのデータバインディングと連携することを前提としていて、ArrayControllerへの操作は自動的にテンプレートを更新するようになっています[1]。
今回、カートに追加した商品はこのCartController
に保存することにします。
ProductsRoute
にactions
を追記してください。
products/show
テンプレート中の「カートに入れる」ボタンにaction
を指定します。
action
ヘルパーは、アクション名に続いて引数を与えられます。ここでは商品を引数に指定しています。
次はcart
のテンプレートを以下の内容で書き換えてください。
ここまでで、「カートに入れる」ボタンをクリックして商品を保存できるようになりました。
ではここで、新しく出てきた機能を解説します。
Route#controllerFor()
Route
に定義されているcontrollerFor
メソッドを使うことで、任意のコントローラのインスタンスを取得できます。ここではカートに商品を追加するために、ProductsRoute
からCartController
にアクセスしています。
ArrayController#pushObject
配列に要素を追加します。ネイティブのArray
のpush
メソッドに相当します。実際はmodel
プロパティに設定された配列に要素が追加されます。
さて、ArrayController
のメソッドがでてきたところで、Ember.jsのArray
について少し詳しく見てみることにしましょう。
Array
ArrayController
に定義されている配列操作のメソッドは、実はEmber.jsによってJavaScriptネイティブのArray
のプロトタイプにも定義されています。
JavaScriptにおけるプロトタイプ拡張には賛否ありますが、Ember.jsでは前向きに取り入れることで便利なメソッドを開発者に提供しています。
ここでは、先ほど登場したpushObject
を含めEmber.jsが提供する配列操作のメソッドのいくつかを紹介します。
pushObject()
配列に要素を追加します。
addObject()
pushObject
によく似ていますが、追加しようとしている要素がすでに存在する場合には何も行いません。
findBy()
配列の中から特定の要素を探します。要素のプロパティに対して探索条件を指定できます。
mapBy()
配列中の要素の特定のプロパティを取り出して新しい配列を返します。
この他にもたくさんのメソッドが提供されています。ぜひ公式リファレンスに目を通してみてください。
ちなみに、先ほど紹介したfindBy
を使うと、App.ProductsShowRoute
のmodel
メソッドの実装を簡素化できます。
プロトタイプ拡張を禁止する
また、どうしてもプロトタイプ拡張が好きではない方は、Ember.jsを読み込む前に次の変数を定義しておくことでプロトタイプ拡張を禁止できます。
この状態でEmber.jsが提供するArrayのメソッドを利用したい場合、適宜Ember.A
メソッドを利用して配列を拡張する必要があります(参考)。
横道に逸れてしまいましたが、以上がEmber.jsが拡張しているArray
の話になります。
ではカートの開発に戻ることにしましょう。
Computed Property
ここまでで商品をカートに追加できるようになりました。ただ、今のカート画面は単純に商品を並べて表示しているだけなので親切ではありませんね。
そこで、「何種類の商品がカートに入っているか」「合計金額はいくらなのか」を表示するようにしましょう。
まずは「何種類の商品がカートに入っているか」を表示します。
CartController
を次のように変更します。
cart
テンプレートの商品数を表示している部分を次のように変更します。
さて、ここでは次のような見慣れないコードが出てきました。
ここではComputed Propertyと呼ばれるEmber.jsの機能を使っています。
Computed Propertyとは
Computed Propertyについて一言で説明すると「他のプロパティから算出されるプロパティを定義する機能」です。これについて具体的に見ていくとしましょう。
このFunction
のproperty()
メソッドは、Ember.jsがネイティブのFunction
のプロトタイプを拡張して提供しているメソッドで、Ember.ComputedProperty
オブジェクトを返します。property()
メソッドの引数には、依存するプロパティ名を指定します。
今回のCartController
のケースだと、「カートに入っている商品の種類数(uniqProductCount
)」は「商品の入っている商品の総数(length
)」から算出されます。
もし複数のプロパティから算出されるComputed Propertyを定義したい場合には、そのプロパティ名を列挙します。
この例では、fullName
を参照するとfirstName
とlastName
からfullName
が算出されます。一度算出された値はキャッシュされ、firstName
、lastName
のどちらかが変更されるまでfullName
の結果は再計算されません。
ここまで見ると、Computed Propertyは少し変わったメソッドのように見えるかもしれません。次はメソッドとの決定的な違いを見てきましょう。
メソッドとの違い
Ember.jsのテンプレートの仕組みを考えてみると、その違いに気づくことができます。
Ember.jsのテンプレートはプロパティの変更を検知して自動でHTMLを更新します。通常のメソッドの場合、あるプロパティの変更によりメソッドの戻り値が変わったとしてもテンプレートからはそのことがわかりません。一方Computed Propertyの場合、依存するプロパティが変更されるとHTMLの再描画が必要であるとテンプレートが検知できます[2]。
プロトタイプ拡張
Function.prototype.property()
のプロトタイプ拡張を禁止したい場合は、先ほどのArrayの場合と同じ変数を定義することで禁止できます。
その場合、次のようにEmber.computed
メソッドを経由してComputed Propertyを利用します。
さて、ではComputed Propertyについてより理解を深めるため、続いて商品の合計金額を表示してみます。
CartController
にComputed Propertyの定義を追加します。
cart
テンプレートに次の行を追記します。
property()
メソッドに指定している@each
は、配列の各要の特定のプロパティへの依存を示す素識別子です。ここでは合計金額はカートに追加されている商品の金額に依存していることを表しています。
以上でComputed Propertyについての解説は終わりです。
最後に、CSSを追加して少しだけ見栄えを整えておきましょう。
ここまでで、アプリケーションは次のようになりました。
まとめ
今回はEmber.jsが提供するArrayのメソッドと、Computed Propertyを解説しました。しかし、今回作成したアプリケーションはショッピングカートには次のような機能がなく、不十分です。
- カートの中身が保存されない
- 一度選択した商品は取り消しできない
- 商品を購入できない
次回はこういったショッピングカートとしては当たり前の機能を作成しつつ、今回紹介できなかったEmber.jsの汎用機能を紹介する予定です。どうぞご期待ください。