サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Angular 2入門 05: Routerを使う


Angular 2入門 04: サービスをつくる」(以下「Angular 2入門 04」)の作例(コード002)のコードをさらにコンポーネントに分け、Routerでコンポーネントを切り替えます。Routerはページの表示やURLのパスを変えるナビゲーションの仕組みです。

01 コンポーネントのモジュールからリストのコンポーネントを分ける

前出「Angular 2入門 04」コード002では、コンポーネントのモジュール(app.component)が項目をリスト表示しました。このリスト表示の仕事を、別のモジュール(heroines.component)に分けます(「app」フォルダ内)。メインのコンポーネントとして、ナビゲーションに専念させるためです。リスト表示のコンポーネントは、つぎのように親コンポーネントのコードのコードのほとんどを受け継ぎます。

app.component.ts→heroines.component.ts

import {Component, OnInit} from '@angular/core';

@Component({
	selector: 'my-heroines',  // 'my-app',
		/* <h1>{{title}}</h1> */
	template: `
		<h2>ヒロインたち</h2>
		<ul class="heroines">

		</ul>
		<my-heroine-detail [heroine]="selectedHeroine"></my-heroine-detail>
		`,

	/* ,
	providers: [HeroineService] */
})
// export class AppComponent {
export class HeroinesComponent implements OnInit {
	// title = 'ヒロイン一覧';

}

すると、親コンポーネント(app.component)は、一旦つぎのような質素なつくりになります。子コンポーネントのナビゲーションは、このあと加えていくつもりです。

app.component.ts

// import {Component, OnInit} from '@angular/core';
import {Component} from '@angular/core';
// import {Heroine} from './heroine';
// import {HeroineService} from './heroine.service';
@Component({
	selector: 'my-app',
	template: `
		<h1>{{title}}</h1>
		<my-heroines></my-heroines>
		`
	/* styles: [`

	`],
	providers: [HeroineService] */
})
export class AppComponent {
	title = 'ヒロイン一覧';
	/* heroines: Heroine[];
	selectedHeroine: Heroine;
	constructor(private heroineService: HeroineService) { }
	onSelect(heroine: Heroine): void {

	}
	getHeroines(): void {

	}
	ngOnInit(): void {

	} */
}

また、アプリケーションのモジュール(app.module)に、つぎのようにリスト表示の子コンポーネント(heroines.component)を加えなければなりません。併せて、サービスのモジュール(heroine.service)も組み込みます。そして、デコレータ関数NgModuleに渡す引数のオブジェクトにprovidersとして定めました。モジュールの組み立てを変えただけですから、アプリケーションの動きは変わりません(図001)。ここまで書いたリスト表示(heroines.component)とメインのコンポーネント(app.component)およびアプリケーションのモジュール(app.module)を、以下のコード001にまとめました。

app.module.ts


import {HeroinesComponent} from './heroines.component';
import {HeroineService} from './heroine.service';
@NgModule({

	declarations: [

		HeroineDetailComponent,
		HeroinesComponent
	],
	providers: [HeroineService],

})
export class AppModule {}

図001■リストの項目をクリックすると下にその情報が表れる

図001

コード001■リスト表示とメインのコンポーネントおよびアプリケーションのモジュール

heroines.component.ts

import {Component, OnInit} from '@angular/core';
import {Heroine} from './heroine';
import {HeroineService} from './heroine.service';
@Component({
	selector: 'my-heroines',
	template: `
		<h2>ヒロインたち</h2>
		<ul class="heroines">
			<li *ngFor="let heroine of heroines"
				[class.selected]="heroine === selectedHeroine"
				(click)="onSelect(heroine)">
				<span class="badge">{{heroine.id}}</span> {{heroine.name}}
			</li>
		</ul>
		<my-heroine-detail [heroine]="selectedHeroine"></my-heroine-detail>
		`,
	styles: [`
		.selected {
		background-color: #CFD8DC !important;
			color: white;
		}
		.heroines {
			margin: 0 0 2em 0;
			list-style-type: none;
			padding: 0;
			width: 15em;
		}
		.heroines li {
			cursor: pointer;
			position: relative;
			left: 0;
			background-color: #EEE;
			margin: 0.5em;
			padding: 0.3em 0;
			height: 1.6em;
			border-radius: 4px;
		}
		.heroines li.selected:hover {
			background-color: #BBD8DC !important;
			color: white;
		}
		.heroines li:hover {
			color: #607D8B;
			background-color: #DDD;
			left: 0.1em;
		}
		.heroines .text {
			position: relative;
			top: -3px;
		}
		.heroines .badge {
			display: inline-block;
			font-size: small;
			color: white;
			padding: 0.8em 0.7em 0 0.7em;
			background-color: #607D8B;
			line-height: 1em;
			position: relative;
			left: -1px;
			top: -4px;
			height: 1.8em;
			margin-right: .8em;
			border-radius: 4px 0 0 4px;
		}
	`]
})
export class HeroinesComponent implements OnInit {
	heroines: Heroine[];
	selectedHeroine: Heroine;
	constructor(private heroineService: HeroineService) { }
	onSelect(heroine: Heroine): void {
		this.selectedHeroine = heroine;
	}
	getHeroines(): void {
		this.heroineService.getHeroines().then((heroines: Heroine[]) => this.heroines = heroines);
	}
	ngOnInit(): void {
		this.getHeroines();
	}
}

