サイトトップ

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

HTML5テクニカルノート

Angular 2入門 08: HTTPサービスが返すObservableを使ったデータの検索


Angular 2入門 07: HTTPサービスでデータを追加・削除する」(以下「Angular 2入門 07」)でつくったサンプル「Angular 2 Example - Tour of Heroines: Part 7」は、HTTPサービスによりリモートにデータを追加し、削除もできるようにしました。さらに、データを検索する機能も加えましょう。そのとき、HTTPサービスが返すObservableオブジェクトを使います。

01 Observableを使う

Httpクラスのサービスメソッドにリクエストを送ると、ResponseオブジェクトのObservableが返されます。Responseは名前のとおり、レスポンスを受け取るときに使うクラスです。 「Angular 2入門 06」や「Angular 2入門 07」でHTTPサービスのメソッドをコンポーネント(HeroineService)に備えたときは、ObservableObservable.toPromise()メソッドによりPromiseオブジェクトに換えました(「Angular 2入門 06」01「HTTPサービスでデータを読み込む」)。今回加えるサービスでは、Observableのまま扱います。

Promiseが扱えるのは、1度にひとつの非同期処理だけです。解決されたらすぐに実行され、取り消すこともできません。Observableは配列のようなストリームに処理がいくつでも加えられ、実行を遅らせることもできます。そのため、つぎのような利点があります(「Angular2のHttpモジュールを眺めてベストプラクティスを考える」「Promise v.s. Observable」参照)。

Angularに備わっているのは、Observableの基本的な扱いだけです。そこで、RxJSライブラリの力を借りることになります。

02 HTTPサービスでデータを新たに加える

サーバーに問い合わせた検索語から当てはまるデータを受け取る処理は、新たにサービス(heroine-search.service)として定めます(「Angular2のHttpモジュールを眺めてベストプラクティスを考える」の「サービスとして分離する」参照)。検索のメソッド(search())は、Http.get()メソッドでクエリー文字列を渡し、受け取ったResponseオブジェクトのデータからRxJSのObservable.map()メソッドでJSONの値として取り出します(Observable.map()メソッドについては後掲表001参照)。

heroine-search.service.ts

export class HeroineSearchService {
	constructor(private http: Http) {}
	search(term: string): Observable<Heroine[]> {
		return this.http
			.get(`app/heroines/?name=${term}`)
			.map((response: Response) => response.json().data as Heroine[]);
	}
}

データを扱うサービスのモジュール(heroine.service)に定めた、ID番号から該当データが得られるメソッド(getHeroine())もつぎのように基本的な流れは同じでした(「Angular 2入門 07」コード002)。ただ、http.get() メソッドにはクエリー文字列でなくURLを渡し、1回の非同期処理で済むためtoPromise()メソッドでPromiseオブジェクトに換えたことが異なります。

heroine.service.ts

export class HeroineService {

	constructor(private http: Http) {}

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

}

サーバーに渡した検索語からデータを受け取るサービス(heroine-search.service)のスクリプト全体は、つぎのコード001にまとめました。クラスObservablemap()メソッドは、RxJSからimportしています。

コード001■HTTPサービスでサーバーに渡した検索語から当てはまるデータを受け取る

heroine-search.service.ts

import {Injectable} from '@angular/core';
import {Http, Response} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import {Heroine} from './heroine';
@Injectable()
export class HeroineSearchService {
	constructor(private http: Http) {}
	search(term: string): Observable<Heroine[]> {
		return this.http
			.get(`app/heroines/?name=${term}`)
			.map((response: Response) => response.json().data as Heroine[]);
	}
}

03 検索のコンポーネントのテンプレートとCSSファイル

つぎに、検索のコンポーネント(heroine-search.component)をつくります。HTMLテンプレートとCSSのファイルを先に定めましょう。ふたつの中身は、以下のコード002にまとめたとおりです。

HTMLテンプレート(heroine-search.component.html)の<input>要素には、テンプレート参照変数#(searchBox)を定めました。テンプレートの中で要素がこの変数で参照できるのです(「Angular 2のローカル変数とexportAs」の「ローカル変数と#シンタックス」および「USER INPUT」の「Get user input from a template reference variable」)。keyupイベントでハンドラとして呼び出す検索のメソッド(search())に、valueプロパティから取り出した入力値を渡します(メソッドは次項でコンポーネントに定めます)。

heroine-search.component.html

<div id="search-component">

	<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />

</div>

