HTML5テクニカルノート
Angular 5入門 10: HTTPサービスでリモートのデータを検索する
- ID: FN1802003
- Technique: HTML5 / JavaScript
- Package: Angular 5.2.4
「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.tsexport 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)),
);
}
}
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
しました。Subject
はObservable
のサブクラスです。そのインスタンスをprivate
のプロパティ(searchTerms)に与えたうえで、コンポーネントがつくられたとき(ngOnInit()
メソッド)Observable
で型づけされたプロパティ(heroines$)に納めています(識別子の終わりの$はObservable
を表すために添えます)。Subject
クラスがObservable
と違うのは、データをあとから加えられることです(「AngularのRxJSを使ってデータの受け渡しをする」の「任意のタイミングでデータを流すSubject」参照)。すると、ユーザーがテキストフィールドに検索語を入力したとき、サービスに渡して検索することができるのです。
src/app/heroine-search/heroine-search.component.tsimport {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.tsimport { 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()
|
[インスタンスメソッド] 参照するObservable オブジェクトから、引数の時間が過ぎたのちに値をひとつ出力する。複数の値が渡された場合は、直近の値のみ出力される。
|
distinctUntilChanged()
|
[インスタンスメソッド] 参照するObservable の出力するすべての項目から、同じ項目の連続した重複は除いて出力する新たなObservable オブジェクトを返す。引数(compare )の比較関数が与えられると連続する項目をふたつの引数に受け取って同じかどうか比べ、引数がなければ等価比較される。
|
of()
|
[静的メソッド] 引数値が出力される新たなObservable オブジェクトをメソッドの実行時ただちにつくり、あとでそのオブジェクトが完了を知らせる。
|
pipe()
|
[インスタンスメソッド] 引数のオペーレータの処理をつなぎあわせて順に行う。 |
subscribe()
|
[インスタンスメソッド] Observable オブジェクトを実行して、引数に登録したObserver または関数により出力の通知を扱う。
|
switchMap()
|
[インスタンスメソッド] 参照するObservable オブジェクトの値に引数(project )の関数に渡してObservable に変え、出力するObservable インスタンスに組み入れる。新たな値からつくられたObservable は、前のオブジェクトを破棄して出力インスタンスに差し込まれる。
|
関数 | 構文と説明 |
---|---|
catchError()
|
[静的関数] Observable オブジェクトのエラーをキャッチして、新たなObservable オブジェクトを返すか、エラーをスローする。
|
tap()
|
[静的関数] 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>
.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.tssrc/app/dashboard/dashboard.component.ts@Component({ selector: 'app-heroine-search', }) export class HeroineSearchComponent implements OnInit { }
<div class="grid grid-pad"> </div> <app-heroine-search></app-heroine-search>
図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>
- 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入門 09: HTTPサービスでリモートのデータを加えたり除いたりする
作成者: 野中文雄
作成日: 2018年2月15日
Copyright © 2001-2018 Fumio Nonaka. All rights reserved.