app.component.ts

import {Component} from '@angular/core';
@Component({
	selector: 'my-app',
	template: `
		<h1>{{title}}</h1>
		<my-heroines></my-heroines>
		`
})
export class AppComponent {
	title = 'ヒロイン一覧';
}

app.module.ts

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import {AppComponent} from './app.component';
import {HeroineDetailComponent} from './heroine-detail.component';
import {HeroinesComponent} from './heroines.component';
import {HeroineService} from './heroine.service';
@NgModule({
	imports: [
		BrowserModule,
		FormsModule
	],
	declarations: [
		AppComponent,
		HeroineDetailComponent,
		HeroinesComponent
	],
	providers: [HeroineService],
	bootstrap: [AppComponent]
})
export class AppModule {}

02 Routerを加える

メインのコンポーネント(app.component)は、子コンポーネントの表示をナビゲートして切り替えることが役割になりました。そのためには、Routerを加えて設定しなければなりません。ナビゲーションには、ブラウザのアドレスバーに示すURLも含みます。そのため、HTMLドキュメント(index.html)には、つぎのようにbase要素を定めます。

index.html

<head>
	<base href="/">

</head>

アプリケーションのモジュール(app.module)には、RouterModuleクラスimportします。すると、NgModuleデコレータ関数に渡す引数のオブジェクトのimportsプロパティに、RouterModule.forRoot()メソッドでつぎのようにURLのパス(pathプロパティ)と表示するコンポーネント(componentブロパティ)が定められます。

app.module.ts


import {RouterModule} from '@angular/router';

@NgModule({
	imports: [

		FormsModule,
		RouterModule.forRoot([
			{
				path: 'heroines',
				component: HeroinesComponent
			}
		])
	],

})
export class AppModule {}

するとつぎに、上記で定めたパスにいつ切り替え、コンポーネントをどこに表示するかです。メインのコンポーネント(app.component)のコードは、つぎのように書き替えます。<a>要素のリンクで切り替えるため、RouterLinkディレクティブにパスを与えます。そして、RouterOutletディレクティブを加えれば、リンクしたパスのコンポーネントがそこに差し込まれます。

app.component.ts


@Component({

	template: `
		<h1>{{title}}</h1>
		<a routerLink="/heroines">ヒロインたち</a>
		<router-outlet></router-outlet>
		`
		/* <my-heroines></my-heroines> */

})
export class AppComponent {

}

図002■リンクをクリックすると下にリストが表れる

図002

コード002■アプリケーションとメインのコンポーネント

app.module.ts

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router';
import {AppComponent} from './app.component';
import {HeroineDetailComponent} from './heroine-detail.component';
import {HeroinesComponent} from './heroines.component';
import {HeroineService} from './heroine.service';
@NgModule({
	imports: [
		BrowserModule,
		FormsModule,
		RouterModule.forRoot([
			{
				path: 'heroines',
				component: HeroinesComponent
			}
		])
	],
	declarations: [
		AppComponent,
		HeroineDetailComponent,
		HeroinesComponent
	],
	providers: [HeroineService],
	bootstrap: [AppComponent]
})
export class AppModule {}

app.component.ts

import {Component} from '@angular/core';
@Component({
	selector: 'my-app',
	template: `
		<h1>{{title}}</h1>
		<a routerLink="/heroines">ヒロインたち</a>
		<router-outlet></router-outlet>
		`
})
export class AppComponent {
	title = 'ヒロイン一覧';
}

03 ダッシュボードのコンポーネントを加える

初めにデフォルトとして、項目リストの前にダッシュボードを表示することにします。そのコンポーネント(dashboard.component)をつぎのように定めましょう。テンプレートはとりあえず「ダッシュボード」というテキストを示すだけです。

dashboard.component.ts

import {Component} from '@angular/core';
@Component({
	selector: 'my-dashboard',
	template: '<h3>ダッシュボード</h3>'
})
export class DashboardComponent {}

アプリケーションのモジュール(app.module)に、つぎのようにデコレータ関数(@NgModule)の引数でコンポーネント(DashboardComponent)を加えます。RouterModule.forRoot()メソッドには、パス(dashboard)を定めたほか、指定がない('')ときのデフォルト転送先(redirectTo)としました(「Redirecting routes」参照)。そして、declarationsの配列に納めています。