コンポーネント(heroine-search.component)の検索のメソッド(search())が呼び出されると、その結果によりプロパティ(heroines)のデータが改められます。そこで、*ngForディレクティブにより項目の数だけ要素を書き替えます。ただし、HTTPサービスから受け取るデータは、そのままではObservableなので扱えません。プロパティに| async(AsyncPipe)を添えると値が取り出せますので、要素がつくれるのです。

clickイベントのハンドラに定めるメソッド(gotoDetail())は、次項で定めます。リスト表示のコンポーネント(heroines.component)に加えた同名のメソッド(gotoDetail())と同じく、選んだ項目の詳細情報の画面を示します。

heroine-search.component.html

<div *ngFor="let heroine of heroines | async"
	(click)="gotoDetail(heroine)" class="search-result" >
	{{heroine.name}}
</div>

コード002■検索のコンポーネントのテンプレートとCSS

heroine-search.component.html

<div id="search-component">
	<h4>ヒロイン検索</h4>
	<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
	<div>
		<div *ngFor="let heroine of heroines | async"
			(click)="gotoDetail(heroine)" class="search-result" >
			{{heroine.name}}
		</div>
	</div>
</div>

heroine-search.component.css

.search-result {
	border-bottom: 1px solid gray;
	border-left: 1px solid gray;
	border-right: 1px solid gray;
	width: 195px;
	height: 16px;
	padding: 5px;
	background-color: white;
	cursor: pointer;
}
.search-result:hover {
	color: #eee;
	background-color: #607D8B;
}
#search-box {
	width: 200px;
	height: 20px;
}

04 検索のコンポーネントのクラスを定める

SubjectはRxJSのクラスで、Observableのひとつです(「Understanding Subjects in RxJS」の「What are Subjects?」参照)。検索のクラス(HeroineSearchComponent)のプロパティ(searchTerms)に文字列のObservableを納め、検索のメソッド(search())が呼び出されるたびに、next()メソッドで引数の検索文字列をストリームに加えます。

heroine-search.component.ts

export class HeroineSearchComponent implements OnInit {

	private searchTerms = new Subject<string>();

	// 検索語をObservableのストリームに加える
	search(term: string): void {
		this.searchTerms.next(term);
	}

}

検索のクラス(HeroineSearchComponent)はOnInitクラスを実装しましたので、ngOnInit()メソッドを定めて初期化します(「Angular 2入門 04: サービスをつくる」02「コンポーネントのモジュールを書き替える」参照)。以下のようにプロパティ(searchTerms)のSubjectオブジェクトにObservableのメソッドを適用します。

Observable.debounceTime()は実行を待って、最新の処理のみ行います(「Angular2でdebounceを設定する」参照)。Observable.distinctUntilChanged()は、処理が同じなら繰り返しません。そして、Observable.switchMap()が検索語のObservableを更新して、結果を返します。検索語がないときには空のObservableが返ります。Observable.catch()はエラーの扱いです。コードに用いたObservableのメソッドは以下の表001にまとめました(使わなかった引数は省いています)。

heroine-search.component.ts

export class HeroineSearchComponent implements OnInit {
	heroines: Observable<Heroine[]>;
	private searchTerms = new Subject<string>();

	ngOnInit(): void {
		this.heroines = this.searchTerms
		.debounceTime(300)  // キー入力から300ms待って検索語を探す
		.distinctUntilChanged()  // 検索語が前と同じなら無視する
		.switchMap((term: string) => term  // 検索語が変わるたびに新たなObservableに切り替える
			// http検索のObservableを返す
			? this.heroineSearchService.search(term)
			// 検索語がなければ空のObservable
			: Observable.of<Heroine[]>([]))
		.catch(error => {
			// 実際のエラー処理を加える
			console.log(error);
			return Observable.of<Heroine[]>([]);
		});
	}

}

表001■Observableクラスのメソッド

メソッド 構文と説明
map()
public map(project: function(value: T): R): Observable<R>
値に引数projectの関数を適用して、戻り値が取り出されるObservableオブジェクトにする。
debounceTime()
public debounceTime(dueTime: number): Observable
Observableオブジェクトからの値の取り出しを、引数dueTimeの指定ミリ秒待つ。値が複数回渡された場合は直近の値のみ受け取る。
distinctUntilChanged()
public distinctUntilChanged(): Observable
複数渡された項目のうち、前と異なった項目のみ受け取る。
switchMap()
public switchMap(project: function(value: T, ?index: number): ObservableInput): Observable
値を引数projectの関数が適用されたObservableインスタンスにして、参照したObservableオブジェクトに含める。値は中のObservableインスタンスから取り出され、新たなオブジェクトが含められると破棄される。
catch()
public catch(selector: function): Observable
Observableオブジェクトが正しく処理できなかったときに、引数selectorの関数を呼び出す。
of()
public static of(values: ...T): Observable<T>
新たなObservableオブジェクトをつくり、引数valuesに渡した値がただちに取り出されて処理を終える。

