AngularJSチュートリアルやってメモメモ(step2 - Angularテンプレート)
今回やるのはこちら↓
AngularJS: 1 - Angular Templates
このstepではstep1で作成した静的HTMLページを、Angularを使用して動的になものにする。
まずAngularJSは、Model-View-Controller(MVC)パターンを使用してコードを構造化することを推奨している。それに従ってモデル, ビュー, コントローラーをアプリケーションに追加する
ここで言うMVCは昨今サーバーサイドでよく使われているMVC(Model2とか言ってた)とは違う、本来のクライアントサイドにおけるMVCのこと。詳細は端折るがざっくりというと、
- モデルはデータを保持している
- ビュー(複数)はModelが変更された通知を受け取って自分自身の表示を更新する。
- コントローラーは主にモデルとビューの橋渡し(ユーザーコマンドの実行やモデルの更新等)を行う
みたいなイメージ・・・かな。
ワークスペースをstep2のにリセット。ローカルの修正は破棄されるので注意
git checkout -f step-2
GitHubでdiffも見られる。
ビューとテンプレート
Angularでは、ビューはHTMLテンプレートを通したモデルの投影だ。つまりモデルが変更されるとAngularは適切なバインディングポイントをリフレッシュし、ビューが更新される。
まずビューから作成
app/index.html(抜粋)
<html lang="en" ng-app> <head> ... <script src="lib/angular/angular.js"></script> <script src="js/controllers.js"></script> </head> <body ng-controller="PhoneListCtrl"> <ul> <li ng-repeat="phone in phones"> {{phone.name}} <p>{{phone.snippet}}</p> </li> </ul> </body> </html>
まずハードコードされていたAndroid端末リストを、ngRepeatディレクティブと、波括弧2個に囲まれたAngular式:{{phone.name}}と{{phone.snippet}}に置き換えた。
- liタグ中のng-repeat="phone in phones"の記述は、Angularリピーター。このリピーターはphonesリスト中の各phoneに対してliタグ描画を繰り返す。
- {{phone.name}}と{{phone.snippet}}のAngular式はアプリケーションのモデルを参照する。モデルは後述するPhoneListCtrlコントローラーで作成される。
モデルとコントローラー
モデルデータphonesはPhoneListCtrlコントローラーで作成されている。
app/js/controller.js:
function PhoneListCtrl($scope) { $scope.phones = [ {"name": "Nexus S", "snippet": "Fast just got faster with Nexus S."}, {"name": "Motorola XOOM™ with Wi-Fi", "snippet": "The Next, Next Generation tablet."}, {"name": "MOTOROLA XOOM™", "snippet": "The Next, Next Generation tablet."} ]; }
ここではまだコントローラは大したコントロールをしていないが、実際は重要な仕事をしている。コントローラーは、データモデルにコンテキストを提供することで、モデル-ビュー間のデータバインディングを確立する。本サンプルでは下記の様にプレゼンテーション、データ、ロジックコンポーネントが紐づいている。
- PhoneListCtrl - コントローラー関数(controller.js内に記述)の関数名が、HTMLのbodyタグに記述されているngControllerディレクティブの値に一致している。
- android端末データはコントローラー関数に渡されたスコープ($scope)に対してアタッチされている。コントローラースコープはプロトタイプ的にアプリケーション起動時に作成されるルートスコープの子孫となっていて、タグ内部に配置された全てのバインディングから利用可能となっている。
Angularにおけるスコープの概念はとても重要。スコープは、テンプレート、モデル、コントローラーが恊働する為の接着剤の役割をする。
scopeについての詳細は→ angular scope documentation
テスト
Angularの推奨する方法に従って開発をすると、テストも容易となる。ここでは本stepサンプルのコントローラを使用したテストを行う。
test/unit/controllersSpec.js:
describe('PhoneCat controllers', function() { describe('PhoneListCtrl', function(){ it('should create "phones" model with 3 phones', function() { var scope = {}, ctrl = new PhoneListCtrl(scope); expect(scope.phones.length).toBe(3); }); }); });
phones arrayに3つのレコードが格納されていることをテストしている。
Angularの開発者達はテストを書く際にはJasmine(振る舞い駆動開発フレームワーク)のシンタクスを好んでいる。
JasmineについてはJasmine home pageやJasmine wikiを参照。
angular-seedプロジェクトは既にKarmaを使用して全てのテストが実行出来るように設定されている。
実行手順:
- test.sh内のテストランナー起動コマンドがtestacularになっているが、testacularはkarmaに改名されているので、書き換える。
- angular-phonecatディレクトリで ./scripts/test.shを実行する。
- karmaは自動でchromeブラウザを起動する。これはテスト実行に利用されるものなのでとりあえずバックグラウンドにでも移して無視しておけばよい。
- ターミナルにテスト結果が表示される
INFO [karma]: Karma server started at http://localhost:9876/ INFO [launcher]: Starting browser Chrome INFO [Chrome 26.0 (Mac)]: Connected on socket id QEFfAp-HW_Gl6Adk_p_l Chrome 26.0 (Mac): Executed 1 of 1 SUCCESS (0.111 secs / 0.003 secs)
↑テスト成功!
テストの再実行は、コードに修正が入ると自動で行われる。
試してみた。
test/unit/controllersSpec.js:
function PhoneListCtrl($scope) { $scope.phones = [ {"name": "Nexus S", "snippet": "Fast just got faster with Nexus S."}, {"name": "Motorola XOOM™ with Wi-Fi", "snippet": "The Next, Next Generation tablet."}, {"name": "MOTOROLA XOOM™", "snippet": "The Next, Next Generation tablet."}, {"name": "Test", "snippet": "Test device"} ]; }
レコードを1件増やしてみた。
ターミナル:
INFO [watcher]: Changed file "/Users/daigoikeda/github/angular-phonecat/app/js/controllers.js". Chrome 26.0 (Mac) PhoneCat controllers PhoneListCtrl should create "phones" model with 3 phones FAILED Expected 4 to be 3. Error: Expected 4 to be 3. at null.<anonymous> (/Users/daigoikeda/github/angular-phonecat/test/unit/controllersSpec.js:12:35) Chrome 26.0 (Mac): Executed 1 of 1 (1 FAILED) (0.301 secs / 0.004 secs)
おお!再実行された!メッセージも分かりやすい(^o^)
実験
- index.htmlに別のバインディングを追加してみよう。
<p>Total number of phones: {{phones.length}}</p>
- コントローラーで新しいモデルプロパティを作成し、テンプレートでバインドしてみよう。
$scope.hello = "Hello, World!"
ブラウザをリフレッシュして"Hello, World!"と表示されることを確認しよう。
- 単純なテーブルを作成するリピーターを作成してみよう。
<table> <tr><th>row number</th></tr> <tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr> </table>
バインディングでiに1を足して1から始まるリストに修正してみよう。
<table> <tr><th>row number</th></tr> <tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr> </table>
- ユニットテストでtoBe(3)をtoBe(4)に変えて失敗することを確認しよう。
今日はここまで。
次はstep3 - リピーター&フィルター