サイトトップ

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

HTML5テクニカルノート

Angular 5入門 08: HTTPサービスでリモートのデータを取り出して書き替える


Angular 5入門 07: ルーティングで個別情報を示す」は、ダッシュボードやリスト表示のコンポーネントで選択した個別データをルーティングして、詳細情報の表示に遷移させました。本稿では、HTTPサービスによりデータをリモートから取り出し、書き替えるようにします。アプリケーションの見た目の動きは変わりません。

01 HTTPサービスでデータを取り出す

HTTPサービスでデータをやり取りするデータサーバーは、今回Angular in-memory-web-apiモジュールでシミュレートすることにします。このモジュールはデータをメモリ内で管理し、AngularのHTTP通信の仕組みであるHttpClientクラスに対して、サーバーに替わってリクエストを受け、レスポンスを返すのです。モジュールのangular-in-memory-web-apiは、アプリケーションのディレクトリ(angular-tour-of-heroines)にnpmのinstallコマンドでつぎのようにインストールします。

npmコマンド

npm install angular-in-memory-web-api --save

これまでアプリケーションが読み込むデータをもっていたモジュール(mock-heroines)に替えて、データサービスのモジュール(in-memory-data.service)を定めてサーバーのデータとして扱うようにします。つぎのようにデータのモジュールをファイル名とともに書き直してしまえばよいでしょう。クラス(InMemoryDataService)には、InMemoryDbServiceimportしてインタフェースとして実装(implements)します。インタフェースにもとづいてクラスに定めたメソッド(createDb())の戻り値が、サーバーのデータとなるのです。

src/app/mock-heroines.ts → in-memory-data.service.ts

// import {Heroine} from './heroine';
import {InMemoryDbService} from 'angular-in-memory-web-api';
// export const HEROINES: Heroine[] = [
export class InMemoryDataService implements InMemoryDbService {
	createDb() {
		const heroines = [
			{id: 11, name: 'シータ'},
			{id: 12, name: 'ナウシカ'},
			{id: 13, name: 'キキ'},
			{id: 14, name: '千尋'},
			{id: 15, name: 'さつき'},
			{id: 16, name: 'ソフィー'},
			{id: 17, name: 'マーニー'},
			{id: 18, name: '菜穂子'},
			{id: 19, name: 'サン'},
			{id: 20, name: 'フィオ'}
		];
		return {heroines};
	}
}

データを返すメソッド(createDb())は、HTTPのリクエストがあると呼び出されます。戻り値はデータの配列をプロパティにもったオブジェクトです(「Angular in-memory-web-api」の「Basic setup」参照)。プロパティ名(heroines)がデータを取り出すときのキーになります。ECMAScript 6では、オブジェクトリテラルのプロパティに変数(定数)を与えると、値は省いて構いません(「オブジェクト初期化子」の「プロパティの定義」)。前述のメソッドの戻り値はつぎのとおりです。


{heroines: [
	{id: 11, name: 'シータ'},
	// ...[中略]...
	{id: 20, name: 'フィオ'}
]}

HttpClientクラスを使うには、つぎのようにアプリケーションのモジュール(app.module)のTypeScriptコードにHttpClientModuleクラスimportして、デコレータ関数NgModule()の引数オブジェクトのimportsに配列要素として与えます。また、Angular in-memory-web-apiモジュールを用いるため、HttpClientInMemoryWebApiModuleとin-memory-data.serviceもimportしたうえで、NgModule()関数の引数のimportsHttpClientInMemoryWebApiModule.forRoot()メソッドの呼び出しを配列に加えてください。引数はサービスのクラスとオプションオブジェクトです。

src/app/app.module.ts

import {HttpClientModule} from '@angular/common/http';  // *document bug
import {HttpClientInMemoryWebApiModule} from 'angular-in-memory-web-api';
import {InMemoryDataService}  from './in-memory-data.service';

@NgModule({

	imports: [

		HttpClientModule,
		// HttpClientInMemoryWebApiModuleはHTTPリクエストとレスポンスをシミュレート
		// 実際にサーバーを使うときには削除
		HttpClientInMemoryWebApiModule.forRoot(
			InMemoryDataService, {dataEncapsulation: false}
		)
	  ],

})
export class AppModule {}

データサービス(in-memory-data.service)とアプリケーション(app.module)のモジュールはこれででき上がりですので、つぎのコード001にTypeScriptコードをまとめておきます。

コード001■データサービスとアプリケーションのモジュールのTypeScriptコード

src/app/in-memory-data.service.ts

