Angularチュートリアルその6 ルーティング

Angularをチュートリアルを使って勉強する機会があったので、その時の内容について説明していきます。

Angularの本家サイトのチュートリアルの「Routing」の説明をしていきたいと思います。

Routingにより、ビューの切り替えができるようになります。今回はダッシュボードビューを追加して、詳細画面と一覧画面を相互に移動できる機能を実装していきたいと思います。

本家サイトはこちらになります。

作成するプロジェクトは「Tour of Heroes」というアプリケーションです。

作成するアプリの概要についてはこちらから確認できます。

では、早速始めていきたいと思います!

開発環境

  • macOS Sierra 10.12.6
  • node 8.4.0
  • npm 5.3.0
  • Angular 4.3.6

AppRoutingModuleの追加

下記コマンドを実行してAppRoutingModuleを追加します。

$ ng generate module app-routing --flat --module=app

作成されたapp-routing.module.tsというファイルの中身はこんな感じです。

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: []
})
export class AppRoutingModule { }

RoutingではCommonModuleではなく、RouterModuleRoutesを使用するので、インポート部分を修正します。

また、RouterModuleをエクスポートする必要があるので、@NgModuleの部分も修正します。

app-routing.module.tsを修正するとこのようになります。

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

@NgModule({
  exports: [ RouterModule ]
})
export class AppRoutingModule {}

ルートの追加

ルーティングに関する情報を定義していきます。パスとコンポーネントを設定していきます。

app-routing.module.tsにコンポーネントのインポート文とルートの定義を行います。

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { HeroesComponent} from './heroes/heroes.component';

const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];

@NgModule({
  exports: [ RouterModule ]
})
export class AppRoutingModule {}

ここまで実装した状態で、ng serverコマンドでアプリを実行して、「http://localhost:4200/heroes」にアクセスすると、以下の画面が表示されるようになります。

RouterModule.forRoot()

RouterModuleを利用してルートをインポートします。app-routing.module.ts@NgModuleを下記のように編集します。

@NgModule({
  imports: [ RouterModule.forRoot(routes) ],
  exports: [ RouterModule ]
})

RouterOutletの追加

app.component.htmlを編集し、router-outletという要素を追加します。router-outletの要素の所にルーティングされたビューが表示されるようになります。

<h1>{{title}}</h1>
<router-outlet></router-outlet>
<app-messages></app-messages>

この状態でng serveコマンドを実行すると「http://localhost:4200」にアクセスしてもタイトルだけが表示されますが、「http://localhost:4200/heroes」にアクセスすると、一覧と詳細のビューが表示されるようになります。

navigation link(routerLink)の追加

app.component.htmlnavigation linkを追加します。

これを追加することで、navigation linkに設定したリンクへ移動することができるようになります。

<h1>{{title}}</h1>
<nav>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

ダッシュボードビューの追加

ダッシュボードビューを追加していきます。

まずは、コンポーネントを追加します。

ng generate component dashboard

追加されたファイルをそれぞれ編集していきます。

dashboard.component.html

<h3>Top Heroes</h3>
<div class="grid grid-pad">
  <a *ngFor="let hero of heroes" class="col-1-4">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </a>
</div>

dashboard.component.ts

import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
  heroes: Hero[] = [];

  constructor(private heroService: HeroService) { }

  ngOnInit() {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes.slice(1, 5));
  }
}

dashboard.component.css

/* DashboardComponent's private CSS styles */
[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;
  }
}

ダッシュボードのルートを追加する

app-routing.module.tsにダッシュボードコンポーネントをルートとして追加します。

DashboardComponentをインポートします。

import { DashboardComponent }   from './dashboard/dashboard.component';

routesDashboardComponentを追加します。

const routes: Routes = [
  { path: 'heroes', component: HeroesComponent },
  { path: 'dashboard', component: DashboardComponent },
];

デフォルトルートの追加

router-outletにデフォルトで表示するビューの情報をルートとして、app-routing.module.tsroutesに追加します。

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'heroes', component: HeroesComponent },
  { path: 'dashboard', component: DashboardComponent },
];

ダッシュボードビューへのリンクの追加

app.component.htmlにダッシュボードビューへのリンクを追加します。

<h1>{{title}}</h1>
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

詳細画面へのナビゲーション

