AngularJSチュートリアルやってメモメモ(step7 - ルーティング&マルチビュー)
Angularの公式ドキュメントやチュートリアルの日本語翻訳作業が進んでいることを教えて頂いた。
早くも無駄な作業になってしまった気がするが、途中までやって止めるのもなんだし(いわゆるサンクコストか)自分の学習の為に最後までやることにする^^;
今回やるのはこちら↓
AngularJS: 7 - Routing & Multiple Views
このstepではレイアウトテンプレートの作成方法と、ルーティング機能を利用してマルチビューを持つアプリを作る方法を学ぶ。
ワークスペースをstep7のにリセット。注:ローカルの修正は破棄される
git checkout -f step-7
ローカルサーバーをリフレッシュ(またはAngularサーバーで確認可)。
ブラウザでapp/index.htmlにアクセスするとapp/index.html#phonesにリダイレクトされ、前回と同様の端末リストが表示される。端末リンクをクリックすると端末詳細ページのスタブが表示される。
step6からstep7への変更を説明。diffはこちら。
マルチビュー、ルーティング&レイアウトテンプレート
step7以前は単一のビュー(端末リスト)のみで構成され、全てのテンプレートコードはindex.htmlに記述されていた。このステップではリスト各端末の詳細情報を表示するビューを追加する。
index.htmlファイルを拡張して両方のビューのテンプレートコードを含めることも可能だが、それだとコードがすぐに複雑になってしまう。代わりに、index.htmlを、いわゆる「レイアウトテンプレート」として扱う。これはアプリケーションの全てのビューの為の共通となるテンプレートとなる。他の「部分テンプレート」は現在の「ルート(ユーザーに現在表示されているビュー)」に基づいてレイアウトテンプレートにインクルードされる。
Angularのアプリケーションルートは$routeサービスのプロバイダーである$routeProviderを通して宣言される。このサービスを使用することでコントローラー、ビューテンプレートと現在のブラウザのURLを結びつけて記述することが可能となる(かなり意訳。原文よく分からんかった-_-;)。この機能を用いてディープリンキング(ブラウザのヒストリーフォワード&バックや、ブックマークの制御)を実装することが出来る。
DIに関してメモ。インジェクターとプロバイダー
依存性注入(DI)はAngularJSのコア機能なので、この辺でその挙動を少し理解しておいた方がよい。
アプリケーションが初期起動される時、Angularはアプリの全てのDI関連機能で使用されるインジェクターを作成する。インジェクターそれ自身は$httpや$routeサービスなどが何をするかは知っていないし、それどころかそれらのサービスの存在すら(固有モジュール定義で設定されない限りは)知らない。インジェクターの役割は、指定されたモジュールの定義をロードして、それらのモジュールに定義されている全てのサービスプロバイダーを登録し、そして要求されれば指定されたfunctionの引数にプロバイダによって遅延初期化された依存コンポーネント(サービス)を注入することだ。
プロバイダーはサービスのインスタンスを作成・提供する役割を持ち、またそれらサービスの生成と挙動を制御する為の設定APIを公開する。例として$routeサービスの場合、$routeProviderプロバイダーはアプリケーションのルートを定義する為のAPIを公開している。
Angularのモジュールはアプリケーションのグローバル状態を取り除くという課題を解決し(scopeのことかな?)、インジェクターを設定する手段を提供する。AMDやrequire.jsと違ってAngularモジュールはスクリプトのロード順や遅延フェッチの問題を解決しようとはしていない。これらのモジュールシステムらの目的とAngularモジュールの目的は直交しており、それぞれを満たす為に併用すればよい。
アプリケーションモジュール
app/js/app.js:
angular.module('phonecat', []). config(['$routeProvider', function($routeProvider) { $routeProvider. when('/phones', {templateUrl: 'partials/phone-list.html', controller: PhoneListCtrl}). when('/phones/:phoneId', {templateUrl: 'partials/phone-detail.html', controller: PhoneDetailCtrl}). otherwise({redirectTo: '/phones'}); }]);
アプリケーションにルートを設定する為には、まずアプリケーションモジュールを作成する必要がある。'phonecat'という名前でモジュールを作成し、config APIを使用して$routeProviderをこちらの設定用関数にDIし、その関数の中で$routeProvider.when APIを呼び出してルートを定義している。
インジェクター設定フェーズの間はプロバイダーは上記の様にDIさせることが出来るが、インジェクターが既に作成されサービスインスタンスの作成を開始した後はDIさせることが出来なくなるので注意。
サンプルアプリケーションのルートは下記の様に定義されている。
- URLのハッシュが'/phones'の場合は端末リストviewを表示。viewを構築するのにAngularはphone-list.htmlテンプレートとPhoneListCtrlを使用する。
- ハッシュが'/phone/:phoneId'の場合は'端末詳細viewを表示する。「:phoneId」という部分は動的な変数パートとなりURLの該当部分がルート変数(後述)phoneIdに格納される。端末リストviewを構築するのにAngularはphone-detail.htmlテンプレートとPhoneDetailCtrlコントローラーを使用する。
前回のステップで作成したPhoneListCtrlコントローラーを使い、端末詳細ビューの為の空の新しいPhoneDetailCtrlコントローラーをapp/js/controller.jsファイルに追加する。
$route.otherwise({redirectTo: '/phones'}) という文はブラウザのアクセスされたアドレスがどのルートにもマッチしなかった場合に/phonesへのリダイレクトさせることを意味する。
2番目のルート宣言の「:phoneId」パラメータに注目。$routeサービスは'/phones/:phoneId'をURLとマッチさせる為のテンプレート文字列として使用する。「:notation」という書式で定義された全ての変数は$routeParamsオブジェクトに展開される。
新しく作成したモジュールでアプリケーションを起動する為に、ngAppディレクティブでモジュール名を指定する必要がある。
app/index.html:
<!doctype html> <html lang="en" ng-app="phonecat"> ...
コントローラー
app/js/controllers.js:
... function PhoneDetailCtrl($scope, $routeParams) { $scope.phoneId = $routeParams.phoneId; } //PhoneDetailCtrl.$inject = ['$scope', '$routeParams'];
テンプレート
$routeサービスは通常ngViewディレクティブと併せて使用される。ngViewディレクティブの役割はレイアウトテンプレート内に現在のルートのビューテンプレートをインクルードすることだ。
app/index.html:
<html lang="en" ng-app="phonecat"> <head> ... <script src="lib/angular/angular.js"></script> <script src="js/app.js"></script> <script src="js/controllers.js"></script> </head> <body> <div ng-view></div> </body> </html>
index.htmlから殆どのコードが取り除かれ、ng-view属性を持つdivタグだけのシンプルな1行に置き換わっていることに注目。取り除かれたコードはphone-list.htmlテンプレートに移されている。
app/partials/phone-list.html:
<div class="container-fluid"> <div class="row-fluid"> <div class="span2"> <!--Sidebar content--> Search: <input ng-model="query"> Sort by: <select ng-model="orderProp"> <option value="name">Alphabetical</option> <option value="age">Newest</option> </select> </div> <div class="span10"> <!--Body content--> <ul class="phones"> <li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail"> <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a> <a href="#/phones/{{phone.id}}">{{phone.name}}</a> <p>{{phone.snippet}}</p> </li> </ul> </div> </div> </div>
phone-detail.htmlテンプレートには今のところシンプルに端末IDのみを表示している。
app/partials/phone-detail.html:
TBD: detail view for {{phoneId}}
PhoneDetailCtrlコントローラーで定義されているphoneIdモデルを使用している。
テスト
全てが適切に結合されていることを自動検証する為に、いくつかのURLにナビゲートして正しいビューが表示されるかどうかをチェックするend-to-endテストを書く。
... it('should redirect index.html to index.html#/phones', function() { browser().navigateTo('../../app/index.html'); expect(browser().location().url()).toBe('/phones'); }); ... describe('Phone detail view', function() { beforeEach(function() { browser().navigateTo('../../app/index.html#/phones/nexus-s'); }); it('should display placeholder page with phoneId', function() { expect(binding('phoneId')).toBe('nexus-s'); }); });
./scripts/e2e-test.shまたは、ent-to-endテストランナーのブラウザタブを更新してテストを実行する。またはAngularのサーバーでテスト実行を確認できる。
実験
index.htmlに{{orderProp}}バインディングを追加して、端末リストビューで何も起きないことを確認しよう。これはorderPropモデルが
今日はここまで。次回は「step8 - さらにテンプレート」で端末詳細ビューを実装する。