AngularDart入門 第6回 ルーティングとビュー切り替え

[第5回](http://qiita.com/laco0416/items/f23289ad85e5715a1d33)ではフォーマッターとサービスについて解説しました。今回はルーティングによりアプリケーションの中でビューを切り替える方法について解説します。

今回の元記事は[6. Creating Views](https://angulardart.org/tutorial/08-ch06-view.html)です。

# まずはサンプルアプリを動かしてみよう
[こちら](https://github.com/angular/angular.dart.tutorial/tree/master/Chapter_06)にサンプルアプリのソースコードがあります。リポジトリをクローンして、**Chapter_06**ディレクトリで`> pub serve`すればサンプルアプリが起動します。

![Imgur](http://i.imgur.com/xKaTqST.png)

新たに"Add Recipe"ボタンと、"Edit Recipe"ボタンが追加され、レシピの詳細表示にURLが対応するようになりました。

# ビューのカプセル化
大きくなってきた`recipe_book.html`をリファクタリングするために、次の2つのコンポーネントを新たに作成します。

- SearchRecipeComponent : レシピの検索部分
- ViewRecipeComponent : レシピの詳細表示部分

```html:recipe_book.html
<h3>Recipe List</h3> ...
    <search-recipe
        name-filter-string="nameFilter"
        category-filter-map="categoryFilterMap">
    </search-recipe> ...
    <div id="recipe-list">...</div> ...
  <section id="details">
    <ng-view></ng-view>
  </section>
```

```html:search_recipe.html
<div id="filters">
  <div>
    <label for="name-filter">Filter ...</label>
    <input id="name-filter" type="text"
           ng-model="nameFilterString">
  </div>
  <div>
    Filter recipes by category:
    <span ng-repeat="category in categories">
      <label>
        <input type="checkbox" ... >{{category}}
      </label>
    </span>
  </div>
  <input type="button" value="Clear Filters"...>
</div>
```

`SearchRecipeComponent`には`nameFilterString`と`categoryFilterMap`の2つのフィールドが定義されています。この2つは`clearFilter()`でDart側から値を更新するために双方向にバインディングします。

```dart
@Component(
    selector: 'search-recipe',
    templateUrl: 'search_recipe.html')
class SearchRecipeComponent {
  Map<String, bool> _categoryFilterMap = {};
  final List<String> categories = [];


  @NgTwoWay('name-filter-string')
  String nameFilterString = "";

  @NgOneWay('category-filter-map')
  Map<String, bool> get categoryFilterMap => _categoryFilterMap;
  void set categoryFilterMap(values) {
    _categoryFilterMap = values;
    categories.clear();
    categories.addAll(categoryFilterMap.keys);
  }

  void clearFilters() {
    _categoryFilterMap.keys.forEach((f) => _categoryFilterMap[f] = false);
    nameFilterString = "";
  }
}
```

# URLによりビューをルーティングする
これまではアプリケーションはひとつのビューしか使っておらず、URLは変わりませんでした。ルーティングを使うことでビューごとにURLが変わり、レシピごとにブックマークできるようになります。

## ルーティングの設定
ルーティングを定義するにはルーティング設定を初期化するためのtypedef `RouteInitializerFn`を実装する必要があります。`RouteInitializerFn`が取る引数は`Router`と`RouteViewFactory`の2つで、これらはAngular側から与えられます。

```dart
recipeBookRouteInitializer(Router router, RouteViewFactory views) {
  // ...
}
```

実装した`RouteInitializerFn`はモジュールにバインドします。

```dart
bind(RouteInitializerFn, toValue: recipeBookRouteInitializer);
```

ルーティングの設定をするために`RouteViewFactory#configure()`に`ngRoute`のマップを渡します。

```dart
void recipeBookRouteInitializer(Router router, RouteViewFactory views) {
  views.configure({
    'add': ngRoute(
        path: '/add',
        view: 'view/addRecipe.html'),
    'recipe': ngRoute(
        path: '/recipe/:recipeId',
        mount: // ...
  });
}
```

`ngRoute`には次のフィールドが設定できます。

- `path` : ルーティングを行うURLパス(相対パス)
- `view` : `<ng-view>`に読み込まれるHTMLファイルの相対パス
- `enter` : ルーティングが行われた際に呼び出されるイベントハンドラ`RouteEnterEventHandler`
- `preEnter` : ルーティング前に呼び出されるイベントハンドラ`RoutePreEnterEventHandler`。戻り値によりルーティングを中断することができる。
- `leave` : ルーティング終了前に呼び出されるイベントハンドラ`RouteLeaveEventHandler`。戻り値によりビューの変更を禁止することができる。
- `defaultRoute` : パスが見つからない際のデフォルトルーティング先にするかどうか

## ルーティング時の処理
サンプルの`defaultRoute`では複雑な処理を行っています。`/recipe`以下のルーティングで不正なURLが与えられた場合、`enter`が呼び出されます。`enter`では`router.go()`によって自動的に`view`ルーティングに切り替えるようになっています。また、`replace: true`により、不正なURLは履歴に残らないようにして、ブラウザバックできないようにしています。

```dart
'view_default': ngRoute(
    defaultRoute: true,
    enter: (RouteEnterEvent e) =>
        router.go('view', {},
            startingFrom: router.root.findRoute('recipe'),
            replace: true))
```

## ルーティングの入れ子構造
`mount`はルーティングを入れ子にするためのフィールドです。次の例では`/recipe/:recipeId`の中に、`/recipe/:recipeId/view`を設定しています。

```dart
'recipe': ngRoute(
    path: '/recipe/:recipeId',
    mount: {
      'view': ngRoute(
          path: '/view',
          view: 'view/viewRecipe.html'),
      // ...
    })
```

今回のアプリケーションでは次のURLをルーティングしています

```
.../#/
.../#/add
.../#/recipe/6/view
.../#/recipe/6/edit
```

## ルーティングとビューの接続
設定したルーティングのビューは`<ng-view>`と接続されます。

```html
<section id="details">
    <ng-view></ng-view>
</section>
```

`<ng-view>`は現在のルーティングに合わせてテンプレートをレンダリングして表示します。

ルーティングからパラメーターを得るためには`RouteProvider`を使います。`ViewRecipeComponent`ではルーティングによりレシピのIDを取得しています。

```dart
ViewRecipeComponent(RouteProvider routeProvider) {
  _recipeId = routeProvider.parameters['recipeId'];
}
```

# その他の機能
今回のサンプルアプリケーションでは、`RecipeBookComponent`からクエリ層を`QueryService`に分離しています。`QueryService`はもともと`_loadData()`で行っていた処理に加えていくつかの追加機能があります。

![QueryService](https://angulardart.org/tutorial/img/ch06-1-QueryServiceOutline.png)

`QueryService`から取得できるのは具体的なオブジェクトではなく、`Future`です。このアプリケーションはデータの量が大きくなると、読み込みに時間がかかるようになることが予想されるからです。これは第5回のサンプルアプリケーションの`_loadData()`も同様です。

`QueryService`にはキャッシュが実装されています。すでにデータを読み込んでいる場合はキャッシュからデータが読み取られます。また、`Future`の連結によってデータが読み込まれたかどうかを保証しています。

```dart
@Injectable()
QueryService(Http this._http) {
  _loaded = Future.wait([_loadRecipes(), _loadCategories()]);
}
```

`_loadRecipes`と`_loadCategories`の呼び出しは`Future.wait`にラップされています。`Future.wait`は「与えられた`Future`がすべて完了したときに値を返す`Future`」を返します。また、`@Injectable`アノテーションはクラスをAngularによる依存性の注入に使えるように公開します。

`QueryServie`のゲッター(`getRecipeById`, `getAllRecipes`, `getAllCategories`)はまず最初にデータがキャッシュされているかどうかをチェックします。キャッシュされていなければロードが終わるまで待ってから値を返す`Future`を返します。キャッシュがある場合はキャッシュされた値を返す`Future`を返します。

```dart
Future<Recipe> getRecipeById(String id) {
  return _recipesCache == null
      ? _loaded.then((_) => _recipesCache[id])
      : new Future.value(_recipesCache[id]);
}
```

# まとめ

- ルーティングは`RouteInitializerFn`を実装する
- `ngRoute`によりルーティングを定義する
- ルーティングされたビューは`<ng-view>`に反映される

# 演習
今回のアプリケーションにはレシピの追加と編集の呼び出し部分だけを作りました。演習ではこの2つを実装してみましょう。演習の1と2ではレシピのデータの追加、変更は`_recipesCache`の中だけに反映すればいいです。最後の演習では変更したデータをデータストア(今回は`recipes.json`)に書き込みます。

1. レシピの変更をサポートしましょう。まずはレシピ編集用のコンポーネントを作る必要があります。初めはレシピのタイトルだけでもいいですが、すべてのプロパティを変更できるように徐々に改良していきましょう。
2. レシピの追加もサポートしましょう。
3. 1と2の演習が終わったら、変更したレシピをサーバーサイドのデータストアとして振る舞う`recipe.json`に書き込んでみましょう。`QueryService`にいくつかのメソッドを追加する必要があるでしょう。Dart Editorから`Run in Dartium`で起動した開発用のサーバーはリソースの保存に対応していないため、この演習のテストを行うにはサンプルアプリケーションの`bin`フォルダに提供されているコマンドライン用のサーバーアプリケーションを実行するか、`pub serve`を使う必要があります。

## さらに演習
演習を終えてまだ余裕がある方は、レシピが編集中のときはページを離れられないようにルーティングの`leave`を実装してみましょう。

最後の[第7回](http://qiita.com/laco0416/items/18e36df9439a8844201c)は[7. Deploying Your App
](https://angulardart.org/tutorial/09-ch07-deploying-your-app.html)を元にバージョン1.0に置き換えて解説します。