ほげにっき

hogedigoの日記

AngularJSチュートリアルやってメモメモ(step3 - リピーターのフィルタリング)

目次


今回やるのはこちら↓
AngularJS: 1 - Filtering Repeaters


このstepではhtmlの端末リストにフルテキストサーチ機能を追加する。そしてさらにend-to-endテストを書いてみる。


ワークスペースをstep3のにリセット。注:ローカルの修正は破棄される

git checkout -f step-3

ローカルサーバーをリフレッシュすると、 「Search」テキストフィールドが追加されているのが確認出来る。

テキストフィールドに文字列を入力すると、その文字列を含む端末情報でリアルタイムに絞り込みが行われる。


step2からstep3への変更を説明。
完全なdiffはこちらで見られる。

コントローラー

変更なし。

テンプレート

app/index.html:

  <div class="container-fluid">
    <div class="row-fluid">
      <div class="span2">
        <!--Sidebar content-->

        Search: <input ng-model="query">

      </div>
      <div class="span10">
        <!--Body content-->

        <ul class="phones">
          <li ng-repeat="phone in phones | filter:query">
            {{phone.name}}
            <p>{{phone.snippet}}</p>
          </li>
        </ul>

      </div>
    </div>
  </div>


標準HTMLのinputタグが追加され、ngRepeatディレクティブの入力を処理するのにAngularの$filter関数を使用している。
これで、ユーザーの入力した検索条件に対して結果が即座に端末リストに反映される様になる。


本stepのサンプルの内部挙動:

  • データバインディング:ページがロードされた時、Angularはテキストフィールド(inputタグ)の名前"query"をデータモデル内の同名の変数にバインドし、同期し続ける。

サンプルコードでは、"query"テキストフィールドへのユーザー入力値は即座にリストリピーター(phone in phones | filter:query)のフィルター入力値として使用される。データモデルへの変更もリピーターの入力値を変化させ、リピーターはモデルの最新の状態でDOMを更新する。

  • filterフィルター:filter関数はqueryの値を使用して、その値に(前後方一致で)マッチするレコードのみで構成される新しいarrayを作成する。

ngRepeatはフィルターの結果(件数の変更された端末リスト)に応じて自動でビューを更新する。

テスト

step2ではユニットテストの書き方を学んだ。ユニットテストはコントローラーやその他コンポーネントをテストするには完璧だが、アプリケーションのDOM操作や出力までテストするのには向いていない。そこまでテストするにはend-to-endテストがよい。


test/e2e/scenarios.js:

describe('PhoneCat App', function() {

  describe('Phone list view', function() {

    beforeEach(function() {
      browser().navigateTo('../../app/index.html');
    });

    it('should filter the phone list as user types into the search box', function() {
      expect(repeater('.phones li').count()).toBe(3);

      input('query').enter('nexus');
      expect(repeater('.phones li').count()).toBe(1);

      input('query').enter('motorola');
      expect(repeater('.phones li').count()).toBe(2);
    });
  });
});

↑のend-to-endテストはシンタックスはJasmineで描かれたコントローラーのユニットテストにとても似ているが、実際にはAngular's end-to-end test runnerAPIを使用している。


このend-to-endテストを実行するには、ブラウザの新しいtabを開いて下記にアクセスする

以前のstepでKarmaを使用した単体テストのやり方を見たが、end-to-endテストもKarmaを利用して行うことができる。./scripts/e2e-test.shスクリプトを実行すればOK。end-to-endテストは遅いので、単体テストのときと違いKarmaはファイル修正を検知して自動実行することはしない。再実行する際は都度スクリプトを実行すること。


上記テストは検索フィールドとリピーターが正しく結合されているかを検査している。Angularをもちいたend-to-endテストは機能的で可読性の高いend-to-endテストをとても簡単に書くことができる。

実験

  • index.htmlに{{query}}バインディングを追加して、queryモデルの現在の値を表示してみよう。そしてそれが入力フィールドに入れた値に対してどのように変化するか見てみよう。
  • どの様にHTMLページのタイトルにqueryモデルの現在の値を表示してみよう。

まず↓の様に書きたくなるかもしれない:

  <title>Google Phone Gallery: {{query}}</title>

しかしこれはうまく働かない。なぜならばqueryモデルはbodyで定義されたコントローラーのスコープにしか存在しない為。

 <body ng-controller="PhoneListCtrl">

もしtitleタグでqueryモデルを参照したければ、これをhtmlタグに移してスコープを広げる必要がある。

<html ng-app ng-controller="PhoneListCtrl">

bodyタグのng-controllerを削除することを忘れないこと。
これでタイトルにqueryモデルが表示されるようになったが、初期ロード時に一瞬だけ{{query}}がそのまま表示されてしまうことに気付くかもしれない。
これはngBindまたはngBindTemplateディレクティブを使用することで防ぐことが出来る。

<title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery</title>
  • 下記のend-to-endテストをtest/e2e/scenarios.jsのdescribeブロックに追加してみよう。
it('should display the current filter value within an element with id "status"',
    function() {
  expect(element('#status').text()).toMatch(/Current filter: \s*$/);
 
  input('query').enter('nexus');
 
  expect(element('#status').text()).toMatch(/Current filter: nexus\s*$/);
 
  //alternative version of the last assertion that tests just the value of the binding
  using('#status').expect(binding('query')).toBe('nexus');
});

ブラウザのend-to-endテストランナーのページをリフレッシュして、テストが失敗することを確認しよう。テストを成功させる為に、index.htmlにid="status"のdivまたはp要素を追加して、"Current filter:"というprefix付きでqueryバインディングを表示してみよう。
例↓

<div id="status">Current filter: {{query}}</div>
  • end-to-endテスト内にpause()文を埋め込んでテストを実行してみよう。テストランナーは途中で停止し、その時点のアプリケーションの状態が確認出来る。テストランナー停止中もアプリケーションは生きている為、検索queryを変更して確認することが出来、デバッグにとても役立つ。


今回はここまで。次回はstep4 - 2Wayデータ結合