詳細画面へ移動するケースは下記の3通りとなります。

  1. ダッシュボードのheroをクリックした場合
  2. hero listからheroをクリックした場合
  3. ブラウザのURL入力欄から直接詳細画面へアクセスされた場合(URL直入力)

HeroesComponentからhero detailsの削除

今まではHeroesComponentを呼び出した際に詳細画面も一緒に表示していましたが、これからは画面遷移してから表示するようになるので、heroes.component.htmlから詳細画面に関するタグ(app-hero-detail)を削除します。

タグを削除した後のheroes.component.htmlは以下のようになります。

<h2>My Heroes</h2>

<ul class="heroes">
  <li *ngFor="let hero of heroes"
      [class.selected]="hero === selectedHero"
      (click)="onSelect(hero)">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

hero detailのルートを追加する

詳細画面は~/detail/11のようなURLでナビゲーションします。11idとなっています。

app-routing.module.tsを編集します。

まず、HeroDetailComponentをインポートします。

import { HeroDetailComponent }  from './hero-detail/hero-detail.component';

routesにHeroDetailComponent“`を追加します。

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'heroes', component: HeroesComponent }
];

DashboardComponentの詳細画面へのリンクについて

DashboardComponentに詳細画面へのリンクを追加します。dashboard.component.htmlを編集します。

<h3>Top Heroes</h3>
<div class="grid grid-pad">
  <a *ngFor="let hero of heroes" class="col-1-4"
     routerLink="/detail/{{hero.id}}">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </a>
</div>

HeroesComponentの詳細画面へのリンクについて

HeroesComponentに詳細画面へのリンクを追加します。heroes.component.htmlを編集します。

<h2>My Heroes</h2>

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </a>
  </li>
</ul>

コードの削除

HeroesComponentで利用しなくなったonSelect()メソッドとselectedHeroプロパティを削除します。

削除後のheroes.component.tsHeroesComponentクラスは以下のようになります。

export class HeroesComponent implements OnInit {
  heroes: Hero[];

  constructor(private heroService: HeroService) { }

  ngOnInit() {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes()
    .subscribe(heroes => this.heroes = heroes);
  }
}

HeroDetailComponentをルーティングできるようにする

今のままではHeroDetailComponentのルーティングはきちんと動かないので、hero-detail.component.tsに修正を加えていきます。

インポート文を追加します。

import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';

import { HeroService }  from '../hero.service';

constructorに変数を追加します。

constructor(
  private route: ActivatedRoute,
  private heroService: HeroService,
  private location: Location
) {}

ルーティングのパラメータであるidの抽出

getHero()を次のように記述し、ngOnInit()にも追加していきます。

ngOnInit(): void {
  this.getHero();
}

getHero(): void {
  const id = +this.route.snapshot.paramMap.get('id');
  this.heroService.getHero(id)
    .subscribe(hero => this.hero = hero);
}

HeroServiceにgetHero()を追加する

HeroServiceを開いて、getHero()メソッドを追加します。

getHero(id: number): Observable<Hero> {
  // Todo: send the message _after_ fetching the hero
  this.messageService.add(`HeroService: fetched hero id=${id}`);
  return of(HEROES.find(hero => hero.id === id));
}

戻る処理の追加

今のままではブラウザバック以外の画面を「戻る」手段がないので、「戻る」ボタンを配置して、戻る処理を明示的に実行できるようにします。

hero-detail.component.htmlに戻るボタンを追加します。

<div *ngIf="hero">
  <h2>{{ hero.name | uppercase }} Details</h2>
  <div><span>id: </span>{{hero.id}}</div>
  <div>
    <label>name:
      <input [(ngModel)]="hero.name" placeholder="name"/>
    </label>
  </div>
  <button (click)="goBack()">go back</button>
</div>

「戻る」ボタンに対応する処理をhero-detail.component.tsに記述していきます。

goBack(): void {
  this.location.back();
}

完成イメージ

チュートリアルが完了するとこんな感じで動きます。

ソースコードについて

今までのソースコードはGithubにあげてますので、詳細を確認したい方はこちらからソースコードを見てもらえればと思います。

最後に

今回のチュートリアルでルーティングの実装ができるようになりました。SPA(Single Page Application)もアリだとは思いますが、業務系のシステムなどでは画面遷移できないと実現できないような機能や画面もあると思うので、今回勉強したルーティングの知識を活用して画面遷移可能なアプリケーションを構築していければと思います。