AngularJSのTutorialのstep-1からstep-4までを実施したレポート 後編。
前回、Data Binding の仕組みについてstep-1とstep-
2に沿ってレポートを書きましたが、今回は、
step-3とstep-4に沿って、AngularのData FilterおよびOrderingに関わるstepのレポートになります。
システムで何らかのリソースの一覧画面を作ったら、データ量によるかもしれませんがほぼ必ず検索やソートの機能は必要ですよね。
Angularはそんな検索機能を、ものすごく簡単に導入する仕組みを持っています。
step-3
本説は、簡単な検索フォームをシンプルかつ直感的に実装できることが解るデモページを元に課題を遂行するstepです。
以下、ポイントとなるHTMLを抜粋します。
<ul class="phones">
<li ng-repeat="phone in phones | filter:query">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
どのくらいのデータ量に対してまで耐えられるのか懐疑的なところがありますが、
ここで出てくるフィルタの仕組み(「| filter:query」の部分)が、今回のstepの肝になります。
あまりにも簡単な記述で、シェルでよく使われるパイプとかテンプレートエンジンに不慣れな人には何を書いているのかさっぱりかもしれません。
一昔前なら、配列をforでぐるぐる回してif文で条件に沿ったデータだけ出力する記述になるような記述を、AngularではHTML上で「ng-repeat="phone in phones | filter: query」という一文に込めてしまえるのです。
いい感じですよね。
Search: <input ng-model="query">
そして、この上部のinputタグで入力した内容が瞬時に反映され、リスト表示が切り替わります。
これがAngularのData Bindingの売りですね。
そして、驚くべきことは、TutorialのController記述を確認してみると分かりますが、step-2からController側に対して特に何も修正されていないことです。
詳しい仕組みについては、後述しますが、
Angularのfilterを使っているようです。
(パイプの内部の仕組みについてはもう少し、学習を積んでから説明できればいいなぁ)
繰り返しになりますが、Angularの「| filter:」を使うことで、Linuxのパイプ的な仕組みを、HTML上で「|filter:」と書くだけでよくて、加えてユーザにインタラクティブな形で簡単に提供できる。ってことですよね。
検索を実装するって聞くと、検索対象のレコードをごにょごにょと編集して、入力文字列とマッチングかけてヒットしたデータ配列を作って、リストするDOM制御をして、、、、、、とか考えてしまうのですが、
Angular使うことで、上記の通りシンプルになるようです(実際は隠ぺいしただけなのですが、「如何にプロダクトを早く手間をかけずに仕上げるのか」という目的の上では重要なところなのではないでしょうか)。
このサンプルだとレコード上のすべてのフィールドを調べてくれているのですが、特定のフィールドを調べたいときはどうするのでしょうか、filterの説明で行います。
テスト
このstep-3で、ユーザの検索操作がページ上で行われることになり、ユーザー操作の入力とそれに伴う結果をテストする目的で、end-to-end tests の考え方を覚える必要性を説き伏せられます。
確かに、end-to-end testsという考え方を用いればテストは有効ですが、新しくツールを入れたりブラウザを介したり面倒なことが必要でしょう。と古い世代の私は想像してしまいます。
と・こ・ろが、Angularには、 E2E Testing で説明されているend-to-end testsが備わっていて、かつ、
先に、テストランナー(先にインストールしたKarmaのようなツール)を利用してテストを実行できます。ただし、若干のツールを入れることと更新時に自動的にテストされることは無いようですが。
以下は、Tutorialのそのままのend-to-end testsを実装したコードです。
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);
});
});
});
Jasmineに触れたことがないので、説明に使われても分からなかったのですが、
記述をみれば何をしたいかはだいたい分かりました。
- describeでテストの見出しを作っています
(結構この見出しって後にも先にも重要です。設計方針であり、保守時の目印であるため。)
- 一階層目は、テストするフィーチャ名
- 二階層目は、フィーチャのアクションタイプや目的に応じたテスト見出しを記述
- そして、beforeEachで今回のデモページにアクセス
- it内で、アクセスしたページ内に対して、入力操作と表示結果のチェック。内訳は、
- inputが入力操作で
- expectがその結果のチェックですね
- repeaterはページ内に、指定したjQuery selectorで見つかる要素数を調べてくれているんですね。
Karmaでブラウザを立ち上げてはいますが、実際に確認するのはすべてコンソール上の情報です。
ブラウザ操作も含めて、動作確認を上記の方法でよりシンプルに行うことができるようになってます。
#### 【補足】Angularのfilter
filterに渡す値(expression)
デモでは、filter:に渡す値にユーザーが入力した文字列を利用する形になっていました。
それ以外にも、Object型やfunction(closure)が利用できるようです(closureは主にJavascript記述時でしょうか)。
Objectの場合、同じプロパーティかつ同じ値を持つObjectに絞込できます。
function(closure)の場合、ユーザー側の条件を自由に設定してbooleanを返すようにすれば、
なんでもできますね。
素直に早く使ってみたい。functionはajaxと併用すれば速度面でどうなるかわかりませんが、強力ですね。
filterにもう一つ渡せる値(comparator)
もう一つ渡せる値として、comparatorがあります。
完全一致と部分一致を指定できます。functionで指定すれば、前方一致なり後方一致なり自由に設定可能です。
これもまた利用方法で強力ですね。
また、filterの説明ででてくる「angular.equals」は、覚えるとtypeofなみに利用できそうな予感です。
ここのサンプルで下記のように書くことで検索する属性値の指定方法が載っていました。
本stepのデモに置き換えて記述するならば、下記のようになるでしょうか。
<div class="span2">
<!--Sidebar content-->
Name Search: <input ng-model="query.name"><br/>
Snipet Search: <input ng-model="query.snipet">
</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>
上記HTMLでは、phones内の名前検索とsunipet検索を実装しました。
何ていうか、「検索っていう仕組み」は基盤の実装次第でこんなに簡単に準備できるように設計できるんやなぁ。としみじみ感動しました。
step-4
本stepではソートの仕組みをデモページを元に課題を実施します。
そして、ソートも同様にパイプと同じくとてもシンプルな書き方で実現できることが解ります。
加えて、別の方法として、Controller側のJSファイルにて$scapeにorderByのフィールド設定を追記するだけでもできます。
どちらにしても、シンプル実装です。
下記は、HTML中に記述したサンプルを抜粋しました。
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
orderPropはphonesのフィールド名が設定されます。
phones配列をqueryi文字列で絞り込んで、orderPropフィールドで昇順に並べる指示を、上記一行で実現できます。
テスト
並び順のテストを Seleniumなどのend-to-endのテストで実装するとすると、DOMの配列をループで回すかSelectorでeq(n)と指定してDOMを抽出して確認するでしょうか。
Jasmin式のrepeaterを利用すると、配列で並び順通りの期待値を作って、やれば確認できるそうです。
簡単です。
expect(repeater('.phones li', 'Phone List').column('phone.name')).
toEqual(["MOTOROLA XOOM\u2122",
"Motorola XOOM\u2122 with Wi-Fi"]);
上記は、repeaterを利用してSelectorで指定した要素の中で名前フィールド部分だけを抽出して、期待値の配列とマッチすれば成功となることを実現しています。
ここで、すごいと思ったのが、DOMで取得したデータとModelの属性をバインディングした形で利用できることです。
調べたい要素のID属性とかClass属性とかを指定するのではなく、Modelの属性を指定するだけで、必要情報を取得できます。
全体的にものすごく直感的です。
今回のデモでは、ソートするカラムをSelectボックスで選択させるようにしています。
この選択するユーザ操作も下記の通り実現できます。当然ながらMultipleも当然サポートされています。
select('orderProp').option('Alphabetical');
結果、end-to-end テストで、ソート順を指定して検索した結果を並び順込で確認できる仕組みを
普通にプログラムを書くように実現して、実行できる。
いい感じです。
ここまでのまとめ。
以上でstep-1からstep-4までのレポートを書いた結果から下記のポイントを自分なりに考えました。
(他の方の内容にかぶっている点はご容赦ください)
- サーバー側において、テンプレートエンジンの仕組みは不要。
場合によってはAngularで利用するテンプレートを出力する必要はあるが、リソース表示エリアが動的である必要性は無い。 - サーバー側はデータをJSONで返すControllerを用意するだけでいい。
- 検索機能を実現する場合、ブラウザで画面に問題なく出力できる量のデータに対しては、Angularがあれば、サーバサイドには何もいらないし、かつ、簡単に実装出来る。
(たぶん、巷のトランザクションデータは数万から数十万件はざらにあると思うのでそれに対してはさすがに無理でしょう。全文検索なり使って、サーバにクエリ送ってその結果をJSONデータで受け取って、出力することになるんでしょうか。) - テストも自然な流れでコーディングできるため、品質も自然と上がりそう。
(これについては、製造者に影響されるところではあるので、希望的スタンスです) - Tutorialには関係ないですが、APIドキュメントを見るとHTML書式とJavaScript書式の二種類が記述されているため、Controller側でもView側でも柔軟に組める。
このまま、AngularJSの使い手になれるとしたら、ホットモックなどを作るとき、Angularはとても重宝しそうです。
後、補足で、
Tutorialでは触れてませんが、下記のページで、レガシーなブラウザIE8以上も対応可能なようです。
新しいものを追い求めるだけではないところもいい感じです。