HTML5テクニカルノート
Angular 2入門 08: HTTPサービスが返すObservableを使ったデータの検索
- ID: FN1704002
- Technique: HTML5 / JavaScript
- Package: Angular 2.2
「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)に備えたときは、Observable
はObservable.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.tsexport 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.tsexport 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にまとめました。クラスObservable
とmap()
メソッドは、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>
.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.tsexport 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.tsexport 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()
|
値に引数projectの関数を適用して、戻り値が取り出されるObservable オブジェクトにする。 |
debounceTime()
|
Observable オブジェクトからの値の取り出しを、引数dueTimeの指定ミリ秒待つ。値が複数回渡された場合は直近の値のみ受け取る。
|
distinctUntilChanged()
|
複数渡された項目のうち、前と異なった項目のみ受け取る。
|
switchMap()
|
値を引数projectの関数が適用されたObservable インスタンスにして、参照したObservable オブジェクトに含める。値は中のObservable インスタンスから取り出され、新たなオブジェクトが含められると破棄される。 |
catch()
|
Observable オブジェクトが正しく処理できなかったときに、引数selectorの関数を呼び出す。
|
of()
|
新たなObservable オブジェクトをつくり、引数valuesに渡した値がただちに取り出されて処理を終える。
|
前述のとおり、Observable
について、Angularに備わっているのは基本的な扱いだけです。そこで、検索のコンポーネント(heroine-search.component)には、RxJSライブラリからふたつのクラスObservable
とSubject
をimport
し、ほかにも拡張とオペーレータを加えました。検索のコンポーネントのクラスの定めは、以下のコード003のとおりです。
heroine-search.component.tsimport {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■テキスト入力フィールドの文字列から検索結果が示される
コード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>
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 {}
- Angular 2: とにかくAngular 2でコードを書いて動かす
- Angular 2入門 01: 編集ページをつくる
- Angular 2入門 02: リストを加える
- Angular 2入門 03: コンポーネントを分ける
- Angular 2入門 04: サービスをつくる
- Angular 2入門 05: Routerを使う
- Angular 2入門 06: HTTPサービスでデータを取得・保存する
- Angular 2入門 07: HTTPサービスでデータを追加・削除する
作成者: 野中文雄
作成日: 2017年4月7日
Copyright © 2001-2017 Fumio Nonaka. All rights reserved.