HTML5テクニカルノート
Angular 2入門 06: HTTPサービスでデータを取得・保存する
- ID: FN1701007
- Technique: HTML5 / JavaScript
- Package: Angular 2.1
「Angular 2入門 05: サービスをつくる」(以下「Angular 2入門 05」)でつくったサンプル「Angular 2 Example - Tour of Heroines: Part 5」は、データをファイル(mock-heroines.ts)から読み込み、オブジェクトのデータを得て、書き替えたりしていました。このデータをHTTPサービスにより、リモートで扱えるようにします。
01 HTTPサービスでデータを読み込む
HTTPサービスで使うHttpModule
は、アドオンの@angular/httpモジュールからimport
します。コアのモジュールには含まれていないのでご注意ください。もっとも、「Angular 2入門 01」(02「HTMLドキュメントを書き替える」参照)から用いているsystemjs.config.web.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.tsimport {HttpModule} from '@angular/http'; @NgModule({ imports: [ HttpModule, ], }) export class AppModule {}
web APIは、練習用としてメモリで動くシミュレーションのモジュールangular-in-memory-web-apiを使うことにします。データをもつだけのモジュール(mock-heroines)は要らなくなりますので、つぎのようにサービスのモジュール(in-memory-data.service)に書き替えます。import
したInMemoryDbService
をクラス(InMemoryDataService)に実装したら、メソッドcreateDb()
を定めなければなりません(「Basic usage」参照)。このメソッドは、データのオブジェクトを配列で返します。
mock-heroines.ts→in-memory-data.service.tsimport {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}; } }
アプリケーションのモジュール()には、つぎのようにInMemoryWebApiModule
クラスをimport
します。このクラスは、デフォルトのHTTPクライアントのXMLHttpRequestバックエンドサービスを、インメモリのweb API拡張サービスが組み込まれた拡張サーバに置き替えます。そして、デコレータ関数@NgModule
に渡すオブジェクトのimports
プロパティで、InMemoryWebApiModule.forRoot()
メソッドにより前掲データサービスのオブジェクト(InMemoryDataService)を加えるのです。
app.module.tsimport {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のモジュールからは、以下のようにHeaders
とHttp
およびResponse
の3つのクラスをimport
します(Headers
クラスは後で使います)。データのオブジェクトが納められた配列は、メソッド(getHeroines())からPromise
オブジェクトで返す仕組みにしてありました(「Angular 2入門 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.tsimport {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 {}
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);
}
}
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.tsexport 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())は詳細情報のコンポーネントにつぎのように加え、データを提供するサービス(heroineServiceプロパティ)のメソッド(update())を呼び出します。渡すのは保存するデータ(heroineプロパティ)です。
heroine-detail.component.tsexport class HeroineDetailComponent implements OnInit { save(): void { this.heroineService.update(this.heroine) .then(() => this.goBack()); } }
データを提供するサービスのモジュール(heroine.service.ts)に加える保存のメソッド(update())はつぎのとおりです。データを返すメソッドと組み立てはほぼ変わりません。Http.get()
に替えてHttp.put()
メソッドを呼び出し、データはJSON.stringify()
でJSONの文字列にして渡します。リクエストヘッダとして与えたHeaders
オブジェクトも、それに合わせて'Content-Type'を'application/json'と定めました。これで、詳細情報の画面で書き替えたデータを保存すると、ダッシュボードやリスト表示のデータも値が改まります(図001)。アプリケーションの動きは「Angular 2入門 05」と変わらないものの、HTTPサービスによりリモートのデータを扱う仕組みになりました。
heroine.service.tsexport 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■詳細情報を編集して保存するとデータが書き替わる
手直ししたモジュールとテンプレートは、つぎのコード002のとおりです。併せて、Plunkerに「Angular 2 Example - Tour of Heroines: Part 6」として、サンプルコードをアップロードしました。実際の動きや、すべてのモジュールおよびテンプレート、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);
}
}
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',
styleUrls: ['heroine-detail.component.css']
})
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();
}
save(): void {
this.heroineService.update(this.heroine)
.then(() => this.goBack());
}
}
<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>
- Angular 2: とにかくAngular 2でコードを書いて動かす
- Angular 2入門 01: 編集ページをつくる
- Angular 2入門 02: リストを加える
- Angular 2入門 03: コンポーネントを分ける
- Angular 2入門 04: サービスをつくる
- Angular 2入門 05: Routerを使う
- Angular 2入門 07: HTTPサービスでデータを追加・削除する
- Angular 2入門 08: HTTPサービスが返すObservableを使ったデータの検索
作成者: 野中文雄
作成日: 2017年1月19日
Copyright © 2001-2017 Fumio Nonaka. All rights reserved.