サイトトップ

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

HTML5テクニカルノート

Angular 5入門 10: HTTPサービスでリモートのデータを検索する


Angular 5入門 08: HTTPサービスでリモートのデータを取り出して書き替えられる」および「Angular 5入門 09: HTTPサービスでリモートのデータを加えたり除いたりする」で、HTTPサービスによりリモートのデータを編集したり、追加・削除もできるようにしました。さらに、フィールドに入力したテキストから、データが検索できるようにコンポーネントを加えます。「Angular 5入門」シリーズは今回が最後です。

01 サービスに検索のメソッドを加える

まず、サービスモジュール(heroine.service)にデータ検索のメソッド(searchHeroines())をつぎのように加えます。引数(term)は検索語の文字列です。テキストが空白でないことを確かめたうえで、HttpClient.get()メソッドに引数としてURL(url)を渡します。この処理の組み立ては、データを取り出すメソッド(getHeroines())とよく似ています(後掲コード001参照)。異なるのは、URLにクエリ文字列を加えていることです。この文字列をもとにリモートデータが検索されます。なお、検索語が空白だった場合の戻り値は、空のObservableオブジェクトです。

src/app/heroine.service.ts

export class HeroineService {

	searchHeroines(term: string): Observable<Heroine[]> {
		if (!term.trim()) {
			return of([]);
		}
		const url = `${this.heroinesUrl}/?name=${term}`;
		return this.http.get<Heroine[]>(url)
		.pipe(
			tap(_ => this.log(`「${term}」のデータを検索`)),
			catchError(this.handleError<Heroine[]>('searchHeroines', []))
		);
	}

}

サービスモジュール(heroine.service)のTypeScriptコード(HeroineServiceクラス)はこれででき上がりですので、つぎのコード001にまとめます。