app.module.ts


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

@NgModule({
	imports: [

		RouterModule.forRoot([
			{
				path: '',
				redirectTo: '/dashboard',
				pathMatch: 'full'
			},
			{
				path: 'dashboard',
				component: DashboardComponent
			},

		])
	],
	declarations: [
		AppComponent,
		DashboardComponent,

	],

})
export class AppModule {}

メインのコンポーネントには、つぎのように<a>要素でダッシュボードへのリンクを加えます。パスはrouterLinkディレクティブで与えます。これで、初めにダッシュボードが表示されるようになりました(図003)。

app.component.ts


@Component({

	template: `

		<nav>
			<a routerLink="/dashboard">ダッシュボード</a>
			<a routerLink="/heroines">ヒロインたち</a>
		</nav>

		`

})
export class AppComponent {

}

図003■初めにダッシュボードが表示される

図003

04 ダッシュボードに上位のリストを示す

ダッシュボードには、リストの上位4項目を掲げましょう。コンポーネントのクラス(DashboardComponent)に以下のようにOnInitクラスを実装し、コンストラクタでサービス(HeroineService)のインスタンスをプロパティ(heroineService)に与えます。そして、ngOnInit()メソッドが呼び出されたとき、リスト項目から取り出した上位4つのエレメントを配列でプロパティ(heroines)に定めました。

テンプレートは外部ファイル(dashboard.component.html)とし、デコレータ関数(@Component)にプロパティtemplateUrlで定めました。このとき、moduleIdmodule.idを与えなければなりません。

dashboard.component.ts

// import {Component} from '@angular/core';
import {Component, OnInit} from '@angular/core';
import {Heroine} from './heroine';
import {HeroineService} from './heroine.service';
@Component({
	moduleId: module.id,
	selector: 'my-dashboard',
	// template: '<h3>ダッシュボード</h3>'
	templateUrl: 'dashboard.component.html'
})
// export class DashboardComponent {}
export class DashboardComponent implements OnInit {
	heroines: Heroine[] = [];
	constructor(private heroineService: HeroineService) {}
	ngOnInit(): void {
		this.heroineService.getHeroines()
		.then((heroines: Heroine[]) => this.heroines = heroines.slice(0, 4));
	}
}

テンプレートはつぎのようにngForディレクティブで、プロパティ(heroines)の配列から取り出した項目を順に要素に加えています(図004)。ダッシュボードを加えるために書き替えた3つのモジュールは、以下のコード003にまとめたとおりです。

dashboard.component.html

<h3>トップヒロイン</h3>
<div class="grid grid-pad">
	<div *ngFor="let heroine of heroines" class="col-1-4">
		<div class="module heroine">
			<h4>{{heroine.name}}</h4>
		</div>
	</div>
</div>

図004■ダッシュボードに示された上位のリスト

図004

コード003■ダッシュボードを加えるために書き替えた3つのモジュール

dashboard.component.ts

import {Component, OnInit} from '@angular/core';
import {Heroine} from './heroine';
import {HeroineService} from './heroine.service';
@Component({
	moduleId: module.id,
	selector: 'my-dashboard',
	templateUrl: 'dashboard.component.html'
})
export class DashboardComponent implements OnInit {
	heroines: Heroine[];
	constructor(private heroineService: HeroineService) {}
	ngOnInit(): void {
		this.heroineService.getHeroines()
		.then((heroines: Heroine[]) => this.heroines = heroines.slice(0, 4));
	}
}

app.module.ts

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router';
import {AppComponent} from './app.component';
import {HeroineDetailComponent} from './heroine-detail.component';
import {HeroinesComponent} from './heroines.component';
import {HeroineService} from './heroine.service';
import {DashboardComponent} from './dashboard.component';
@NgModule({
	imports: [
		BrowserModule,
		FormsModule,
		RouterModule.forRoot([
			{
				path: '',
				redirectTo: '/dashboard',
				pathMatch: 'full'
			},
			{
				path: 'dashboard',
				component: DashboardComponent
			},
			{
				path: 'heroines',
				component: HeroinesComponent
			}
		])
	],
	declarations: [
		AppComponent,
		DashboardComponent,
		HeroineDetailComponent,
		HeroinesComponent
	],
	providers: [HeroineService],
	bootstrap: [AppComponent]
})
export class AppModule {}

app.component.ts

import {Component} from '@angular/core';
@Component({
	selector: 'my-app',
	template: `
		<h1>{{title}}</h1>
		<nav>
			<a routerLink="/dashboard">ダッシュボード</a>
			<a routerLink="/heroines">ヒロインたち</a>
		</nav>
		<router-outlet></router-outlet>
		`
})
export class AppComponent {
	title = 'ヒロイン一覧';
}

05 詳細情報のコンポーネントからテンプレートを外部ファイルに分ける

