HTML5テクニカルノート
Angular 6入門 08: HTTPサービスでリモートのデータを取り出して書き替える
- ID: FN1805012
- Technique: HTML5 / JavaScript
- Package: Angular 6.0.0
「Angular 6入門 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)には、InMemoryDbService
をimport
してインタフェースとして実装(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()
関数の引数のimports
にHttpClientInMemoryWebApiModule.forRoot()
メソッドの呼び出しを配列に加えてください。引数はサービスのクラスとオプションオブジェクトです。
src/app/app.module.tsimport {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};
}
}
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の通信をするために、サービスのモジュール(heroine.service)にはクラスHttpClient
とHttpHeaders
をimport
します(HttpHeaders
クラスはのちほど使います)。コンストラクタはHttpClient
インスタンスを引数に受け取りますので、private
なプロパティにもっておきます。
そして、データを得るために用いるのは、HttpClient.get()
メソッドです。データを受け取るふたつのメソッド(getHeroines()とgetHeroine())のof()
オペレータの呼び出しを、つぎのようにHttpClient.get()
メソッドで書き替えます。URLのプロパティ(heroinesUrl)でパスに含めたのが、InMemoryDataService.createDb()メソッドの戻り値に与えたキー(heroines)です。データひとつを取り出すメソッド(getHeroine())のパス(url)には、さらにそのidが加わります。HttpClient.get()
メソッドの引数にそれぞれのパスを渡します。戻り値はObservable
オブジェクトのままなので、このサービスを使うほかのモジュールには手を加えずに済むのです。
src/app/heroine.service.tsimport {HttpClient, HttpHeaders} from '@angular/common/http'; // import {HEROINES} from './mock-heroines'; export 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); } }
なお、HttpClient.get()
メソッドから返されたObservable
オブジェクトは、HTTPリクエストのボディに解決されます。そのままでは素のJSONデータですので、上記コードでは適切に型づけ(Heroine[]またはHeroine)しました。
これで、HTTPによりデータが得られるようになります。けれど、詳細情報画面でデータを書き替えても、ダッシュボードやリスト表示に戻るとデータはもとのまま変わりません。これは、HTTPでデータを更新していないためです。
02 メッセージを加えるメソッドとエラーの扱い
HTTPでデータを更新する前に、サービスモジュール(heroine.service)のクラス(HeroineService)は少し整えましょう。まず、メッセージを加えるメソッド(log())はつぎのように別に書き起こし、HTTPでやり取りするメソッド(getHeroines()とgetHeroine())から呼び出すようにします。
src/app/heroine.service.tsexport 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.tsimport {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.tsexport 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■メッセージにエラーが加えられる
03 データが得られたあとの処理を加える
前項では、データが得られたあとにエラーの処理を加えました。メッセージを加えるのも、データが取り出されてからにしましょう。Observable.pipe()
メソッドには、オペレータによる処理を順にカンマ区切りで引数に与えられます。tap()
オペレータは引数の処理を行ったあと、参照したObservable
と同じオブジェクトが戻り値となります。つまり、もとのオブジェクトと中身が変わらないということです。そこでつぎのように、メッセージを加えるメソッド(log())の呼び出しは、このオペレータの引数に移して、Observable.pipe()
メソッドに加えます。
src/app/heroine.service.tsexport 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>(`getHeroine 番号=${id}`)) ); } }
引数の番号から該当する1件のデータを取り出すメソッド(getHeroine())にも、エラーの扱いが加わりました。これを試すには、たとえばつぎのように引数の番号から1差し引くとよいでしょう。すると、選んだ番号のひとつ前のデータが詳細情報に示されます。先頭の項目をクリックすれば、その前の番号のデータはないので、エラーが示されるはずです。
src/app/heroine.service.tsexport 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.tsconst 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.tsexport class HeroineDetailComponent implements OnInit { save(): void { this.heroineService.updateHeroine(this.heroine) .subscribe(() => this.goBack()); } }
図002■詳細情報コンポーネントに「保存」ボタンが加わった
これで詳細情報の画面で書き替えて保存したデータは、ダッシュボードやリスト表示に戻っても更新が反映されます(図003)。でき上がったサービス(heroine.service)のTypeScriptモジュールと詳細情報コンポーネント(heroine-detail.component)のテンプレートおよびTypeScriptコードは、以下のコード002にまとめたとおりです。ファイルごとのコードにつきましては、StackBlitz「angular-6-example-tour-of-heroines-08」をご覧ください。
図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);
}
}
<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>
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());
}
}
- Angular 6: Angular CLIで手早くアプリケーションをつくる
- Angular 6入門 01: アプリケーションの枠組みをつくる
- Angular 6入門 02: 編集ページをつくる
- Angular 6入門 03: データのリストを表示する
- Angular 6入門 04: 詳細情報のコンポーネントを分ける
- Angular 6入門 05: データをサービスにより提供する
- Angular 6入門 06: ルーティングで画面を切り替える
- Angular 6入門 07: ルーティングで個別情報を示す
作成者: 野中文雄
作成日: 2018年5月23日
Copyright © 2001-2018 Fumio Nonaka. All rights reserved.