Dart2×AngularDart5 チュートリアル Part4. Routing

この記事は「Webアプリ入門しようよ! in AngularDart」の続きです。

目次
Part1:The Hero Editor
Part2:Master/Detail
Part3:Multiple Components/Services
Part4:Routing ←イマココ!
Part5:HTTP

参考

さっそくPart3も終わったようですね!素晴らしい!

え?終わってない?
そんなあなたにはこちら↓を差し上げましょう。
Part3終了時点のソースコード

いよいよ後半戦です!
それでは続きから!Fight!!(再びゴングが鳴る)

ボタンクリックでリストを表示!

ボタンクリックによる条件分岐は、templateにngIfを追加する方法も考えられます。
ですがここでは、ルーターを活用して実装しましょう!

  1. pubspec.yamlを編集してパッケージを追加!

        dependencies:
          angular: ^5.0.0
          angular_forms: ^2.0.0
          # ↓追加!
          angular_router: ^2.0.0-alpha+19
    
  2. Ctrl + C->pub get->wevdev serveルーティーンでパッケージを更新!

  3. web/main.dartを修正してルーターを有効化!

    import 'package:angular/angular.dart';
    // ↓追加!
    import 'package:angular_router/angular_router.dart';
    
    import 'package:angular_app/app_component.template.dart' as ng;
    // ↓追加!
    import 'main.template.dart' as self;
    
    // ↓追加!
    @GenerateInjector(
      routerProvidersHash,
    )
    final InjectorFactory injector = self.injector$Injector;
    
    void main() {
        // ↓ルーターを使うように変更!
      runApp(ng.AppComponentNgFactory, createInjector: injector);
    }
    

    ここらへんは正直よく分からない!
    助けて!!←

  4. ルートのパスを定義するroute_paths.dartを作成!

    import 'package:angular_router/angular_router.dart';
    
    class RoutePaths {
      // ↓パス(http://localhost:8080/#heroes)を定義
      static final heroes = RoutePath(path: 'heroes');
    }
    
  5. ルートを定義するsrc/routes.dartを作成!

    import 'package:angular_router/angular_router.dart';
    
    import 'route_paths.dart';
    import 'hero_list_component.template.dart' as hero_list_template;
    
    export 'route_paths.dart';
    
    class Routes {
      static final heroes = RouteDefinition(
        routePath: RoutePaths.heroes,
        // ↓おまじない?
        component: hero_list_template.HeroListComponentNgFactory,
      );
    
      // ↓将来複数のルートを追加しそうなので、全てのルートを取得する関数を定義しておく。
      static final all = <RouteDefinition>[
        heroes,
      ];
    }
    
  6. app_component.dartを修正!

    import 'package:angular/angular.dart';
    // ↓追加!
    import 'package:angular_router/angular_router.dart';
    
    // ↓HeroListComponentではなく、ルーターを使うように変更!
    import 'src/routes.dart';
    
    @Component(
      selector: 'my-app',
      styleUrls: ['app_component.css'],
      templateUrl: 'app_component.html',
      // ↓ルーターを使うように変更!
      directives: [routerDirectives],
      // ↓ここで設定したクラスのみtemplate内で使用できる!
      exports: [RoutePaths, Routes],
    )
    
  7. app_component.htmlを修正!

    <h1>{{title}}</h1>
    <!-- ↓/localhost:8080/#heroesへのリンクを設置 -->
    <nav>
        <a [routerLink]="RoutePaths.heroes.toUrl()" [routerLinkActive]="'active'">Heroes</a>
    </nav>
    <!-- ↓<router-outlet>を設置した所にルート毎のコンポーネントが表示される! -->
    <router-outlet [routes]="Routes.all"></router-outlet>
    

ブラウザを更新!
Heroesボタンをクリックでリストが表示されて、URLが変わったらOK!
dart_8-1

ネクストッ!

ダッシュボードを追加!

これでHeroリストは完成しました!
次はあなたの選ぶトップHeroを表示するダッシュボードを追加してみます!

  1. src/dashboard_component.dartを作成!

    import 'package:angular/angular.dart';
    
    @Component(
      selector: 'my-dashboard',
      templateUrl: 'dashboard_component.html',
    )
    class DashboardComponent {}
    
  2. src/dashboard_component.htmlを作成!

    <h3>Dashboard</h3>
    
  3. パスをsrc/route_paths.dartに定義!

    class RoutePaths {
      static final heroes = RoutePath(path: 'heroes');
      // ↓追加!
      static final dashboard = RoutePath(path: 'dashboard');
    }
    
  4. ルートをsrc/routes.dartに定義!

    import 'hero_list_component.template.dart' as hero_list_template;
    // ↓追加!
    import 'dashboard_component.template.dart' as dashboard_template;
    
    export 'route_paths.dart';
    
    class Routes {
      static final heroes = RouteDefinition(
        routePath: RoutePaths.heroes,
        component: hero_list_template.HeroListComponentNgFactory,
      );
    
      // ↓追加!
      static final dashboard = RouteDefinition(
        routePath: RoutePaths.dashboard,
        component: dashboard_template.DashboardComponentNgFactory,
      );
    
      static final all = <RouteDefinition>[
        heroes,
        // ↓追加!
        dashboard,
      ];
    }
    