詳細情報のコンポーネントも、テンプレートを外部ファイル(heroine-detail.component.html)に分けましょう。手順は前項と同じです。つぎのように、デコレータ関数(@Component)に渡すオブジェクトに、プロパティtemplateUrlmoduleIdを定めます。もちろん、アプリケーションの動きは、そのまま変わりません。

heroine-detail.component.ts


@Component({
	moduleId: module.id,

	/* template: `
		<div *ngIf="heroine">
			<h2>{{heroine.name}}の情報</h2>
			<div><label>番号: </label>{{heroine.id}}</div>
			<div>
				<label>名前: </label>
				<input [(ngModel)]="heroine.name" placeholder="名前">
			</div>
		</div>
	` */
	templateUrl: 'heroine-detail.component.html'
})
export class HeroineDetailComponent {

}

heroine-detail.component.html

<div *ngIf="heroine">
	<h2>{{heroine.name}}の情報</h2>
	<div><label>番号: </label>{{heroine.id}}</div>
	<div>
		<label>名前: </label>
		<input [(ngModel)]="heroine.name" placeholder="名前">
	</div>
</div>

06 詳細情報からボタンでダッシュボードに戻る

詳細情報のテンプレートには、つぎのようにダッシュボードに<button>要素を加えます。clickイベントで呼び出すメソッド(goBack())は、この後コンポーネント(heroine-detail.component)に定めます。

heroine-detail.component.html

<div *ngIf="heroine">

	<div>

	</div>
	<button (click)="goBack()">戻る</button>
</div>

詳細情報のコンポーネント(heroine-detail.component)には、つぎのように@angularのモジュールの3つのクラス(ActivatedRouteParamsおよびLocation)、そしてサービスのクラス(HeroineService)をimportします。すると、コンストラクタには3つのクラスのインスタンスが渡されますので、それらをプロパティに納めておきます。新たに加えたメソッド(goBack())は、Location.back()メソッドでブラウザの履歴にしたがってURLを戻ります(図005)。

heroine-detail.component.ts


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

import {HeroineService} from './heroine.service';

export class HeroineDetailComponent {

	constructor(
		private heroineService: HeroineService,
		private route: ActivatedRoute,
		private location: Location
	) {}
	goBack(): void {
		this.location.back();
	}
}

図005■詳細情報のボタンをクリックするとダッシュボードに戻る

図005

07 ダッシュボードから詳細情報のURLに移る

ダッシュボードの上位リストをリンクにして、クリックしたら詳細情報のID番号を加えた゜URLに移るようにしてみます。テンプレートは、つぎのように項目を<a>要素に書き替えます。リンクを動的に決めるときは、routerLinkディレクティブを角かっこ[]の中に書き、値となるパスの要素は配列に加えます。アプリケーションのモジュール(app.module)に、RouterModule.forRoot()メソッドでそのパスを加えれば、クリックした項目の詳細情報にURLが移ります。ただし、選択した項目のデータはコンポーネントに送られませんので、情報が何も示されません(図006)。

dashboard.component.html

<h3>トップヒロイン</h3>
<div class="grid grid-pad">
	<!--<div *ngFor="let heroine of heroines" class="col-1-4">-->
	<a *ngFor="let heroine of heroines" [routerLink]="['/detail', heroine.id]" class="col-1-4">

	</a>
</div>

app.module.ts


@NgModule({
	imports: [

		RouterModule.forRoot([

			{
				path: 'detail/:id',
				component: HeroineDetailComponent
			},

		])
	],

})
export class AppModule {}

図006■ダッシュボードの上位リストをクリックすると詳細情報のURLに移る

08 ダッシュボードで選択した項目のデータを詳細情報のコンポーネントに示す

詳細情報のURLには、id番号が添えてありました。この値は、後に述べるやり方で調べられます。また、サービス(heroine.service)のモジュールからは、idの含まれたすべての項目のリストが得られました。すると、idを頼りにして項目の情報が取り出せるということです。そのメソッド(getHeroine())も、つぎのようにサービスに加えましょう。引数の値(id)から、Array.find()メソッドで当てはまる項目のオブジェクトを探して返します。

heroine.service.ts


export class HeroineService {
	getHeroines(): Promise<Heroine[]> {

	}
	getHeroine(id: number): Promise<Heroine> {
		return this.getHeroines()
		.then((heroines: Heroine[]) => heroines.find((heroine: Heroine) => heroine.id === id));
	}
}

詳細情報のコンポーネント(heroine-detail.component)は、コンストラクタでサービス(HeroineService)とActivatedRouteオブジェクトをプロパティ(heroineServiceおよびroute)に定めてありました。さらに、RxJSのオペレータswitchMapとAngular 2のクラスOnInitを、つぎのようにimportします。ngOnInit()メソッドは、ActivatedRoute.paramsプロパティにswitchMapオペレータで、サービスのメソッド(getHeroine())から引数のidのオブジェクトを受け取るよう定めます(引数に+演算子を加えたのは数値に変換するためです)。オブジェクトが得られたら、subscribeオペレータでプロパティに定めます(「これだけ知ってれば大丈夫(かもしれない)RxJS5の厳選オペレータ」参照)。