コード001■サービスモジュールの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}`))
		);
	}
	searchHeroines(term: string): Observable<Heroine[]> {
		if (!term.trim()) {
			return of([]);
		}
		const url = `${this.heroinesUrl}/?name=${term}`;
		return this.http.get<Heroine[]>(url)
		.pipe(
			tap(_ => this.log(`「${term}」のデータを検索`)),
			catchError(this.handleError<Heroine[]>('searchHeroines', []))
		);
	}
	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);
	}
}

02 検索のコンポーネントをつくる

つぎに、ng generate componentコマンドで、アプリケーションのディレクトリ(angular-tour-of-heroines)につぎのように検索のコンポーネントをつくります。

ngコマンド

ng generate component heroine-search

検索コンポーネント(heroine-search.component)のTypeScptコードから定めます。このクラス(HeroineSearchComponent)は、書いている途中で動きを試して確かめるのはむずかしいので、コード002にでき上がりを示してしまいましょう。アプリケーションのモジュール(app.module)にはngコマンドで検索コンポーネントが加えられました。こちらもこれ以上手は加えませんので併せて掲げます。

コード002■検索のコンポーネントとアプリケーションモジュールのTypeScriptコード

src/app/heroine-search/heroine-search.component.ts

import {Component, OnInit} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {of} from 'rxjs/observable/of';
import {
	debounceTime, distinctUntilChanged, switchMap
} from 'rxjs/operators'; 
import {Heroine} from '../heroine';
import {HeroineService} from '../heroine.service';
@Component({
	selector: 'app-heroine-search',
	templateUrl: './heroine-search.component.html',
	styleUrls: ['./heroine-search.component.css']
})
export class HeroineSearchComponent implements OnInit {
	heroines$: Observable<Heroine[]>;
	private searchTerms = new Subject<string>();
	constructor(private heroineService: HeroineService) {}
	search(term: string): void {
		this.searchTerms.next(term);
	}
	ngOnInit(): void {
		this.heroines$ = this.searchTerms
		.pipe(
			debounceTime(300),
			distinctUntilChanged(),
			switchMap((term: string) => this.heroineService.searchHeroines(term)),
		);
	}
}

src/app/app.module.ts

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

検索のコンポーネント(heroine-search.component)のTypeScriptコードは、つぎのようにObservableのほかにSubjectクラスimportしました。SubjectObservableのサブクラスです。そのインスタンスをprivateのプロパティ(searchTerms)に与えたうえで、コンポーネントがつくられたとき(ngOnInit()メソッド)Observableで型づけされたプロパティ(heroines$)に納めています(識別子の終わりの$はObservableを表すために添えます)。SubjectクラスがObservableと違うのは、データをあとから加えられることです(「AngularのRxJSを使ってデータの受け渡しをする」の「任意のタイミングでデータを流すSubject」参照)。すると、ユーザーがテキストフィールドに検索語を入力したとき、サービスに渡して検索することができるのです。

src/app/heroine-search/heroine-search.component.ts

import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';

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

	ngOnInit(): void {
		this.heroines$ = this.searchTerms

	}
}

SubjectクラスはObservableを継承するとともに、Observerインタフェースが実装されています(「RxJS: Closed Subjects」の「Subject」参照)。Observer.next()はオブジェクトにあとからデータを加えるメソッドです。ユーザーが検索語を入力したときに呼び出すメソッド(search()は、引数(term)に渡されたその文字列をObserver.next()でオブジェクトに差し込みます。そのデータを処理するためにRxJSから3つのメソッド(オペレータ)をimportしました。ngOnInit()メソッドでSubjectインスタンス(searchTerms)をObservableのプロパティ(heroines$)に納めたあと、Observable.pipe()メソッドで3つつづけて呼び出しています。

検索のメソッド(search()は、テキストフィールドでキー入力されるたびに呼び出されることになります。それをそのままデータサーバーに送ったら、負荷が膨らみ過ぎてしまうでしょう。入力されたデータの出力を、引数のミリ秒待たせるのがdebounceTime()です。指定時間が過ぎたとき、直近の値だけをObservableオブジェクトで返します。同じ検索語をつづけざまに問い合わせるのも無駄です。distinctUntilChanged()は、同じ項目がつづいたときはそのデータを省いて出力する新たなObservableオブジェクトにします。そのうえで、出力された検索語(term)をサービスのメソッド(searchHeroines())に送るのが、switchMap()です。

src/app/heroine-search/heroine-search.component.ts

import {
	debounceTime, distinctUntilChanged, switchMap
} from 'rxjs/operators'; 

import {HeroineService} from '../heroine.service';

export class HeroineSearchComponent implements OnInit {

	constructor(private heroineService: HeroineService) {}
	search(term: string): void {
		this.searchTerms.next(term);
	}
	ngOnInit(): void {
		this.heroines$ = this.searchTerms
		.pipe(
			debounceTime(300),
			distinctUntilChanged(),
			switchMap((term: string) => this.heroineService.searchHeroines(term)),
		);
	}
}

debounceTime()が負荷を下げ、distinctUntilChanged()で無駄を減らしました。それでも、検索結果が返る前につぎの検索語を送ってしまうことはありえます。それでも、switchMap()は新たなObservableを送るとき、前のオブジェクトは破棄してくれるのです。ただし、HTTPリクエストそのものは取り消されません。Observableクラスのメソッド(オペーレータ)とオブジェクトの扱いに用いられる関数を、前にご説明したものも含めてつぎの表001にまとめます。

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

メソッド 構文と説明
debounceTime()
public debounceTime(dueTime: number, scheduler: Scheduler): Observable
[インスタンスメソッド] 参照するObservableオブジェクトから、引数の時間が過ぎたのちに値をひとつ出力する。複数の値が渡された場合は、直近の値のみ出力される。
distinctUntilChanged()
public distinctUntilChanged(compare: function): Observable
[インスタンスメソッド] 参照するObservableの出力するすべての項目から、同じ項目の連続した重複は除いて出力する新たなObservableオブジェクトを返す。引数(compare)の比較関数が与えられると連続する項目をふたつの引数に受け取って同じかどうか比べ、引数がなければ等価比較される。
of()
public static of(values: ...T, scheduler: Scheduler): Observable<T>
[静的メソッド] 引数値が出力される新たなObservableオブジェクトをメソッドの実行時ただちにつくり、あとでそのオブジェクトが完了を知らせる。
pipe()
public pipe(operations: ...*): Observable
[インスタンスメソッド] 引数のオペーレータの処理をつなぎあわせて順に行う。
subscribe()
public subscribe(observerOrNext: Observer | Function, error: Function, complete: Function): ISubscription
[インスタンスメソッド] Observableオブジェクトを実行して、引数に登録したObserverまたは関数により出力の通知を扱う。
switchMap()
public switchMap(project: function(value: T, ?index: number): ObservableInput, resultSelector: function(outerValue: T, innerValue: I, outerIndex: number, innerIndex: number): any): Observable
[インスタンスメソッド] 参照するObservableオブジェクトの値に引数(project)の関数に渡してObservableに変え、出力するObservableインスタンスに組み入れる。新たな値からつくられたObservableは、前のオブジェクトを破棄して出力インスタンスに差し込まれる。

関数 構文と説明
catchError()
public catchError(selector: function): Observable
[静的関数] Observableオブジェクトのエラーをキャッチして、新たなObservableオブジェクトを返すか、エラーをスローする。
tap()
public tap(nextOrObserver: Observer | function, error: function, complete: function): Observable
[静的関数] Observableオブジェクトの出力すべてに引数の副次的な処理を行い、もとのObservableオブジェクトそのものを返す。

03 検索コンポーネントのテンプレートをつくる

検索コンポーネント(heroine-search.component)のテンプレートはさほど行数もありません。でき上がりをコード003に示しましょう。また、スタイルを定めるCSSファイルも併せて掲げます。テキストフィールド(<input>要素)に入力したキーを放すイベント(keyup)に、コンポーネントの検索メソッド(search())がバインディングされて呼び出されます。引数は変数(searchBox)で参照した要素の入力値(valueプロパティ)です。検索して得られたデータはプロパティ(heroines$)に納められますので、*ngForディレクティブでひとつずつ取り出してリストに加え、詳細情報へのリンク(routerLinkディレクティブ)も与えられています。

ここで気になるのは、*ngForディレクティブでプロパティ(heroines$)に添えた| asyncの記述でしょう。これはAsyncPipeと呼ばれます。プロパティに納められているのはObservableオブジェクトです(わかるように$をつけました)。このままでは値が得られません。TypeScriptコードであれば、Observable.subscribe()メソッドを呼び出さなければならないところです。その役割を担って値を取り出すのが| asyncなのです。

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

src/app/heroine-search/heroine-search.component.html

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

src/app/heroine-search/heroine-search.component.css

.search-result li {
	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;
	list-style-type: none;
}
.search-result li:hover {
	background-color: #607D8B;
}
.search-result li a {
	color: #888;
	display: block;
	text-decoration: none;
}
.search-result li a:hover {
	color: white;
}
.search-result li a:active {
	color: white;
}
#search-box {
	width: 200px;
	height: 20px;
}
ul.search-result {
	margin-top: 0;
	padding-left: 0;
}

04 ダッシュボードに検索コンポーネントを加える

検索コンポーネント(heroine-search.component)の要素(セレクタapp-heroine-search)は、以下のようにダッシュボードコンポーネント(dashboard.component)のテンプレートの最後に加えてください。これでダッシュボードに検索フィールドが加わり、テキストを打ち込めばその文字列を含むデータがリストで表れます(図001)。項目をクリックすれば、そのデータの詳細情報画面に移るはずです。

src/app/heroine-search/heroine-search.component.ts

@Component({
	selector: 'app-heroine-search',

})
export class HeroineSearchComponent implements OnInit {

}

src/app/dashboard/dashboard.component.ts

<div class="grid grid-pad">

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

図001■検索フィールドにテキストを入力すると該当するデータがリストで表れる

図001

ダッシュボードコンポーネントのテンプレートは、つぎのコード004のとおりです。アプリケーションの動きやそれぞれのファイルのコードは、Plunker「Angular 5 Example - Tour of Heroines 10」でお確かめください。

コード004■ダッシュボードコンポーネントのテンプレート

src/app/dashboard/dashboard.component.ts

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


作成者: 野中文雄
作成日: 2018年2月15日


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