サイトトップ

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

HTML5テクニカルノート

Angular 4入門 08: HTTPサービスでデータを取得・保存する


Angular 4入門 07: Routerで画面を遷移させる」(以下「Angular 4入門 07」)でつくったサンプル「Angular 4 Example - Tour of Heroines: Part 7」は、データをファイル(mock-heroines.ts)から読み込み、オブジェクトのデータを得て、書き替えたりしていました。このデータをHTTPサービスにより、リモートで扱えるようにします。

Angular Version 5については「Angular 5入門 08: HTTPサービスでリモートのデータを取り出して書き替える」をお読みください。

01 HTTPサービスでデータを読み込む

HTTPサービスで使うHttpModuleは、アドオンの@angular/httpモジュールからimportします。コアのモジュールには含まれていないのでご注意ください。もっとも、「Angular 4入門 01」(コード001「アプリケーションを動かすHTMLドキュメント」参照)から用いているsystemjs.config.jsには、つぎのように予めこのモジュールが加えてあります。ですから、今回はとくに手は加えなくて構いません。

systemjs.config.web.js

(function (global) {
  System.config({

    map: {

      // angular bundles

      '@angular/http': 'npm:@angular/http/bundles/http.umd.js',

    },

  });
})(this);

では、アプリケーションのモジュール(app.module)に、つぎのようにHttpModuleクラスimportしましょう。そして、デコレータ関数NgModule()に渡すオブジェクトのimportsプロパティに配列エレメントとして加えます。

app.module.ts


import {HttpModule} from '@angular/http';

@NgModule({
	imports: [

		HttpModule,

	],

})
export class AppModule {}

web APIは、練習用としてメモリで動くシミュレーションのモジュールangular-in-memory-web-apiを使うことにします(このモジュールもsystemjs.config.jsに含まれています)。データをもつだけのモジュール(mock-heroines)は要らなくなりますので、つぎのようにサービスのモジュール(in-memory-data.service)に書き替えます。importしたInMemoryDbServiceをクラス(InMemoryDataService)に実装したら、メソッドcreateDb()を定めなければなりません(「Basic usage」参照)。このメソッドは、データのオブジェクトを配列に入れて返します。

mock-heroines.ts→in-memory-data.service.ts

import {InMemoryDbService} from 'angular-in-memory-web-api';
import {Heroine} from './heroine';
export class InMemoryDataService implements InMemoryDbService {
	createDb() {
		let heroines: Heroine[] = [
			{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};
	}
}

アプリケーションのモジュール(app.module)には、つぎのように上に定めたInMemoryWebApiModuleクラスをimportします。このクラスは、デフォルトのHTTPクライアントのXMLHttpRequestバックエンドサービスを、インメモリのweb API拡張サービスが組み込まれた拡張サーバに置き替えます。そして、デコレータ関数NgModule()に渡すオブジェクトのimportsプロパティで、InMemoryWebApiModule.forRoot()メソッドにより前掲データサービスのオブジェクト(InMemoryDataService)を加えるのです。

app.module.ts


import {InMemoryWebApiModule} from 'angular-in-memory-web-api';
import {InMemoryDataService} from './in-memory-data.service';

@NgModule({
	imports: [

		InMemoryWebApiModule.forRoot(InMemoryDataService),

	],

})
export class AppModule {}

データを提供するサービスのモジュール(heroine.service)は、HTTPサービスからデータが得られるように書き替えましょう。web APIのモジュールからは、以下のようにHeadersHttpおよびResponseの3つのクラスをimportします(Headersクラスはあとで使います)。データのオブジェクトが納められた配列は、メソッド(getHeroines())からPromiseオブジェクトで返す仕組みにしてありました(「Angular 4入門 04: サービスをつくる」03「データを非同期で受け取れるようにする」参照)。すでに、非同期処理の用意は整っているということです。

コンストラクタでHttpオブジェクトをプロパティ(http)に受け取っておき、メソッド(getHeroines())の呼び出しによりデータが求められたとき、Http.get()メソッドにURL(heroinesUrl)を渡します。戻り値はObservableですので、Observable.toPromise()メソッドによりPromiseオブジェクトに換えます。このメソッドは、rxjsからimportしました。コールバック関数にResponseオブジェクトが渡されたらjson()メソッドでJSONデータを取り出し、dataプロパティからデータの配列が得られます(このプロパティはインメモリweb APIの場合です)。なお、Promise.catch()メソッドで、エラーが起きたときに扱う関数(handleError())を定めました。

heroine.service.ts


import {Headers, Http, Response} from '@angular/http';
import 'rxjs/add/operator/toPromise';

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

export class HeroineService {
	private heroinesUrl: string = 'api/heroines';
	constructor(private http: Http) {}
	getHeroines(): Promise<Heroine[]> {
		// return Promise.resolve(HEROINES);
		return this.http.get(this.heroinesUrl)
			.toPromise()
			.then((response: Response) => response.json().data as Heroine[])
			.catch(this.handleError);
	}