import {InMemoryDbService} from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
	createDb() {
		const heroines = [
			{id: 11, name: 'シータ'},
			{id: 12, name: 'ナウシカ'},
			{id: 13, name: 'キキ'},
			{id: 14, name: '千尋'},
			{id: 15, name: 'さつき'},
			{id: 16, name: 'ソフィー'},
			{id: 17, name: 'マーニー'},
			{id: 18, name: '菜穂子'},
			{id: 19, name: 'サン'},
			{id: 20, name: 'フィオ'}
		];
		return {heroines};
	}
}

src/app/app.module.ts

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {HttpClientModule} from '@angular/common/http';
import {HttpClientInMemoryWebApiModule} from 'angular-in-memory-web-api';
import {InMemoryDataService} from './in-memory-data.service';
import {AppComponent} from './app.component';
import {HeroinesComponent} from './heroines/heroines.component';
import {HeroineDetailComponent} from './heroine-detail/heroine-detail.component';
import {HeroineService} from './heroine.service';
import {MessagesComponent} from './messages/messages.component';
import {MessageService} from './message.service';
import {AppRoutingModule} from './app-routing.module';
import {DashboardComponent} from './dashboard/dashboard.component';
@NgModule({
	declarations: [
		AppComponent,
		HeroinesComponent,
		HeroineDetailComponent,
		MessagesComponent,
		DashboardComponent
	],
	imports: [
		BrowserModule,
		FormsModule,
		AppRoutingModule,
		HttpClientModule,
		HttpClientInMemoryWebApiModule.forRoot(
			InMemoryDataService, {dataEncapsulation: false}
		)
	],
	providers: [HeroineService, MessageService],
	bootstrap: [AppComponent]
})
export class AppModule {}

HTTPの通信をするためには、クラスHttpClientHttpHeadersimportします。データを得るために用いるのは、HttpClient.get()メソッドです(HttpClientクラスはのちほど使います)。データを受け取るふたつのメソッド(getHeroines()とgetHeroine())のof()オペレータの呼び出しを、つぎのようにHttpClient.get()メソッドで書き替えます。URLのプロパティ(heroinesUrl)でパスに含めたのが、InMemoryDataService.createDb()メソッドの戻り値に与えたキー(heroines)です。HttpClient.get()メソッドの引数にはURLを渡します。戻り値はObservableオブジェクトなので、ほかは手を加えずに済むのです。

src/app/heroine.service.ts

import {HttpClient, HttpHeaders} from '@angular/common/http';

// import {HEROINES} from './mock-heroines';

export class HeroineService {
	private heroinesUrl = 'api/heroines';
	constructor(
		private http: HttpClient,

		) {}
	getHeroines(): Observable<Heroine[]> {

		// return of(HEROINES);
		return this.http.get<Heroine[]>(this.heroinesUrl);
	}
	getHeroine(id: number): Observable<Heroine> {
		const url = `${this.heroinesUrl}/${id}`;

		// return of(HEROINES.find(heroine => heroine.id === id));
		return this.http.get<Heroine>(url);
	}
}

これで、HTTPによりデータが得られるようになりました。けれど、詳細情報画面でデータを書き替えても、ダッシュボードやリスト表示に戻るとデータはもとのまま変わりません。これは、HTTPでデータを更新していないためです。

02 メッセージを加えるメソッドとエラーの扱い

HTTPでデータを更新する前に、サービスモジュール(heroine.service)のクラス(HeroineService)は少し整えましょう。まず、メッセージを加えるメソッド(log())はつぎのように新たに書き起こし、HTTPでやり取りするメソッドから呼び出すようにします。

src/app/heroine.service.ts

export class HeroineService {

	getHeroines(): Observable<Heroine[]> {
		// this.messageService.add('HeroineService: データを取得');
		this.log('データを取得');

	}
	getHeroine(id: number): Observable<Heroine> {

		// this.messageService.add(`HeroineService: 番号${id}のデータを取得`);
		this.log(`番号${id}のデータを取得`);

	}
	private log(message: string) {
		this.messageService.add('HeroineService: ' + message);
	}
}

つぎに、モジュール(heroine.service)にエラーの扱いを加えます。オペレータcatchError()map()およびtap()importしておきましょう。Observableオブジェクトに順に処理を加えるために用いるのがObservable.pipe()メソッドです。catchError()に渡すのは関数で、エラーが起きたとき引数にエラーを受け取って呼び出されます。コールバックは、つぎのようにメソッド(handleError())から返すようにしました。コールバックがObservableオブジェクトを返せば、それがあとの処理に使われ、アプリケーションは動き続けられるのです。

