ほげにっき

hogedigoの日記

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