ブラウザでhttp://localhost:8080/#dashboardにアクセス!
Dashboardと表示されればOK!
dart_9

ネクストッ!

初期画面をダッシュボードにする!

現状、http://localhost:8080はAppComponentのみ表示される画面ですが、自動でhttp://localhost:8080/#dashboardへ飛ぶように設定してみましょう!(これをリダイレクトといいます。)

  1. src/routes.dartでリダイレクト先を定義!

    class Routes {
      static final heroes = RouteDefinition(
        routePath: RoutePaths.heroes,
        component: hero_list_template.HeroListComponentNgFactory,
      );
    
      static final dashboard = RouteDefinition(
        routePath: RoutePaths.dashboard,
        component: dashboard_template.DashboardComponentNgFactory,
      );
    
      // ↓リダイレクトの定義を追加!
      static final redirect = RouteDefinition.redirect(
        path: '',
        redirectTo: RoutePaths.dashboard.toUrl(),
      );
    
      static final all = <RouteDefinition>[
        heroes,
        dashboard,
        // ↓追加!
        redirect,
      ];
    }
    

ブラウザでhttp://localhost:8080にアクセス!
勝手にhttp://localhost:8080/#dashboardへ飛べばされればOK!

ネクストッ!

ダッシュボードを拡充!

ダッシュボードにトップHeroを表示しましょう!
ついでにタッシュボードを表示するためのボタンも追加します!

  1. app_component.htmlを修正してボタンを追加!

    <h1>{{title}}</h1>
    <nav>
        <!-- ↓追加! -->
        <a [routerLink]="RoutePaths.dashboard.toUrl()" [routerLinkActive]="'active'">Dashboard</a>
        <a [routerLink]="RoutePaths.heroes.toUrl()" [routerLinkActive]="'active'">Heroes</a>
    </nav>
    <router-outlet [routes]="Routes.all"></router-outlet>
    
  2. 'src/dashboard_component.dart'を修正!

    import 'package:angular/angular.dart';
    
    // ↓追加!
    import 'hero.dart';
    import 'hero_service.dart';
    
    @Component(
      selector: 'my-dashboard',
      templateUrl: 'dashboard_component.html',
      // ↓CSSを追加!
      styleUrls: ['dashboard_component.css'],
      // ↓templateで使いたいので追加!
      directives: [coreDirectives],
      // ↓HeroServiceを使いたいので追加!
      providers: [ClassProvider(HeroService)],
    )
    // ↓クラスの詳細を追加!
    class DashboardComponent implements OnInit {
      List<Hero> heroes;
    
      final HeroService _heroService;
    
      DashboardComponent(this._heroService);
    
      @override
      void ngOnInit() async {
        // ↓HeroListの2,3,4,5番目のHeroを取得
        heroes = (await _heroService.getAll()).skip(1).take(4).toList();
      }
    }
    
  3. 'src/dashboard_component.html'を修正!

    • 4つのHero名を■で縦1×横4に並べた画面をつくる。
    • grid grid-pad→Hero名をパッド(■)で格子状に並べるための定義。
    • col-1-4→1×4で横並びにするための定義。
    • module→パッド内のスタイルの定義。
    • hero→Hero名(文字列)のスタイルの定義。
    <h3>Top Heroes</h3>
    <div class="grid grid-pad">
        <div *ngFor="let hero of heroes" class="col-1-4">
            <div class="module hero">
                <h4>{{hero.name}}</h4>
            </div>
        </div>
    </div>
    
  4. 'src/dashboard_component.css'を作成!

    [class*='col-'] {
      float: left;
      padding-right: 20px;
      padding-bottom: 20px;
    }
    [class*='col-']:last-of-type {
      padding-right: 0;
    }
    a {
      text-decoration: none;
    }
    *, *:after, *:before {
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }
    h3 {
      text-align: center; margin-bottom: 0;
    }
    h4 {
      position: relative;
    }
    .grid {
      margin: 0;
    }
    .col-1-4 {
      width: 25%;
    }
    .module {
      padding: 20px;
      text-align: center;
      color: #eee;
      max-height: 120px;
      min-width: 120px;
      background-color: #607D8B;
      border-radius: 2px;
    }
    .module:hover {
      background-color: #EEE;
      cursor: pointer;
      color: #607d8b;
    }
    .grid-pad {
      padding: 10px 0;
    }
    .grid-pad > [class*='col-']:last-of-type {
      padding-right: 20px;
    }
    @media (max-width: 600px) {
      .module {
        font-size: 10px;
        max-height: 75px; }
    }
    @media (max-width: 1024px) {
      .grid {
        margin: 0;
      }
      .module {
        min-width: 60px;
      }
    }
    