src/app/heroine.service.ts

import {catchError, map, tap} from 'rxjs/operators';

export class HeroineService {

	getHeroines(): Observable<Heroine[]> {

		return this.http.get<Heroine[]>(this.heroinesUrl)  // ;
		.pipe(
			catchError(this.handleError('getHeroines', []))
		);
	}

	private handleError<T> (operation = 'operation', result?: T) {
		return (error: any): Observable<T> => {
			console.error(error);
			this.log(`${operation} failed: ${error.message}`);
			return of(result as T);
		};
	}

}

エラーの扱いが働くかどうか確かめるには、たとえばつぎのようにHttpClient.get()メソッドに渡すURLを正しくないものに書き替えればよいでしょう。

src/app/heroine.service.ts

export class HeroineService {

	getHeroines(): Observable<Heroine[]> {

		return this.http.get<Heroine[]>(this.heroinesUrl + 'ng')  // URLに文字列を加える

	}

}

ブラウザのコンソールにはつぎのようにエラーの情報が示され、アプリケーションのメッセージにもエラーが加えられます(図001)。

{body: {…}, url: "api/heroinesng", headers: HttpHeaders, status: 404, statusText: "Not Found"}
	body: {error: "Collection 'heroinesng' not found"}
	headers: HttpHeaders {normalizedNames: Map(0), lazyUpdate: null, lazyInit: ƒ}
	status: 404
	statusText: "Not Found"
	url: "api/heroinesng"
	__proto__: Object

図001■メッセージにエラーが加えられる

図001

03 データが得られたあとの処理を加える

前項では、データが得られたあとにエラーの処理を加えました。メッセージを加えるのも、データが取り出されてからにしましょう。Observable.pipe()メソッドには、オペレータによる処理を順にカンマ区切りで引数に与えられます。tap()オペレータは引数の処理を行ったあと、参照したObservableオブジェクトがそのまま戻り値となります。つまり、もとのオブジェクトが変わらないということです。そこでつぎのように、メッセージを加えるメソッド(log())の呼び出しは、このオペレータの引数に移して、Observable.pipe()メソッドに加えます。

src/app/heroine.service.ts

export class HeroineService {

	getHeroines(): Observable<Heroine[]> {
		// this.log('データを取得');
		return this.http.get<Heroine[]>(this.heroinesUrl)
		.pipe(
			tap(heroines => this.log('データを取得')),  // 追加
			catchError(this.handleError('getHeroines', []))
		);
	}
	getHeroine(id: number): Observable<Heroine> {

		// this.log(`番号${id}のデータを取得`);
		return this.http.get<Heroine>(url)  // ;
		.pipe(  // 追加
			tap(_ => this.log(`番号${id}のデータを取得`)),
			catchError(this.handleError<Heroine>(`getHero 番号=${id}`))
		);
	}

}

引数の番号から該当する1件のデータを取り出すメソッド(getHeroine())にも、エラーの扱いが加わりました。これを試すには、たとえばつぎのように引数の番号から1差し引くとよいでしょう。すると、選んだ番号のひとつ前のデータが詳細情報に示されます。先頭の項目をクリックすれば、その番号のデータはないので、エラーが示されるはずです。

src/app/heroine.service.ts

export class HeroineService {

	getHeroine(id: number): Observable<Heroine> {
		const url = `${this.heroinesUrl}/${id - 1}`;  // 数値を1引く

	}
}

04 書き替えたデータを更新する

ここで、詳細情報の画面で書き替えたデータを、HTTPサービスで更新できるようにしましょう。用いるのはHttpClient.put()メソッドです。第1引数(url)はHttpClient.get()と同じくURL、第2引数(body)には更新するデータを定めます。第3引数(options)のオプションとして渡すのはヘッダ情報です。

put(url: string, body: any | null, options: {})

サービスモジュール(heroine.service)のTypeScriptコードで、ヘッダ情報はつぎのようにHttpHeadersオブジェクトでつくって定数(httpOptions)に定めます。新たに加えるデータ更新のメソッド(updateHeroine())からHttpClient.put()メソッドを呼び出したあと、pipe()オペレータにつなぐのはデータ取得のメソッドと同じです。どのデータが書き替えられるかは、idプロパティの値で決まります。Angular in-memory-web-apiでは、データが一意のプロパティidをもつことになっているからです(「Basic setup」のNotes参照)。

src/app/heroine.service.ts

const httpOptions = {
	headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};

export class HeroineService {

