AngularJSチュートリアルやってメモメモ(step4 - 2wayデータバインディング)
今回やるのはこちら↓
AngularJS: 4 - Two-way Data Binding
このstepでは端末リストの表示順をコントロールする機能を追加する。この動的並び替えを実現する為には、新しいモデルプロパティを追加して、リピーターと一緒に記述するだけでよい。残りはデータバインディングがよろしくやってくれる。
ワークスペースをstep4のにリセット。注:ローカルの修正は破棄される
git checkout -f step-4
ローカルサーバーをリフレッシュすると、 「Search」テキストフィールドに加えて「Sort by」ドロップダウンリストが追加されているのが確認出来る。
リストからソート条件を選択すると、端末リストの表示順が動的に入れ替わる。
step3からstep4への変更を説明。
完全なdiffはこちらで見られる。
テンプレート
app/index.html:
Search: <input ng-model="query"> Sort by: <select ng-model="orderProp"> <option value="name">Alphabetical</option> <option value="age">Newest</option> </select> <ul class="phones"> <li ng-repeat="phone in phones | filter:query | orderBy:orderProp"> {{phone.name}} <p>{{phone.snippet}}</p> </li> </ul>
変更点↓
- ユーザーがソート条件を選択する為のorderPropというselect要素を追加
- filterフィルターの指定をorderByフィルターにチェーン。orderByフィルターはfilterフィルターの結果arrayを受け取ってそれをコピーし、そのコピーを並べ替えてから返却する。リピーターは並べ替えられたarrayを元に端末リストを出力する。
Angularはselect要素とorderPropモデルとの間に2wayデータバインディングを構築する。orderPropはorderByフィルターの入力値に指定されている。
step3で見た様に、モデルへの変更(例えばユーザーのselectドロップダウン選択等による)は即座にviewに自動で反映される。もはやウザいDOM操作コードは不要だ!
コントローラー
function PhoneListCtrl($scope) { $scope.phones = [ {"name": "Nexus S", "snippet": "Fast just got faster with Nexus S.", "age": 0}, {"name": "Motorola XOOM™ with Wi-Fi", "snippet": "The Next, Next Generation tablet.", "age": 1}, {"name": "MOTOROLA XOOM™", "snippet": "The Next, Next Generation tablet.", "age": 2} ]; $scope.orderProp = 'age'; }
- phonesモデル(端末リスト)が修正された。各端末レコードにageプロパティが追加された。このプロパティはselectで「Newest」を選択した際のソートに用いられる
- orderPropのデフォルト値としてageを設定。もしここでデフォルト値を設定しなかった場合、ユーザーがドロップダウンを選択するまでモデルは未初期化のままとなる。
ここで2wayデータバインディングについて説明。ブラウザ上でangularアプリケーションがロードされると、ドロップダウンリストは「Newest」が選択されている。これはコントローラーでorderPropに'age'を設定しているからだ。つまりバインディングはモデル→UIの方向に働いている。そしてユーザーによってドロップダウンに「Alphabetically」が選択されると、モデルも同様に即時更新され、端末リストの順序が自動で入れ替わる。つまりデータバインディングは反対方向(UI→モデル)の方向に対しても働いている。
テスト
変更はユニットテストとend-to-endテストの両方で検証されるのが望ましい。
まずは単体テストの修正から。
test/unit/controllersSpec.js:
describe('PhoneCat controllers', function() { describe('PhoneListCtrl', function(){ var scope, ctrl; beforeEach(function() { scope = {}, ctrl = new PhoneListCtrl(scope); }); it('should create "phones" model with 3 phones', function() { expect(scope.phones.length).toBe(3); }); it('should set the default value of orderProp model', function() { expect(scope.orderProp).toBe('age'); }); }); });
単体テストに、デフォルトソート条件が設定されていることのテストが追加されている。
さらに、Jasmine APIによってコントローラーの構築をbeforeEachブロックで記述する様に拡張された。beforeEachで、それを囲むdescribeブロック内の全てのテストの共通前処理を記述することが出来る。
Karmaの実行結果が以下の様に出力されればOK。
Chrome 22.0: Executed 2 of 2 SUCCESS (0.021 secs / 0.001 secs)
次にend-to-endテストの修正を見る。
test/e2e/scenarios.js
... it('should be possible to control phone order via the drop down select box', function() { //let's narrow the dataset to make the test assertions shorter input('query').enter('tablet'); expect(repeater('.phones li', 'Phone List').column('phone.name')). toEqual(["Motorola XOOM\u2122 with Wi-Fi", "MOTOROLA XOOM\u2122"]); select('orderProp').option('Alphabetical'); expect(repeater('.phones li', 'Phone List').column('phone.name')). toEqual(["MOTOROLA XOOM\u2122", "Motorola XOOM\u2122 with Wi-Fi"]); }); ...
end-to-endテストで、selectボックスによるソート機能が正しく動作しているかを検証することができる。
テストは ./scripts/e2e-test.shを実行するか、 ブラウザでhttp://localhost:8000/test/e2e/runner.htmlを開き更新することで実行出来る。またはAngularJSのgithub上で直接実行できる。
※e2e-test.shは自分の環境では失敗した。あとで調べる
実験
- PhoneListCtrl controllerコントローラーでorderPropに値を設定している文を削除して、Angularがドロップダウンリストに"unknown"という一時的なoptionを追加することを確認しよう。またその時端末リストがソートされていない(本来の記述順)ことを確認しよう。
- index.htmlに{{orderProp}}バインディングを追加して、現在のorderPropの値をテキスト表示してみよう。
今回はここまで。次回はstep5 - XHR(Ajax)&DI。