Dart2×AngularDart5 チュートリアル Part4. Routing
この記事は「Webアプリ入門しようよ! in AngularDart」の続きです。
目次
Part1:The Hero Editor
Part2:Master/Detail
Part3:Multiple Components/Services
Part4:Routing ←イマココ!
Part5:HTTP
参考
- 本家英語版 → 公式チュートリアル
- つくるもの → Tour of Heroes(デモ)
- 最終的なソースコード → GitHub
さっそくPart3も終わったようですね!素晴らしい!
え?終わってない?
そんなあなたにはこちら↓を差し上げましょう。
Part3終了時点のソースコード
いよいよ後半戦です!
それでは続きから!Fight!!(再びゴングが鳴る)
ボタンクリックでリストを表示!
ボタンクリックによる条件分岐は、templateにngIf
を追加する方法も考えられます。
ですがここでは、ルーターを活用して実装しましょう!
-
pubspec.yaml
を編集してパッケージを追加!dependencies: angular: ^5.0.0 angular_forms: ^2.0.0 # ↓追加! angular_router: ^2.0.0-alpha+19
-
Ctrl + C
->pub get
->wevdev serve
ルーティーンでパッケージを更新! -
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); }
ここらへんは正直よく分からない!
助けて!!← -
ルートのパスを定義する
route_paths.dart
を作成!- 今回はhttp://localhost:8080/#heroesでHeroリストを表示するべく次のように設定!
import 'package:angular_router/angular_router.dart'; class RoutePaths { // ↓パス(http://localhost:8080/#heroes)を定義 static final heroes = RoutePath(path: 'heroes'); }
-
ルートを定義する
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, ]; }
-
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], )
-
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!
ネクストッ!
ダッシュボードを追加!
これでHeroリストは完成しました!
次はあなたの選ぶトップHeroを表示するダッシュボードを追加してみます!
-
src/dashboard_component.dart
を作成!import 'package:angular/angular.dart'; @Component( selector: 'my-dashboard', templateUrl: 'dashboard_component.html', ) class DashboardComponent {}
-
src/dashboard_component.html
を作成!<h3>Dashboard</h3>
-
パスを
src/route_paths.dart
に定義!class RoutePaths { static final heroes = RoutePath(path: 'heroes'); // ↓追加! static final dashboard = RoutePath(path: 'dashboard'); }
-
ルートを
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!
ネクストッ!
初期画面をダッシュボードにする!
現状、http://localhost:8080はAppComponentのみ表示される画面ですが、自動でhttp://localhost:8080/#dashboardへ飛ぶように設定してみましょう!(これをリダイレクトといいます。)
-
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を表示しましょう!
ついでにタッシュボードを表示するためのボタンも追加します!
-
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>
-
'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(); } }
-
'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>
-
'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!
ネクストッ!
Hero毎の詳細画面を追加!
世の中興奮する事いっぱいあるけど、一番興奮するのは「Heroをクリックしたら、Heroの詳細が表示されること」だね。(伊達)
間違いないね。(富澤)
さて実装しましょう!
-
パスを
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'); }
-
ルートを
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, ]; }
-
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); }
-
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(); }
-
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!
一応、戻るボタンが効くことも確認しておこう!
ネクストッ!
Hero毎の画面へのリンクを各コンポーネントに追加!
-
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'}); }
-
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! -
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'}); }
-
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>
-
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; }
-
src/hero_component.dart
を修正!@Component( selector: 'my-hero', templateUrl: 'hero_component.html', // ↓追加! styleUrls: ['hero_component.css'], directives: [coreDirectives, formDirectives], providers: [ClassProvider(HeroService), ClassProvider(Location)], )
-
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!
ひと休み
ここまでいかがだったでしょうか。
今回はなかなかボリューミーでした。
流石のKentaurosもそろそろお疲れ気味です。
だがやめない!
君がッ!
チュートリアルを終えるまでッ!
説明するのをやめないッ!
ネクストッ! → Part5:HTTP