AngularDart入門 第5回 FormatterとService

[第4回](http://qiita.com/laco0416/items/f23289ad85e5715a1d33)ではデコレーターについて解説しました。今回はフォーマッターとサービスについて説明します。今までハードコーディングしていたレシピのデータをサービスから取得し、フォーマッターを使ってデータをフィルタリングします。

今回の元記事は[5. Introducing Formatters and Services](https://angulardart.org/tutorial/07-ch05-filter-service.html)です。

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

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

レシピを名前で絞り込むフィルターと、カテゴリで絞り込むフィルターが追加されています。

# フォーマッターを理解する

## 組み込みのフォーマッター
AngularDartには便利なフォーマッターがすでに組み込まれています。[Currency](https://docs.angulardart.org/#angular/angular-formatter.Currency)フォーマッターは数字を通貨のように整形します。[Date](https://docs.angulardart.org/#angular/angular-formatter.Date)は日付を整形します。[その他](https://docs.angulardart.org/#angular/angular-formatter)にも便利なフォーマッターがあります。今回の名前での絞り込みには[Filter](https://docs.angulardart.org/#angular/angular-formatter.Filter)フォーマッターを使います。`Filter`フォーマッターはリストの中からプロパティが指定された値と一致するものをフィルタリングしてくれます。

まずはじめにフィルターに使うモデルを`input`に設定します

```html
<input id="name-formatter" type="text" ng-model="nameFilterString">
```

そしてフォーマッターを`ng-repeat`に与えます。

```html
<ul>
    <li class="pointer" ng-repeat="recipe in recipes | filter:{name:nameFilterString}">
    ...
    </li>
</ul>
```

最後にコンポーネントにモデルを宣言します

```dart
String nameFilterString = "";
```

# 独自フォーマッターを作る
組み込みフォーマッターは便利ですが、アプリケーションの中ではもっと目的に特化したフォーマッターが必要になることがあります。今回はレシピをカテゴリで絞り込むために`CategoryFilter`という新しいフォーマッターを作成します。

AngularDartのフォーマッターはシンプルな実装からなり、次の`call`メソッドを実装するだけで動作します。

```dart
class MyFormatter {
  call(valueToFormat, optArg1, ..., optArgN) { ... }
}
```

第1引数はフォーマッターの対象となるもので、今回はレシピのリストです。残りの引数はオプショナルな引数で、HTML側から与えられます。今回はオプショナルパラメータを1つだけ使い、表示するレシピをフィルタリングします。

```dart
List call(recipeList, filterMap) {
    if (recipeList is Iterable && filterMap is Map) {
      // If there is nothing checked, treat it as "everything is checked"
      bool nothingChecked = filterMap.values.every((isChecked) => !isChecked);
      return nothingChecked
          ? recipeList.toList()
          : recipeList.where((i) => filterMap[i.category] == true).toList();
    }
    return const [];
  }
```

フォーマッターとしてクラスを定義するには`@Formatter`アノテーションを付け、HTML側での名前を設定します。

```dart
@Formatter(name: 'categoryfilter')
class CategoryFilter { ... }
```

クラスを作ったらモジュールにも忘れずにバインドします。

```dart
class MyAppModule extends Module {
  MyAppModule() {
    ...
    bind(CategoryFilter)
    ...
  }
}
```

HTML側にフィルタリング用のマップのモデルを設定します。

```html
<div>
    <label>Filter recipes by category
        <span ng-repeat="category in categories">
          <label>
            <input type="checkbox"ng-model="categoryFilterMap[category]">
            {{category}}
          </label>
        </span>
    </label>
</div>
```

独自フォーマッターは組み込みフォーマッターと全く同じように呼び出せます。

```html
<li class="pointer" ng-repeat="recipe in recipes | categoryfilter:categoryFilterMap">
```

フォーマッターは連結することができます。

```html
<li class="pointer"
    ng-repeat="recipe in recipes | orderBy:'name' | filter:{name:nameFilterString} | categoryfilter:categoryFilterMap">
```

# Httpサービスを使う
今回はレシピとカテゴリのデータを外部のJSONファイルからHTTP通信で読み込んでいます。HTTP通信を使うには組み込みの[Http](https://docs.angulardart.org/#angular-core.Http)サービスを使います。

はじめにrecipe-bookコンポーネントから`Http`サービスにアクセスできるようにします。コンストラクタに`Http`型の引数を取るだけです。AngularDartは型情報を元に実行時に動的に依存性の注入を行ってくれます。モジュールへの変更は必要ありません。

```dart
class RecipeBookComponent {
  final Http _http;
  ...
  RecipeBookComponent(this._http) {
    _loadData();
  }
  ...
}
```

レシピのデータを取得する処理は次のように書きます。

```dart
void _loadData() {
  recipesLoaded = false;
  ...
  _http.get('recipes.json')
    .then((HttpResponse response) {
      recipes = response.data.map((d) => new Recipe.fromJson(d)).toList();
      recipesLoaded = true;
    })
    .catchError((e) {
      recipesLoaded = false;
      message = ERROR_MESSAGE;
    });
  ...
}
```

`Http`の`get`は次のように書きます。Dartの組み込みの`Future`を使っているので非同期に実行されます。

```dart

_http.get(URL)
  .then((value) {
    // use value
  })
  .catchError((e) {
    // handle error
  });
```

データの取得は非同期に実行されるので、最初に同期的にモデルを初期化する必要があります。

```dart

List categories = [];
List<Recipe> recipes = [];
```

HTML側ではレシピとカテゴリ両方の取得が終わったかどうかで`ng-switch`を使いビューを切り替えています。`ng-switch`に与えられた値は`ng-switch-when`で一致するビューを表示します。

```html
<div recipe-book ng-cloak>
  <div ng-switch="recipesLoaded && categoriesLoaded">
    <div ng-switch-when="false">
      {{message}}
    </div>
    <div ng-switch-when="true">
      <h3>Recipe List</h3>
      ...
    </div>
  </div>
</body>
```

# Angularの機能

# ng-cloak
HTMLが読み込まれてからバインディングが始まるまでの時間、`{{ }}`が見えることを防ぐための仕組みです。`ng-cloak`が付けられた要素の中身はAngularがレンダリングを終えるまで表示されません。

# まとめ

- フォーマッターはモデルを整形する
- HTTP通信にはHttpサービスを使う
- コンストラクタの引数の型をもとにサービスへの依存性が注入される

# 演習
ここまでの内容が理解できているかどうか、次のことを試してみましょう

1. レシピの中の“sugar”と“powdered sugar”を“maple syrup”に置き換えるフォーマッターを作ってみましょう。ヒント: String#replaceAll()
2. レシピの指示中のファーレンハイト温度(degree F)をセルシウス温度(degree C)に変換するフォーマッターを作ってみましょう。変換した値は5の倍数に丸めてください。(例: 300 degrees F は150 Cになります) ヒント: Dartの正規表現を使ってみましょう。
3. 大きなパーティを開くために、レシピの分量を人数倍にできるようにしましょう。ヒント: まず、レシピのモデルを変更し、Ingredientに量と単位、そして材料の名前の3つのフィールドを追加しなければなりません。次に、ユーザーが分量を2倍、3倍にできるよう、`multipilier`の入力エリアを追加しましょう。最後に入力された数字を元に分量を倍にするフォーマッターを作成しましょう。
4. 最後に、1と2のフォーマッターをチェックボックスで切り替えられるようにしてみましょう。

[第6回](http://qiita.com/laco0416/items/f540cf4a40d5a8218d73)は[6. Creating Views
](https://angulardart.org/tutorial/08-ch06-view.html)を元にバージョン1.0に置き換えて解説します。