ブラウザを更新!
Hero名が4つ表示されていればOK!
dart_10

ネクストッ!

Hero毎の詳細画面を追加!

世の中興奮する事いっぱいあるけど、一番興奮するのは「Heroをクリックしたら、Heroの詳細が表示されること」だね。(伊達)

間違いないね。(富澤)

さて実装しましょう!

  1. パスをsrc/route_paths.dartに定義!

    import 'package:angular_router/angular_router.dart';
    
    // ↓Hero毎のIDが反映される!
    const idParam = 'id';
    
    class RoutePaths {
      static final heroes = RoutePath(path: 'heroes');
      static final dashboard = RoutePath(path: 'dashboard');
      // ↓パスを追加!
      static final hero = RoutePath(path: '${heroes.path}/:$idParam');
    }
    
  2. ルートをsrc/routes.dartに定義!

    // ···
    
    // ↓追加!
    import 'hero_component.template.dart' as hero_template;
    
    export 'route_paths.dart';
    
    class Routes {
    
      // ···
    
      // ↓Heroのルートを定義
      static final hero = RouteDefinition(
        routePath: RoutePaths.hero,
        component: hero_template.HeroComponentNgFactory,
      );
    
      static final all = <RouteDefinition>[
        heroes,
        dashboard,
        redirect,
        // ↓追加!
        hero,
      ];
    }
    
  3. src/hero_service.dartにIDからHeroを取得する関数を実装!

    class HeroService {
      Future<List<Hero>> getAll() async => mockHeroes;
      // ↓追加!
      // ↓全てのHeroの中から、IDが最初に一致したHeroを返す!
      Future<Hero> get(int id) async =>
          (await getAll()).firstWhere((hero) => hero.id == id);
    }
    
  4. src/hero_component.dartを修正!

    • HeroComonentはルーターからHeroのIDを取得して特定のHeroを表示するようにしたい!
    • あと戻るボタンを追加したい!
    import 'package:angular/angular.dart';
    import 'package:angular_forms/angular_forms.dart';
    // ↓ルーターを使うので追加!
    import 'package:angular_router/angular_router.dart';
    
    import 'hero.dart';
    // ↓上で実装したget関数を使いたいので追加!
    import 'hero_service.dart';
    // ↓ルーターを使うので追加!
    import 'route_paths.dart';
    
    @Component(
      selector: 'my-hero',
      templateUrl: 'hero_component.html',
      directives: [coreDirectives, formDirectives],
      // ↓HeroServiceとLocationを使いたいので追加!
      providers: [ClassProvider(HeroService), ClassProvider(Location)],
    )
    class HeroComponent implements OnActivate {
      // 親コンポーネントからは取得しないので、`@Input()`は削除!
      Hero hero;
    
      // ↓
      final HeroService _heroService;
      final Location _location;
    
      HeroComponent(this._heroService, this._location);
    
      // ↓Heroルートに入ったときにHeroを取得する!
      @override
      void onActivate(_, RouterState current) async {
        final id = getId(current.parameters);
        if (id != null) hero = await (_heroService.get(id));
      }
    
      // ↓idが見つかったら文字列からIntに変換して返す!
      // ↓そうでなければ、nullを返す!
      int getId(Map<String, String> parameters) {
        final id = parameters[idParam];
        return id == null ? null : int.tryParse(id);
      }
    
      // ↓戻るボタンのための関数`実装!
      void goBack() => _location.back();
    }
    
  5. src/hero_component.htmlで戻るボタンを追加!

    <div *ngIf="hero != null">
        <h2>{{hero.name}}</h2>
        <div>
            <label>id: </label>{{hero.id}}</div>
        <div>
            <label>name: </label>
            <input [(ngModel)]="hero.name" placeholder="name" />
        </div>
        <!-- 戻るボタンを追加! -->
        <button (click)="goBack()">Back</button>
    </div>
    

ブラウザでhttp://localhost:8080/#heroes/11にアクセス!
Mr. Niceの詳細画面が表示されればOK!
dart_11

一応、戻るボタンが効くことも確認しておこう!

ネクストッ!