heroine-detail.component.ts

import 'rxjs/add/operator/switchMap';
// import {Component, Input} from '@angular/core';
import {Component, Input, OnInit} from '@angular/core';
import {ActivatedRoute, Params} from '@angular/router';

// export class HeroineDetailComponent {
export class HeroineDetailComponent implements OnInit {

	constructor(
		private heroineService: HeroineService,
		private route: ActivatedRoute,

	) {}
	ngOnInit(): void {
		this.route.params
		.switchMap((params: Params) => this.heroineService.getHeroine(+params['id']))
		.subscribe((heroine: Heroine) => this.heroine = heroine);
	}

}

これで詳細情報のコンポーネントに、パスのidに一致する項目、つまりダッシュボードで選択された項目のデータが示されます(図007)。ダッシュボードから詳細情報の画面に移すために、書き改めたモジュールとテンプレートは以下のコード004にまとめました。

図007■ダッシュボードのでクリックした項目のデータが詳細情報に示される

図007

コード004■ダッシュボードから詳細情報の画面に移すために書き改めたモジュールとテンプレート

heroine.service.ts

import {Injectable} from '@angular/core';
import {Heroine} from './heroine';
import {HEROINES} from './mock-heroines';
@Injectable()
export class HeroineService {
	getHeroines(): Promise<Heroine[]> {
		return Promise.resolve(HEROINES);
	}
	getHeroine(id: number): Promise<Heroine> {
		return this.getHeroines()
		.then((heroines: Heroine[]) => heroines.find((heroine: Heroine) => heroine.id === id));
	}
}

heroine-detail.component.ts

import 'rxjs/add/operator/switchMap';
import {Component, Input, OnInit} from '@angular/core';
import {ActivatedRoute, Params} from '@angular/router';
import {Location} from '@angular/common';
import {Heroine} from './heroine';
import {HeroineService} from './heroine.service';
@Component({
	moduleId: module.id,
	selector: 'my-heroine-detail',
	templateUrl: 'heroine-detail.component.html'
})
export class HeroineDetailComponent implements OnInit {
	@Input()
	heroine: Heroine;
	constructor(
		private heroineService: HeroineService,
		private route: ActivatedRoute,
		private location: Location
	) {}
	ngOnInit(): void {
		this.route.params
		.switchMap((params: Params) => this.heroineService.getHeroine(+params['id']))
		.subscribe((heroine: Heroine) => this.heroine = heroine);
	}
	goBack(): void {
		this.location.back();
	}
}

app.module.ts

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router';
import {AppComponent} from './app.component';
import {HeroineDetailComponent} from './heroine-detail.component';
import {HeroinesComponent} from './heroines.component';
import {HeroineService} from './heroine.service';
import {DashboardComponent} from './dashboard.component';
@NgModule({
	imports: [
		BrowserModule,
		FormsModule,
		RouterModule.forRoot([
			{
				path: '',
				redirectTo: '/dashboard',
				pathMatch: 'full'
			},
			{
				path: 'dashboard',
				component: DashboardComponent
			},
			{
				path: 'heroines',
				component: HeroinesComponent
			},
			{
				path: 'detail/:id',
				component: HeroineDetailComponent
			}
		])
	],
	declarations: [
		AppComponent,
		DashboardComponent,
		HeroineDetailComponent,
		HeroinesComponent
	],
	providers: [HeroineService],
	bootstrap: [AppComponent]
})
export class AppModule {}

heroine-detail.component.html

<div *ngIf="heroine">
	<h2>{{heroine.name}}の情報</h2>
	<div><label>番号: </label>{{heroine.id}}</div>
	<div>
		<label>名前: </label>
		<input [(ngModel)]="heroine.name" placeholder="名前">
	</div>
	<button (click)="goBack()">戻る</button>
</div>

dashboard.component.html

<h3>トップヒロイン</h3>
<div class="grid grid-pad">
	<a *ngFor="let heroine of heroines" [routerLink]="['/detail', heroine.id]" class="col-1-4">
		<div class="module heroine">
			<h4>{{heroine.name}}</h4>
		</div>
	</a>
</div>

09 テンプレートとスタイルを外部ファイルに分ける

項目リストのコンポーネント(heroines.component.ts)は、テンプレートとスタイルを別ファイルに分けましょう。前述のとおりテンプレートのHTMLファイル(heroines.component.html)は、デコレータ関数(@Component)に渡すオブジェクトのtemplateUrlプロパティに定めます。スタイルのCSSファイル(heroines.component.css)は、つぎのようにstyleUrlsプロパティに配列エレメントとして加えます。moduleIdプロパティにmodule.idを与えることも忘れないでください。

