Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 5 years have passed since last update.

第5回ではフォーマッターとサービスについて解説しました。今回はルーティングによりアプリケーションの中でビューを切り替える方法について解説します。

今回の元記事は6. Creating Viewsです。

まずはサンプルアプリを動かしてみよう

こちらにサンプルアプリのソースコードがあります。リポジトリをクローンして、Chapter_06ディレクトリで> pub serveすればサンプルアプリが起動します。

Imgur

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

ビューのカプセル化

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

  • SearchRecipeComponent : レシピの検索部分
  • ViewRecipeComponent : レシピの詳細表示部分
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>
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にはnameFilterStringcategoryFilterMapの2つのフィールドが定義されています。この2つはclearFilter()で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が取る引数はRouterRouteViewFactoryの2つで、これらはAngular側から与えられます。

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

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

bind(RouteInitializerFn, toValue: recipeBookRouteInitializer);

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

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は履歴に残らないようにして、ブラウザバックできないようにしています。

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

ルーティングの入れ子構造

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

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

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

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

ルーティングとビューの接続

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

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

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

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

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

その他の機能

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

QueryService

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

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

@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を返します。

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回7. Deploying Your App
を元にバージョン1.0に置き換えて解説します。

lacolaco
I play Angular and pray for Angular. No Breaking Changes No Life.
https://lacolaco.net
classi
学校の先生・生徒・保護者向けのB2B2Cの学習支援Webサービス「Classi(クラッシー)」 を開発・運営している会社です。
https://classi.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away