Hero毎の画面へのリンクを各コンポーネントに追加!

  1. src/dashboard_component.dartにid毎のURLを生成する関数を追加!

    import 'package:angular/angular.dart';
    // ↓ルーターを使うので追加!
    import 'package:angular_router/angular_router.dart';
    
    // …
    
    @Component(
      // …
      styleUrls: ['dashboard_component.css'],
      // ↓ルーターを使うので追加!
      directives: [coreDirectives, routerDirectives],
      providers: [ClassProvider(HeroService)],
    )
    class DashboardComponent implements OnInit {
    
      // …
    
      // ↓Hero毎のURLを取得する関数を実装!
      String heroUrl(int id) => RoutePaths.hero.toUrl(parameters: {idParam: '$id'});
    }
    
  2. src/dashboard_component.htmlにリンクを追加!

    <h3>Top Heroes</h3>
    <div class="grid grid-pad">
        <!-- ↓div要素からa要素(パイパーリンク)に変更! -->
        <!-- ↓そしてリンク先を追加! -->
        <a *ngFor="let hero of heroes" class="col-1-4" [routerLink]="heroUrl(hero.id)">
            <div class="module hero">
                <h4>{{hero.name}}</h4>
            </div>
        </a>
    </div>
    

    ブラウザを更新!
    ダッシュボードのHero名をクリックして、Heroの詳細が表示されればOK!

  3. src/hero_list_component.dartを修正!

    // …
    
    // ↓ルーターを使うので追加!
    import 'package:angular_router/angular_router.dart';
    
    // …
    
    // ↓ルーターを使うので追加!
    import 'route_paths.dart';
    
    @Component(
    
      // …
    
      // ↓HeroComponentは使わないので削除!
      directives: [coreDirectives],
      providers: [ClassProvider(HeroService)],
      // ↓templateでパイプ処理を書くので追加!
      pipes: [commonPipes],
    )
    class HeroListComponent implements OnInit {
      final HeroService _heroService;
      // ↓追加!
      final Router _router;
      List<Hero> heroes;
      Hero selected;
    
      // ↓ルーターを追加!
      HeroListComponent(this._heroService, this._router);
    
      // …
    
      // ↓Heroの詳細を表示するボタンのための関数を実装!
      Future<NavigationResult> gotoDetail() =>
          _router.navigate(_heroUrl(selected.id));
    
      String _heroUrl(int id) =>
          RoutePaths.hero.toUrl(parameters: {idParam: '$id'});
    }
    
  4. src/hero_list_component.htmlを修正!

    <!-- 選択したHeroの詳細は表示しないので削除! -->
    <h2>Heroes</h2>
    <ul class="heroes">
        <li *ngFor="let hero of heroes" [class.selected]="hero === selected" (click)="onSelect(hero)">
            <span class="badge">{{hero.id}}</span> {{hero.name}}
        </li>
        <!-- ↓選択したHeroの概要と詳細へのリンクを表示! -->
        <div *ngIf="selected != null">
            <!-- ↓パイプ処理でselected.nameを全て大文字に変換! -->
            <h2>
                {{selected.name | uppercase}} is my hero
            </h2>
            <button (click)="gotoDetail()">View Details</button>
        </div>
    </ul>
    
  5. src/hero_component.cssを作成!

    label {
      display: inline-block;
      width: 3em;
      margin: .5em 0;
      color: #607D8B;
      font-weight: bold;
    }
    input {
      height: 2em;
      font-size: 1em;
      padding-left: .4em;
    }
    button {
      margin-top: 20px;
      font-family: Arial;
      background-color: #eee;
      border: none;
      padding: 5px 10px;
      border-radius: 4px;
      cursor: pointer; cursor: hand;
    }
    button:hover {
      background-color: #cfd8dc;
    }
    button:disabled {
      background-color: #eee;
      color: #ccc;
      cursor: auto;
    }
    
  6. src/hero_component.dartを修正!

    @Component(
      selector: 'my-hero',
      templateUrl: 'hero_component.html',
      // ↓追加!
      styleUrls: ['hero_component.css'],
      directives: [coreDirectives, formDirectives],
      providers: [ClassProvider(HeroService), ClassProvider(Location)],
    )
    
  7. app_component.cssを修正!

    h1 {
      font-size: 1.2em;
      color: #999;
      margin-bottom: 0;
    }
    h2 {
      font-size: 2em;
      margin-top: 0;
      padding-top: 0;
    }
    nav a {
      padding: 5px 10px;
      text-decoration: none;
      margin-top: 10px;
      display: inline-block;
      background-color: #eee;
      border-radius: 4px;
    }
    nav a:visited, a:link {
      color: #607D8B;
    }
    nav a:hover {
      color: #039be5;
      background-color: #CFD8DC;
    }
    nav a.active {
      color: #039be5;
    }
    

ブラウザを更新!

ダッシュボードとHeroリストの両方からHeroの詳細画面へ行けたらOK!
dart_12

ひと休み

ここまでいかがだったでしょうか。

今回はなかなかボリューミーでした。

流石のKentaurosもそろそろお疲れ気味です。

だがやめない!

君がッ!
チュートリアルを終えるまでッ!
説明するのをやめないッ!

ネクストッ! → Part5:HTTP