heroines.component.ts

@Component({
	moduleId: module.id,
	selector: 'my-heroines',
	/* template: `

		`, */
	templateUrl: 'heroines.component.html',
	/* styles: [`

	`] */
	styleUrls: ['heroines.component.css']
})
export class HeroinesComponent implements OnInit {

}

heroines.component.html

<h2>ヒロインたち</h2>
<ul class="heroines">
	<li *ngFor="let heroine of heroines"
		[class.selected]="heroine === selectedHeroine"
		(click)="onSelect(heroine)">
		<span class="badge">{{heroine.id}}</span> {{heroine.name}}
	</li>
</ul>
<my-heroine-detail [heroine]="selectedHeroine"></my-heroine-detail>

heroines.component.css

.selected {
background-color: #CFD8DC !important;
	color: white;
}
.heroines {
	margin: 0 0 2em 0;
	list-style-type: none;
	padding: 0;
	width: 15em;
}
.heroines li {
	cursor: pointer;
	position: relative;
	left: 0;
	background-color: #EEE;
	margin: 0.5em;
	padding: 0.3em 0;
	height: 1.6em;
	border-radius: 4px;
}
.heroines li.selected:hover {
	background-color: #BBD8DC !important;
	color: white;
}
.heroines li:hover {
	color: #607D8B;
	background-color: #DDD;
	left: 0.1em;
}
.heroines .text {
	position: relative;
	top: -3px;
}
.heroines .badge {
	display: inline-block;
	font-size: small;
	color: white;
	padding: 0.8em 0.7em 0 0.7em;
	background-color: #607D8B;
	line-height: 1em;
	position: relative;
	left: -1px;
	top: -4px;
	height: 1.8em;
	margin-right: .8em;
	border-radius: 4px 0 0 4px;
}

10 Routerのモジュールを分ける

アプリケーション(app.module)のモジュール(app.module)から、Routerの仕事も分けることにします。Routerのモジュール(app-routing.module)には、アプリケーションのコードをコピーしてつぎのように書き替えます。パスとコンポーネントの配列は、わかりやすいように予め定数(routes)に納めました。RouterModuleクラスは、デコレータ関数(@NgModule)に渡すオブジェクトのexportsプロパティに、配列エレメントとして与えます。

app.module.ts→app-routing.module.ts

import {NgModule} from '@angular/core';
/* import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router'; */
import {RouterModule, Routes} from '@angular/router';
// import {AppComponent} from './app.component';
import {HeroineDetailComponent} from './heroine-detail.component';
import {HeroinesComponent} from './heroines.component';
// import {HeroineService} from './heroine.service';
import {DashboardComponent} from './dashboard.component';
const routes: Routes = [
	{
		path: '',
		redirectTo: '/dashboard',
		pathMatch: 'full'
	},
	{
		path: 'dashboard',
		component: DashboardComponent
	},
	{
		path: 'heroines',
		component: HeroinesComponent
	},
	{
		path: 'detail/:id',
		component: HeroineDetailComponent
	}
];
@NgModule({
	imports: [
		/* BrowserModule,
		FormsModule,
		RouterModule.forRoot([

		]) */
		RouterModule.forRoot(routes)
	],
	/* declarations: [
		AppComponent,
		DashboardComponent,
		HeroineDetailComponent,
		HeroinesComponent
	],
	providers: [HeroineService],
	bootstrap: [AppComponent] */
	exports: [RouterModule]
})
// export class AppModule {}
export class AppRoutingModule {}

アプリケーション(app.module)は、つぎのようにRouterのコードを除く替わりに、Routerモジュールのクラス(AppRoutingModule)を読み込みます。ここまで書き替えたリスト表示(heroines.component)とRouterおよびアプリケーションのモジュールは、以下のコード005にまとめました。なお、リスト表示のコンポーネントから外部ファイルに分けたテンプレート(heroines.component.html)とスタイル(heroines.component.css)は前掲のとおりですので省きました。

app.module.ts

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
// import {RouterModule} from '@angular/router';
import {AppComponent} from './app.component';
import {HeroineDetailComponent} from './heroine-detail.component';
import {HeroinesComponent} from './heroines.component';
import {HeroineService} from './heroine.service';
import {DashboardComponent} from './dashboard.component';
import {AppRoutingModule} from './app-routing.module';
@NgModule({
	imports: [
		BrowserModule,
		FormsModule,
		/* RouterModule.forRoot([
			{
				path: '',
				redirectTo: '/dashboard',
				pathMatch: 'full'
			},
			{
				path: 'dashboard',
				component: DashboardComponent
			},
			{
				path: 'heroines',
				component: HeroinesComponent
			},
			{
				path: 'detail/:id',
				component: HeroineDetailComponent
			}
		]) */
		AppRoutingModule
	],
	declarations: [
		AppComponent,
		DashboardComponent,
		HeroineDetailComponent,
		HeroinesComponent
	],
	providers: [HeroineService],
	bootstrap: [AppComponent]
})
export class AppModule {}