	updateHeroine(heroine: Heroine): Observable<any> {
		return this.http.put(this.heroinesUrl, heroine, httpOptions)
		.pipe(
			tap(_ => this.log(`番号${heroine.id}のデータを変更`)),
			catchError(this.handleError<any>('updateHeroine'))
		);
	}

}

詳細情報コンポーネント(heroine-detail.component)のテンプレートには「保存」ボタンをつぎのように加えましょう(図002)。クリック(clickイベント)で呼び出すハンドラ(save())はクラス(HeroineDetailComponent)にメソッドとして以下のように定め、サービス(heroineService)のデータ更新メソッド(updateHeroine())を呼び出します。引数に渡すのは、書き替えたデータ(heroine)です。

src/app/heroine-detail/heroine-detail.component.html

<div *ngIf="heroine">

	<button (click)="save()">保存</button>
</div>

src/app/heroine-detail/heroine-detail.component.ts

export class HeroineDetailComponent implements OnInit {

	save(): void {
		this.heroineService.updateHeroine(this.heroine)
		.subscribe(() => this.goBack());
	}
}

図002■詳細情報コンポーネントに「保存」ボタンが加わった

図002

これで詳細情報の画面で書き替えて保存したデータは、ダッシュボードやリスト表示に戻っても更新が反映されます(図003)。でき上がったサービス(heroine.service)のTypeScriptモジュールと詳細情報コンポーネント(heroine-detail.component)のテンプレートおよびTypeScriptコードは、以下のコード002にまとめたとおりです。ファイルごとのコードにつきましては、Plunker「Angular 5 Example - Tour of Heroines 08」をご覧ください。

図003■詳細情報コンポーネントで入力したデータが書き替わる

図003

コード002■でき上がったサービスのTypeScriptモジュールと詳細情報コンポーネントのテンプレートおよびTypeScriptコード

src/app/heroine.service.ts

import {Injectable} from '@angular/core';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {Observable} from 'rxjs/Observable';
import {of} from 'rxjs/observable/of';
import {catchError, map, tap} from 'rxjs/operators';
import {Heroine} from './heroine';
import {MessageService} from './message.service';
const httpOptions = {
	headers: new HttpHeaders({'Content-Type': 'application/json'})
};
@Injectable()
export class HeroineService {
	private heroinesUrl = 'api/heroines';
	constructor(
		private http: HttpClient,
		private messageService: MessageService) {}
	getHeroines(): Observable<Heroine[]> {
		return this.http.get<Heroine[]>(this.heroinesUrl)
		.pipe(
			tap(heroines => this.log('データを取得')),
			catchError(this.handleError('getHeroines', []))
		);
	}
	getHeroine(id: number): Observable<Heroine> {
		const url = `${this.heroinesUrl}/${id}`;
		return this.http.get<Heroine>(url)
		.pipe(
			tap(_ => this.log(`番号${id}のデータを取得`)),
			catchError(this.handleError<Heroine>(`getHeroine 番号=${id}`))
		);
	}
	updateHeroine(heroine: Heroine): Observable<any> {
		return this.http.put(this.heroinesUrl, heroine, httpOptions)
		.pipe(
			tap(_ => this.log(`番号${heroine.id}のデータを変更`)),
			catchError(this.handleError<any>('updateHeroine'))
		);
	}
	private handleError<T> (operation = 'operation', result?: T) {
		return (error: any): Observable<T> => {
			console.error(error);
			this.log(`${operation} failed: ${error.message}`);
			return of(result as T);
		};
	}
	private log(message: string) {
		this.messageService.add('HeroineService: ' + message);
	}
}

src/app/heroine-detail/heroine-detail.component.html

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

src/app/heroine-detail/heroine-detail.component.ts

import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {Location} from '@angular/common';
import {Heroine} from '../heroine';
import {HeroineService} from '../heroine.service';
@Component({
	selector: 'app-heroine-detail',
	templateUrl: './heroine-detail.component.html',
	styleUrls: ['./heroine-detail.component.css']
})
export class HeroineDetailComponent implements OnInit {
	heroine:Heroine;
	constructor(
		private route:ActivatedRoute,
		private heroineService:HeroineService,
		private location:Location
	) {}
	ngOnInit(): void {
		this.getHeroine();
	}
	getHeroine(): void {
		const id = +this.route.snapshot.paramMap.get('id');
		this.heroineService.getHeroine(id)
		.subscribe(heroine => this.heroine = heroine);
	}
	goBack(): void {
		this.location.back();
	}
	save(): void {
		this.heroineService.updateHeroine(this.heroine)
		.subscribe(() => this.goBack());
	}
}


作成者: 野中文雄
作成日: 2018年2月6日


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