	private handleError(error: any): Promise<any> {
		console.error('An error occured', error);
		return Promise.reject(error.message || error);
	}
}

これで、HttpModuleクラスを用いてHTTPサービスによりデータが得られるようになりました。手を加えたモジュールは、つぎのコード001にまとめたとおりです。

コード001■HttpModuleクラスによりHTTPサービスでデータを得る

app.module.ts

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';
import {InMemoryWebApiModule} from 'angular-in-memory-web-api';
import {InMemoryDataService} from './in-memory-data.service';
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,
		HttpModule,
		InMemoryWebApiModule.forRoot(InMemoryDataService),
		AppRoutingModule
	],
	declarations: [
		AppComponent,
		DashboardComponent,
		HeroineDetailComponent,
		HeroinesComponent
	],
	providers: [HeroineService],
	bootstrap: [AppComponent]
})
export class AppModule {}

heroine.service.ts

import {Injectable} from '@angular/core';
import {Headers, Http, Response} from '@angular/http';
import 'rxjs/add/operator/toPromise';
import {Heroine} from './heroine';
@Injectable()
export class HeroineService {
	private heroinesUrl: string = 'api/heroines';
	constructor(private http: Http) {}
	getHeroines(): Promise<Heroine[]> {
		return this.http.get(this.heroinesUrl)
			.toPromise()
			.then((response: Response) => response.json().data as Heroine[])
			.catch(this.handleError);
	}
	getHeroine(id: number): Promise<Heroine> {
		return this.getHeroines()
		.then((heroines: Heroine[]) => heroines.find((heroine: Heroine) => heroine.id === id));
	}
	private handleError(error: any): Promise<any> {
		console.error('An error occured', error);
		return Promise.reject(error.message || error);
	}
}

in-memory-data.service.ts