コード005■書き替えたリスト表示とRouterおよびアプリケーションのモジュール

heroines.component.ts

import {Component, OnInit} from '@angular/core';
import {Heroine} from './heroine';
import {HeroineService} from './heroine.service';
@Component({
	moduleId: module.id,
	selector: 'my-heroines',
	templateUrl: 'heroines.component.html',
	styleUrls: ['heroines.component.css']
})
export class HeroinesComponent implements OnInit {
	heroines: Heroine[];
	selectedHeroine: Heroine;
	constructor(private heroineService: HeroineService) {}
	onSelect(heroine: Heroine): void {
		this.selectedHeroine = heroine;
	}
	getHeroines(): void {
		this.heroineService.getHeroines().then((heroines: Heroine[]) => this.heroines = heroines);
	}
	ngOnInit(): void {
		this.getHeroines();
	}
}

app-routing.module.ts

import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {HeroineDetailComponent} from './heroine-detail.component';
import {HeroinesComponent} from './heroines.component';
import {DashboardComponent} from './dashboard.component';
const routes: Routes = [
    {
        path: '',
        redirectTo: '/dashboard',
        pathMatch: 'full'
    },
    {
        path: 'dashboard',
        component: DashboardComponent
    },
    {
        path: 'heroines',
        component: HeroinesComponent
    },
    {
        path: 'detail/:id',
        component: HeroineDetailComponent
    }
];
@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule {}

app.module.ts

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import {AppComponent} from './app.component';
import {HeroineDetailComponent} from './heroine-detail.component';
import {HeroinesComponent} from './heroines.component';
import {HeroineService} from './heroine.service';
import {DashboardComponent} from './dashboard.component';
import {AppRoutingModule} from './app-routing.module';
@NgModule({
    imports: [
        BrowserModule,
        FormsModule,
        AppRoutingModule
    ],
    declarations: [
        AppComponent,
        DashboardComponent,
        HeroineDetailComponent,
        HeroinesComponent
    ],
    providers: [HeroineService],
    bootstrap: [AppComponent]
})
export class AppModule {}

11 詳細情報のナビゲーションを整理する

リスト表示(heroines.component)のコンポーネントで項目をクリックすると、詳細情報はリストの下に示されました(前掲図005参照)。これを、ダッシュボードと同じように、別画面で表示するようにしましょう。そのため、テンプレート(heroines.component.html)はつぎのように書き替えて、ボタンで詳細情報のURLに移るようにします。ボタンクリックで呼び出すメソッド(gotoDetail())は、この後コンポーネントに加えます。

heroines.component.html


<ul class="heroines">

</ul>
<div *ngIf="selectedHeroine">
	<h2>
		ヒロイン「{{selectedHeroine.name}}」
	</h2>
	<button (click)="gotoDetail()">詳細を見る</button>
</div>
<!-- <my-heroine-detail [heroine]="selectedHeroine"></my-heroine-detail> -->

リスト表示(heroines.component)のコンポーネントは、つぎのようにRouterクラスをimportし、コンストラクタで引数として受け取ったインスタンスをプロパティ(router)に与えます。すると、Router.navigate()メソッドで、引数に渡した配列の定めるパスに移れるのです。ボタンクリックで呼び出されるメソッド(gotoDetail())に、この処理を加えました。これで、リストの項目を選んだ後、ボタンのクリックでダッシュボードと同じ詳細情報の画面が示されます。

heroines.component.ts


import {Router} from '@angular/router';

export class HeroinesComponent implements OnInit {

	constructor(
		private router: Router,
		private heroineService: HeroineService
	) {}

	gotoDetail(): void {
		this.router.navigate(['/detail', this.selectedHeroine.id]);
	}
}

12 メインのコンポーネントにスタイルを定める

メインのコンポーネントにもスタイルを定めましょう。CSSファイルは「Angular Example - Tour of Heroes: Part 5」のapp.component.cssを使わせてもらいます。つぎのように、ファイルまたはコードをそのままコピーしてしまって構いません。

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;
}

そのうえで、メインのコンポーネント(app.component)のデコレータ関数(@Component)には、引数のオブジェクトにプロパティmoduleIdstyleUrlsをつぎのように与えます。もうひとつ、リンクの<a>要素には、routerLinkActiveディレクティブにクラス(active)を定めました。リンクがアクティブになると、このクラスは動的に加えられます。ダッシュボードとリスト表示のどちらが選ばれているかで、スタイルが変わるということです(図008)。書き改めたリスト表示のコンポーネント(app.component.ts)のテンプレートとコード、およびメインのコンポーネント(app.component)は、以下のコード006にまとめました。