前述のとおり、Observableについて、Angularに備わっているのは基本的な扱いだけです。そこで、検索のコンポーネント(heroine-search.component)には、RxJSライブラリからふたつのクラスObservableSubjectimportし、ほかにも拡張とオペーレータを加えました。検索のコンポーネントのクラスの定めは、以下のコード003のとおりです。

heroine-search.component.ts

import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
// Observableクラス拡張
import 'rxjs/add/observable/of';
// Observableオペーレータ
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

コード003■検索のコンポーネントのクラスの定め

heroine-search.component.ts

import {Component, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import {HeroineSearchService} from './heroine-search.service';
import {Heroine} from './heroine';
@Component({
	moduleId: module.id,  // [*1]
	selector: 'heroine-search',
	templateUrl: './heroine-search.component.html',
	styleUrls: ['./heroine-search.component.css'],
	providers: [HeroineSearchService]
})
export class HeroineSearchComponent implements OnInit {
	heroines: Observable<Heroine[]>;
	private searchTerms = new Subject<string>();
	constructor(
		private heroineSearchService: HeroineSearchService,
		private router: Router) {}
	search(term: string): void {
		this.searchTerms.next(term);
	}
	ngOnInit(): void {
		this.heroines = this.searchTerms
		.debounceTime(300)
		.distinctUntilChanged()
		.switchMap((term: string) => term
			? this.heroineSearchService.search(term)
			: Observable.of<Heroine[]>([]))
		.catch(error => {
			console.log(error);
			return Observable.of<Heroine[]>([]);
		});
	}
	gotoDetail(heroine: Heroine): void {
		let link = ['/detail', heroine.id];
		this.router.navigate(link);
	}
}

[*1] moduleIdは、CommonJSを用いるときに与えなければなりません(「Angular2 beta relative paths for templateUrl and styleUrls」)。angular-cliは内部でwebpackを使うため、省く必要があるようです(「angular-cliでangular2 TOUR OF HEROESチュートリアルでつまずいた点」)。

05 検索のコンポーネントをアプリケーションに組み込む

検索のコンポーネント(heroine-search.component)をアプリケーションに組み込みましょう。テンプレート(selector名heroine-search)はダッシュボード(dashboard.component)の下側に加えます。

dashboard.component.html

<div class="grid grid-pad">

</div>
<heroine-search></heroine-search>

アプリケーションのモジュール(app.module)では、@NgModuleデコレータのdeclarationsに検索のコンポーネントのクラス(HeroineSearchComponent)を含めます。これで、テキスト入力フィールドの文字列が検索され、結果はリストで示されるようになりました(図001)。リストから項目をクリックすると、詳細情報の画面に移ります。書き替えたダッシュボードのテンプレートとアプリケーションモジュールは、以下のコード004にまとめまたとおりです。併せて、Plunkerに「Angular 2 Example - Tour of Heroines: Part 8」として、サンプルコードをアップロードしました。実際の動きや、すべてのモジュールおよびテンプレート、CSSのコードは、こちらでお確かめください。

app.module.ts

@NgModule({

	declarations: [

		HeroineSearchComponent
	],

})

図001■テキスト入力フィールドの文字列から検索結果が示される

図001

コード004■ダッシュボードのテンプレートとアプリケーションモジュール

dashboard.component.html

<h3>トップヒロイン</h3>
<div class="grid grid-pad">
	<a *ngFor="let heroine of heroines" [routerLink]="['/detail', heroine.id]" class="col-1-4">
		<div class="module heroine">
			<h4>{{heroine.name}}</h4>
		</div>
	</a>
</div>
<heroine-search></heroine-search>

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';
import {HeroineSearchComponent} from './heroine-search.component';
@NgModule({
	imports: [
		BrowserModule,
		FormsModule,
		HttpModule,
		InMemoryWebApiModule.forRoot(InMemoryDataService),
		AppRoutingModule
	],
	declarations: [
		AppComponent,
		DashboardComponent,
		HeroineDetailComponent,
		HeroinesComponent,
		HeroineSearchComponent
	],
	providers: [HeroineService],
	bootstrap: [AppComponent]
})
export class AppModule {}


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


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