import {InMemoryDbService} from 'angular-in-memory-web-api';
import {Heroine} from './heroine';
export class InMemoryDataService implements InMemoryDbService {
	createDb() {
		let heroines: Heroine[] = [
			{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};
	}
}

02 idに当てはまるデータをHTTPサービスから得る

データを提供するサービスのモジュール(heroine.service)には、もうひとつidに当てはまるデータを返すメソッド(getHeroine())が備わっています。とくに動きに問題はありません。けれど、すべてのデータをサーバーから受け取って、idで探すというのは無駄です。web APIを使えば、求めるデータだけをもらうことができます。

つぎのように、前述のデータすべてを受け取るメソッド(getHeroines())と同じ組み立てで、引数のidを定めたURLからデータが得られるのです。なお、URLには文字列内挿機能(${})を使っているので、バックティック(`)によるテンプレート文字列を用いることにお気をつけください。サービスのメソッドの中身はいろいろと書き替えたものの、それらを呼び出す側は何も直さずに済みました。サービスのモジュールを分ける意味がここにあります。

heroine.service.ts


export class HeroineService {

	getHeroine(id: number): Promise<Heroine> {
		const url: string = `${this.heroinesUrl}/${id}`;
		/* return this.getHeroines()
		.then((heroines: Heroine[]) => heroines.find((heroine: Heroine) => heroine.id === id)); */
		return this.http.get(url)
			.toPromise()
			.then((response: Response) => response.json().data as Heroine)
			.catch(this.handleError);
	}

}

03 HTTPサービスでデータを保存する

前掲コード001を試していると、ひとつ問題に気づくでしょう。詳細情報の画面でデータを編集すると、その中ではテキストが書き替わります。ところが、ダッシュボードやリスト表示の画面に移ったとき、データがもとに戻ってしまいます。これは、コンポーネントの中でオブジェクトの値を変えただけで、リモートのデータはそのままになっているからです。書き替えたデータは、保存できる仕組みにしなければなりません。そこで、詳細情報画面のテンプレートに、つぎのように保存のボタン(<button>要素)を加えます。

heroine-detail.component.html

<div *ngIf="heroine">

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

保存ボタンのクリック(clickイベント)に定めたコールバックのメソッド(save())は詳細情報のコンポーネント(heroine-detail.component)につぎのように加え、データを提供するサービス(heroineServiceプロパティ)のメソッド(update())を呼び出します。渡すのは保存するデータ(heroineプロパティ)です。

heroine-detail.component.ts


export class HeroineDetailComponent implements OnInit {

	save(): void {
		this.heroineService.update(this.heroine)
			.then(() => this.goBack());
	}
}

データを提供するサービスのモジュール(heroine.service)に加える保存のメソッド(update())はつぎのとおりです。データを返すメソッドと組み立てはほぼ変わりません。Http.get()に替えてHttp.put()メソッドを呼び出し、データはJSON.stringify()でJSONの文字列にして渡します。リクエストヘッダとして与えたHeadersオブジェクトも、それに合わせて'Content-Type'を'application/json'と定めました。これで、詳細情報の画面で書き替えたデータを保存すると、ダッシュボードやリスト表示のデータも値が改まります(図001)。アプリケーションの動きは「Angular 4入門 07」と変わらないものの、HTTPサービスによりリモートのデータを扱う仕組みになりました。

heroine.service.ts


export class HeroineService {
	private headers: Headers = new Headers({'Content-Type': 'application/json'});

	update(heroine: Heroine): Promise<Heroine> {
		const url: string = `${this.heroinesUrl}/${heroine.id}`;
		return this.http
			.put(url, JSON.stringify(heroine), {headers: this.headers})
			.toPromise()
			.then(() => heroine)
			.catch(this.handleError);
	}

}

図001■詳細情報を編集して保存するとデータが書き替わる

図001

手直ししたモジュールとテンプレートは、つぎのコード002のとおりです。併せて、Plunkerに「Angular 4 Example - Tour of Heroines: Part 8」として、サンプルコードをアップロードしました。実際の動きや、すべてのモジュールおよびテンプレート、CSSのコードは、こちらでお確かめください。

コード002■HTTPサービスによりリモートのデータを取得・保存する

heroine.service.ts

import {Injectable} from '@angular/core';
import {Headers, Http, Response} from '@angular/http';
import 'rxjs/add/operator/toPromise';
import {Heroine} from './heroine';
@Injectable()
export class HeroineService {
	private headers: Headers = new Headers({'Content-Type': 'application/json'});
	private heroinesUrl: string = 'api/heroines';
	constructor(private http: Http) {}
	getHeroines(): Promise<Heroine[]> {
		return this.http.get(this.heroinesUrl)
			.toPromise()
			.then((response: Response) => response.json().data as Heroine[])
			.catch(this.handleError);
	}
	getHeroine(id: number): Promise<Heroine> {
		const url: string = `${this.heroinesUrl}/${id}`;
		return this.http.get(url)
			.toPromise()
			.then((response: Response) => response.json().data as Heroine)
			.catch(this.handleError);
	}
	update(heroine: Heroine): Promise<Heroine> {
		const url: string = `${this.heroinesUrl}/${heroine.id}`;
		return this.http
			.put(url, JSON.stringify(heroine), {headers: this.headers})
			.toPromise()
			.then(() => heroine)
			.catch(this.handleError);
	}
	private handleError(error: any): Promise<any> {
		console.error('An error occured', error);
		return Promise.reject(error.message || error);
	}
}

heroine-detail.component.ts

import 'rxjs/add/operator/switchMap';
import {Component, 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({
	selector: 'heroine-detail',
	templateUrl: './heroine-detail.component.html',
	styleUrls: ['./heroine-detail.component.css']
})
export class HeroineDetailComponent implements OnInit {
	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();
	}
	save(): void {
		this.heroineService.update(this.heroine)
			.then(() => this.goBack());
	}
}

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>
	<button (click)="save()">保存</button>
</div>


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


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