app.component.ts


@Component({
	moduleId: module.id,

	template: `


		<nav>
			<a routerLink="/dashboard" routerLinkActive="active">ダッシュボード</a>
			<a routerLink="/heroines" routerLinkActive="active">ヒロインたち</a>
		</nav>

		`,
	styleUrls: ['app.component.css']
})
export class AppComponent {

}

図008■アクティブなリンクのスタイルが変わる

図008

コード006■リスト表示のコンポーネントのテンプレートとコードおよびメインのコンポーネント

heroines.component.html

<h2>ヒロインたち</h2>
<ul class="heroines">
	<li *ngFor="let heroine of heroines"
		[class.selected]="heroine === selectedHeroine"
		(click)="onSelect(heroine)">
		<span class="badge">{{heroine.id}}</span> {{heroine.name}}
	</li>
</ul>
<div *ngIf="selectedHeroine">
	<h2>
		ヒロイン「{{selectedHeroine.name}}」
	</h2>
	<button (click)="gotoDetail()">詳細を見る</button>
</div>

heroines.component.ts

import {Component, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {Heroine} from './heroine';
import {HeroineService} from './heroine.service';
@Component({
	moduleId: module.id,
	selector: 'my-heroines',
	templateUrl: 'heroines.component.html',
	styleUrls: ['heroines.component.css']
})
export class HeroinesComponent implements OnInit {
	heroines: Heroine[];
	selectedHeroine: Heroine;
	constructor(
		private router: Router,
		private heroineService: HeroineService
	) {}
	onSelect(heroine: Heroine): void {
		this.selectedHeroine = heroine;
	}
	getHeroines(): void {
		this.heroineService.getHeroines().then((heroines: Heroine[]) => this.heroines = heroines);
	}
	ngOnInit(): void {
		this.getHeroines();
	}
	gotoDetail(): void {
		this.router.navigate(['/detail', this.selectedHeroine.id]);
	}
}

app.component.ts

import {Component} from '@angular/core';
@Component({
	moduleId: module.id,
	selector: 'my-app',
	template: `
		<h1>{{title}}</h1>
		<nav>
			<a routerLink="/dashboard" routerLinkActive="active">ダッシュボード</a>
			<a routerLink="/heroines" routerLinkActive="active">ヒロインたち</a>
		</nav>
		<router-outlet></router-outlet>
		`,
	styleUrls: ['app.component.css']
})
export class AppComponent {
	title = 'ヒロイン一覧';
}

13 スタイルを仕上げる

Routerを使ったアプリケーションの動きそのものはこれででき上がりといえます。後は、スタイルを整えましょう。使うのは、やはり前掲「Angular Example - Tour of Heroes: Part 5」のCSSファイルです。詳細情報のコンポーネント(heroine-detail.component)には、同じ名前のつぎのCSSファイル(heroine-detail.component.css)を定めます。デコレータ関数(@Component)にはmoduleIdがすでに与えられていますから、styleUrlsの配列エレメントに加えるだけです。

heroine-detail.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;
}

heroine-detail.component.ts


@Component({
	moduleId: module.id,

	templateUrl: 'heroine-detail.component.html',
	styleUrls: ['heroine-detail.component.css']
})
export class HeroineDetailComponent implements OnInit {

}

ダッシュボードのコンポーネント(dashboard.component)にも、「Angular Example - Tour of Heroes: Part 5」から同じ名前のCSSファイルを定めます。つぎのコードをそのままコピーしてしまって構いません。こちらも、styleUrlsに配列エレメントとして加えるだけです。これで、見栄えはずっとよくなります(図009)。

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;
	}
}

dashboard.component.ts


@Component({
	moduleId: module.id,

	templateUrl: 'dashboard.component.html',
	styleUrls: ['dashboard.component.css']
})
export class DashboardComponent implements OnInit {

}

図009■スタイルが与えられたダッシュボード

図009

余裕があれば、ページのHTMLドキュメント(index.html)が読み込んでいるスタイルシート(styles.css)も、「Angular Example - Tour of Heroes: Part 5」からコピーして上書きしてしまうと、スタイルがさらに細かく加えられます。リスト表示の下に表れるボタンのスタイルが整い、ポインタを重ねたとき指差しカーソルに変わります(図010)。ファイルの数も増えましたので、改めてすべてのコードをこのページに掲げることは避けます。Plunkerの「Angular 2 Example - Tour of Heroines: Part 5」でコードと動きを確かめられるようにアップロードしましたので、そちらをご覧ください。

図010■リスト表示の下に表れるボタンにスタイルが加わった

図010

作成者: 野中文雄
作成日: 2017年1月1日


Copyright © 2001-2017 Fumio Nonaka.  All rights reserved.