サイトトップ

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

HTML5テクニカルノート

Angular 5入門 09: HTTPサービスでリモートのデータを加えたり除いたりする


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.ts

export 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.ts

export 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■テキストフィールドに入力したデータが「追加」ボタンで加えられる

図001

02 HTTPサービスで項目のデータを除く

つぎは、サーバーのデータから項目を除く処理です。サービスモジュール(heroine.service)のクラス(HeroineService)に、つぎのようにメソッド(deleteHeroine())を加えます。引数(heroine)は削除するデータのオブジェクトです。HttpClient.delete()メソッドに、ふたつの引数、URLとオプションのオブジェクトを渡して呼び出します。URL(url)には引数のプロパティ(id)から得た番号を添えて定めています。Observable.pipe()メソッドにつなげた処理は、クラスの他のメソッドと基本的に同じです。

src/app/heroine.service.ts

export 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.html

<ul class="heroines">
	<li *ngFor="let heroine of heroines">

		<button class="delete" title="データを削除"
		(click)="delete(heroine)">x</button>
	</li>
</ul>

src/app/heroines/heroines.component.css

button:hover {
	background-color: #cfd8dc;
}  
button.delete {
	position: relative;
	left: 194px;
	top: -32px;
	background-color: gray !important;
	color: white;
}

図002■リスト表示コンポーネントの項目に削除ボタンが加わった

図002

リスト表示コンポーネント(heroines.component)のクラス(HeroinesComponent)に加えるのがつぎの削除のメソッド(delete())です。引数(heroine)には削除するデータのオブジェクトを受け取ります。まず、クラス(HeroinesComponent)のデータのプロパティ(heroines)からArray.filter()メソッドでその項目を除き、リストから消します。つぎに呼び出すのが、サービス(heroineService)に加えた前述のメソッド(deleteHeroine())です。データのオブジェクトを引数に渡します。そのうえで、戻り値のObservableオブジェクトに対して、Observable.subscribe()メソッドを引数(コールバック)なしで呼び出してください。この呼び出しがないと処理は実行されず、サーバーのデータは削除されません。

src/app/heroines/heroines.component.ts

export 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();
	}
}

src/app/heroines/heroines.component.html

<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>

src/app/heroines/heroines.component.css

.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.ts

export 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■入力したテキストがリストに加わらない

図003

そこで、リスト表示のコンポーネント(heroines.component)は、データ追加のメソッド(add())からサービス(heroineService)のメソッド(addHeroine())を呼び出すとき、引数にデータ配列の長さ(Array.lengthプロパティ)も渡すことにします。

src/app/heroines/heroines.component.ts

export 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.ts

export 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.ts

export 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.ts

export class HeroinesComponent implements OnInit {

	add(name: string): void {

		// this.heroineService.addHeroine({name} as Heroine, this.heroines.length)
		this.heroineService.addHeroine({name} as Heroine)

	}

}
src/app/heroine.service.ts

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)

	}

}


作成者: 野中文雄
更新日: 2018年2月11日 04「【Advanced】Angular in-memory-web-apiモジュールのメソッドをオーバーライドする」を追加。
作成日: 2018年2月9日


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