HTML5テクニカルノート
Angular 5入門 09: HTTPサービスでリモートのデータを加えたり除いたりする
- ID: FN1802002
- Technique: HTML5 / JavaScript
- Package: Angular 5.2.3
「Angular 5入門 08: HTTPサービスでリモートのデータを取り出して書き替えられる」は、HTTPサービスによりデータをリモートから取り出し、書き替えるようにしました。さらに、データを新たに加えたり、すでにあるデータも除けるようにしましょう。
01 HTTPサービスでデータを加える
まずは、HTTPサービスで新たなデータを加える処理です。用いるのはHttpClient.post()
メソッドで、HttpClient.put()
と同じく、第1引数(url)がURL、第2引数(body)は加えるデータ、第3引数(options)のオプションにはヘッダ情報を渡します(「Angular 5入門 08」04「書き替えたデータを更新する」参照)。
post(url: string, body: any | null, options: {})
データを加えるメソッド(addHeroine())は、サービスのモジュール(heroine.service)につぎのように加えます。引数(heroine)はHeroineクラスで型づけされたオブジェクトです。HttpClient.post()
を使うほかは、データを更新するメソッド(updateHeroine())と組み立てはほぼ変わりません。ただ、データのidプロパティはサーバー側で振りますので、引数オブジェクトはnameプロパティしかもたず、解決したオブジェクト(tap()
オペレータのコールバックが受け取る引数heroine)からid値を取り出しています。
src/app/heroine.service.tsexport class HeroineService { addHeroine(heroine: Heroine): Observable<Heroine> { return this.http.post<Heroine>(this.heroinesUrl, heroine, httpOptions) .pipe( tap((heroine: Heroine) => this.log(`番号${heroine.id}にデータを追加`)), catchError(this.handleError<Heroine>('addHeroine')) ); } }
データはリスト表示のコンポーネント(heroines.component)から加えることにして、クラス(HeroinesComponent)につぎのようにメソッド(add())を定めます。引数(name)はデータとして加える文字列です。String.trim()
メソッドで前後の空白は除いて、空でないことを確かめたうえで、サービスのクラス(HeroineService)に加えた前掲メソッド(addHeroine)を呼び出します。渡すのは文字列をプロパティ(name)に納めたオブジェクトです。そして、リストに表示するため、クラスのプロパティ(heroines)にもデータを加えなければなりません。なお、as
構文はそのあとに添えた型(Heroine)でデータを評価し直します。
src/app/heroines/heroines.component.tsexport class HeroinesComponent implements OnInit { add(name: string): void { name = name.trim(); if (!name) {return;} this.heroineService.addHeroine({name} as Heroine) .subscribe(heroine => this.heroines.push(heroine) ); } }
リスト表示コンポーネント(heroines.component)のテンプレートには、つぎのようにテキスト入力フィールド(<input>
要素)とボタン(<button>
要素)を加えます。ボタンクリック(click
イベント)でデータ追加のメソッド(add())を呼び出し、引数には入力フィールドのテキストを渡せばよいでしょう。要素に属性のかたちでハッシュ(#
)を添えた識別子(heroineName)は、テンプレートで参照する変数です。ボタンをクリックしたら、入力フィールドのテキスト(value
プロパティ)は空にします。
src/app/heroines/heroines.component.html<div> <label>名前: <input #heroineName /> </label> <button (click)="add(heroineName.value); heroineName.value=''"> 追加 </button> </div>
これで、リスト表示の画面にテキスト入力フィールドが備わりました(図001)。テキストを打ち込んだら、「追加」ボタンでリストに項目が加えられます。項目の番号は、前述のとおりデータサーバーが振る連番です。
図001■テキストフィールドに入力したデータが「追加」ボタンで加えられる
02 HTTPサービスで項目のデータを除く
つぎは、サーバーのデータから項目を除く処理です。サービスモジュール(heroine.service)のクラス(HeroineService)に、つぎのようにメソッド(deleteHeroine())を加えます。引数(heroine)は削除するデータのオブジェクトです。HttpClient.delete()
メソッドに、ふたつの引数、URLとオプションのオブジェクトを渡して呼び出します。URL(url)には引数のプロパティ(id)から得た番号を添えて定めています。Observable.pipe()
メソッドにつなげた処理は、クラスの他のメソッドと基本的に同じです。
src/app/heroine.service.tsexport class HeroineService { deleteHeroine(heroine: Heroine): Observable<Heroine> { const id = heroine.id; const url = `${this.heroinesUrl}/${id}`; return this.http.delete<Heroine>(url, httpOptions) .pipe( tap(_ => this.log(`番号${id}のデータを削除`)), catchError(this.handleError<Heroine>('deleteHeroine')) ); } }
データを除くのもリスト表示コンポーネント(heroines.component)の仕事です。テンプレートにつぎのように、項目ごとに削除のボタン(<button>
要素)を加えます。クリック(click
イベント)で呼び出すメソッド(delete())は、このあとコンポーネントのクラスに定めます。引数はその項目データのオブジェクト(heroine)です。CSSに以下の指定を書き加えれば、削除ボタンのスタイルが整います(図002)。
src/app/heroines/heroines.component.htmlsrc/app/heroines/heroines.component.css<ul class="heroines"> <li *ngFor="let heroine of heroines"> <button class="delete" title="データを削除" (click)="delete(heroine)">x</button> </li> </ul>
button:hover { background-color: #cfd8dc; } button.delete { position: relative; left: 194px; top: -32px; background-color: gray !important; color: white; }
図002■リスト表示コンポーネントの項目に削除ボタンが加わった
リスト表示コンポーネント(heroines.component)のクラス(HeroinesComponent)に加えるのがつぎの削除のメソッド(delete())です。引数(heroine)には削除するデータのオブジェクトを受け取ります。まず、クラス(HeroinesComponent)のデータのプロパティ(heroines)からArray.filter()
メソッドでその項目を除き、リストから消します。つぎに呼び出すのが、サービス(heroineService)に加えた前述のメソッド(deleteHeroine())です。データのオブジェクトを引数に渡します。そのうえで、戻り値のObservable
オブジェクトに対して、Observable.subscribe()
メソッドを引数(コールバック)なしで呼び出してください。この呼び出しがないと処理は実行されず、サーバーのデータは削除されません。
src/app/heroines/heroines.component.tsexport class HeroinesComponent implements OnInit { delete(heroine: Heroine): void { this.heroines = this.heroines.filter(_heroine => _heroine !== heroine); this.heroineService.deleteHeroine(heroine).subscribe(); } }
これで、HTTPサービスによりサーバーに対して、リスト表示の画面からデータを加えたり、リスト項目のデータを除いたりできるようになりました。書き替えたリスト表示コンポーネント(heroines.component)のTypeScriptコードとテンプレートおよびCSSファイルの中身はつぎのコード001のとおりです。
コード001■リスト表示コンポーネントのTypeScriptコードとテンプレートおよびCSSファイル
src/app/heroines/heroines.component.ts
import {Component, OnInit} from '@angular/core';
import {Heroine} from '../heroine';
import {HeroineService} from '../heroine.service';
@Component({
selector: 'app-heroines',
templateUrl: './heroines.component.html',
styleUrls: ['./heroines.component.css']
})
export class HeroinesComponent implements OnInit {
heroines: Heroine[];
constructor(private heroineService: HeroineService) {}
ngOnInit() {
this.getHeroines();
}
getHeroines(): void {
this.heroineService.getHeroines()
.subscribe(heroines => this.heroines = heroines);
}
add(name: string): void {
name = name.trim();
if (!name) {return;}
this.heroineService.addHeroine({name} as Heroine)
.subscribe(heroine =>
this.heroines.push(heroine)
);
}
delete(heroine: Heroine): void {
this.heroines = this.heroines.filter(_heroine => _heroine !== heroine);
this.heroineService.deleteHeroine(heroine).subscribe();
}
}
<h2>ヒロインリスト</h2>
<div>
<label>名前:
<input #heroineName />
</label>
<button (click)="add(heroineName.value); heroineName.value=''">
追加
</button>
</div>
<ul class="heroines">
<li *ngFor="let heroine of heroines">
<a routerLink="/detail/{{heroine.id}}">
<span class="badge">{{heroine.id}}</span> {{heroine.name}}
</a>
<button class="delete" title="データを削除"
(click)="delete(heroine)">x</button>
</li>
</ul>
.heroines {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroines li {
position: relative;
cursor: pointer;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroines li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroines a {
color: #888;
text-decoration: none;
position: relative;
display: block;
width: 250px;
}
.heroines a:hover {
color:#607D8B;
}
.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;
min-width: 16px;
text-align: right;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
button:hover {
background-color: #cfd8dc;
}
button.delete {
position: relative;
left: 194px;
top: -32px;
background-color: gray !important;
color: white;
}
03 コードを改善する
TypeScriptコードにあとふたつ手直しを加えましょう。この「Angular 5入門」が下じきにしている公式Tutorial「HTTP」のコードでは、サービスモジュール(heroine.service)のデータを削除するメソッド(deleteHeroine())は、ひとつの引数(heroine)にふたつの型づけが与えられています。つぎのように型を縦棒(|
)で結ぶと、それらのいずれかの型(union型)という定めになります。データのオブジェクト(Heroine)のほかに、数値(number
)が受け入れられ、項目番号を渡してもデータが除けるということです。今回書いたコードでは、番号を受け取ることはありません。
src/app/heroine.service.tsexport class HeroineService { // deleteHeroine(heroine: Heroine): Observable<Heroine> { deleteHeroine (heroine: Heroine | number): Observable<Heroine> { // const id = heroine.id; const id = typeof heroine === 'number' ? heroine : heroine.id; const url = `${this.heroinesUrl}/${id}`; } }
もうひとつは、不具合の改善です。リスト表示の画面で項目をすべて削除してから新たに追加すると、コンソールにはつぎのようなエラーが出て、入力したテキストが加わりません(図003)。データの配列を空にすると、番号(id)が振られず、そのあとの処理は正しくできないようです。
error: "Collection 'heroines' id type is non-numeric or unknown. Can only generate numeric ids."
図003■入力したテキストがリストに加わらない
そこで、リスト表示のコンポーネント(heroines.component)は、データ追加のメソッド(add())からサービス(heroineService)のメソッド(addHeroine())を呼び出すとき、引数にデータ配列の長さ(Array.length
プロパティ)も渡すことにします。
src/app/heroines/heroines.component.tsexport class HeroinesComponent implements OnInit { add(name: string): void { // this.heroineService.addHeroine({name} as Heroine) this.heroineService.addHeroine({name} as Heroine, this.heroines.length) } }
サービスのコンポーネント(heroine.service)はこれに応じて、データ削除のメソッド(addHeroine())が第2引数(numHeroines)にデータ配列の長さ0を受け取ったときは、つぎのようにオブジェクト(heroine)のidプロパティに初期値11を与えたうえで、HttpClient.put()
メソッドにより番号とともにデータを与えるようにします。これで、リスト項目をすべて削除しても、新たなテキストの項目が加えられるでしょう。
src/app/heroine.service.tsexport class HeroineService { // addHeroine(heroine: Heroine): Observable<Heroine> { addHeroine(heroine: Heroine, numHeroines: number): Observable<Heroine> { let heroineOvserbable; // return this.http.post<Heroine>(this.heroinesUrl, heroine, httpOptions) if (numHeroines === 0) { heroine.id = 11; heroineOvserbable = this.http.put(this.heroinesUrl, heroine, httpOptions) } else { heroineOvserbable = this.http.post<Heroine>(this.heroinesUrl, heroine, httpOptions); } return heroineOvserbable } }
書き上がったサービスモジュール(heroine.service)のTypeScriptコードは、つぎのコード002にまとめます。リスト表示コンポーネント(heroines.component)のTypeScriptコードは1行直しただけですので、改めて全体は示しません。それぞれのファイルのコードについては、Plunker「Angular 5 Example - Tour of Heroines 09」をご覧ください。
コード002■サービスモジュールの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}`))
);
}
addHeroine(heroine: Heroine, numHeroines: number): Observable<Heroine> {
let heroineOvserbable;
if (numHeroines === 0) {
heroine.id = 11;
heroineOvserbable = this.http.put(this.heroinesUrl, heroine, httpOptions)
} else {
heroineOvserbable = this.http.post<Heroine>(this.heroinesUrl, heroine, httpOptions);
}
return heroineOvserbable
.pipe(
tap((heroine: Heroine) => this.log(`番号${heroine.id}にデータを追加`)),
catchError(this.handleError<Heroine>('addHeroine'))
);
}
deleteHeroine (heroine: Heroine | number): Observable<Heroine> {
const id = typeof heroine === 'number' ? heroine : heroine.id;
const url = `${this.heroinesUrl}/${id}`;
return this.http.delete<Heroine>(url, httpOptions)
.pipe(
tap(_ => this.log(`番号${id}のデータを削除`)),
catchError(this.handleError<Heroine>('deleteHeroine'))
);
}
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);
}
}
04【Advanced】Angular in-memory-web-apiモジュールのメソッドをオーバーライドする
前項03で確かめた、データの配列を空にしたとき番号(id)が振られないのは、Angular in-memory-web-apiモジュールの仕様によるようです。抽象クラスBackendService
のデフォルト番号を与えるBackendService.genIdDefault()
メソッドはBackendService.isCollectionIdNumeric()
を呼び出し、今あるデータが数値のidプロパティをもつときのみ最後の値に1加えた番号を返します(前掲のエラーはここで生じました)。
新たな番号を振るのはBackendService.genId()
メソッドで、InMemoryDbService
を実装するInMemoryDataServiceクラス(in-memory-data.serviceモジュール)からオーバーライドできます。定めるメソッドはつぎのとおりです(「Tutorial example has a problem to enter new item into an empty list」参照)。引数のデータ配列の長さ(Array.length
プロパティ)が0、つまり空のときは初期値(11)を返します。空でなければ、Array.map()
メソッドでデータのid属性を配列に取り出して、その最大値 + 1がメソッドの戻り値です。ECMAScript 2015(ECMAScript 6)のスプレッド演算子...
は、配列をカンマ区切りの引数に変えてMath.max()
メソッドに渡します。
src/app/in-memory-data.service.tsexport class InMemoryDataService implements InMemoryDbService { genId(heroines: Heroine[]): number { return heroines.length > 0 ? Math.max(...heroines.map(heroine => heroine.id)) + 1 : 11; } }
そうすると、リスト表示コンポーネント(heroines.component)とサービスモジュール(heroine.service)のTypeScriptコードはもとに戻してしまえます。書き直したコードの全体は改めて掲げません。Plunker「Angular 5 Example - Tour of Heroines 09-2」でそれぞれのファイルのコードをお確かめください。
src/app/heroines/heroines.component.tssrc/app/heroine.service.tsexport class HeroinesComponent implements OnInit { add(name: string): void { // this.heroineService.addHeroine({name} as Heroine, this.heroines.length) this.heroineService.addHeroine({name} as Heroine) } }
export class HeroineService { // addHeroine(heroine: Heroine, numHeroines: number): Observable<Heroine> { addHeroine(heroine: Heroine): Observable<Heroine> { /* let heroineOvserbable; if (numHeroines === 0) { heroine.id = 11; heroineOvserbable = this.http.put(this.heroinesUrl, heroine, httpOptions) } else { heroineOvserbable = this.http.post<Heroine>(this.heroinesUrl, heroine, httpOptions); } return heroineOvserbable */ return this.http.post<Heroine>(this.heroinesUrl, heroine, httpOptions) } }
- Angular 5: Angular CLIで手早くアプリケーションをつくる
- Angular 5入門 01: アプリケーションの枠組みをつくる
- Angular 5入門 02: 編集ページをつくる
- Angular 5入門 03: データのリストを表示する
- Angular 5入門 04: 詳細情報のコンポーネントを分ける
- Angular 5入門 05: データをサービスにより提供する
- Angular 5入門 06: ルーティングで画面を切り替える
- Angular 5入門 07: ルーティングで個別情報を示す
- Angular 5入門 08: HTTPサービスでリモートのデータを取り出して書き替える
- Angular 5入門 10: HTTPサービスでリモートのデータを検索する
作成者: 野中文雄
更新日: 2018年2月11日 04「【Advanced】Angular in-memory-web-apiモジュールのメソッドをオーバーライドする」を追加。
作成日: 2018年2月9日
Copyright © 2001-2018 Fumio Nonaka. All rights reserved.