HTTP
データの取得、追加、保存、編集、削除、検索をHTTPリクエストを通して行えるようにします。
リモートサーバー
今回は、In-memory Web APIを利用してリモートサーバーとの通信をします。
まずはインメモリwebAPIをインストール
$ npm install angular-in-memory-web-api --save
また、インメモリデータのサービスを作成します。
$ cd src/spp
$ ng generate service InMemoryData
今作成した奴らを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 { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { MessageComponent } from './message/message.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { InMemoryDataService } from './in-memory-data.service';//追加
@NgModule({
declarations: [
AppComponent,
HeroesComponent,
HeroDetailComponent,
MessageComponent,
DashboardComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
//追加
HttpClientModule,
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
HttpClientModule をインポートすることでHTTPサービスを有効にします。
さっき作ったinmemorydataサービスのtsファイルを以下のように書き換えます。
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Hero } from './hero';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 11, name: 'Dr Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return {heroes};
}
// Overrides the genId method to ensure that a hero always has an id.
// If the heroes array is empty,
// the method below returns the initial number (11).
// if the heroes array is not empty, the method below returns the highest
// hero id + 1.
genId(heroes: Hero[]): number {
return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11;
}
}
これがmock-heroes.tsの代わりになります。
ヒーローデータを取得
hero.service.tsをこのように変更します。
import { Injectable } from '@angular/core';
import {HttpClient, HttpHeaders} from '@angular/common/http';//追加
import {Hero} from './hero';
import {HEROES} from './mock-heroes';
import {Observable, of} from 'rxjs';
import {MessageService} from './message.service';
@Injectable({
providedIn: 'root'
})
export class HeroService {
private heroesUrl = 'api/heroes'; // 追加(Web APIのURL)
constructor(
private http: HttpClient,//追加
private messageService: MessageService
) { }
//変更(サーバーからヒーローを取得する)
getHeroes(): Observable<Hero[]>{
return this.http.get<Hero[]>(this.heroesUrl)
}
getHero(id: number): Observable<Hero>{
this.messageService.add(`HeroService: fetched hero id=${id}`);
return of(HEROES.find(hero => hero.id === id));
}
//追加(メッセージを代入する関数を作成(あとで使う))
private log(message: string){
this.messageService.add(`HeroService: ${message}`);
}
}
getHeroes()におけるヒーローの取得をof()からhttp.get()に変えました。これでも問題なく動きます。
予備知識をまとめます。
-
HttpClientメソッドは、RxJSのObservableを返す。 -
HttpClient.get()は、のように型を指定すると、その型で返す。デフォルトはJSON。
エラーハンドリング
リモートサーバーからデータの受け取りに失敗した場合のエラー処理を記述します。
まずは必要なオペレーターをインポート
import {catchError, map, tap} from 'rxjs/operators';
次にgetHeroes()に追記
getHeroes(): Observable<Hero[]>{
return this.http.get<Hero[]>(this.heroesUrl).pipe(
catchError(this.handleError<Hero[]>('getHeroes', []))
);
}
ここで出てきた関数を定義します。
/**
* 失敗したHttp操作を処理します。
* アプリを持続させます。
* @param operation - 失敗した操作の名前
* @param result - observableな結果として返す任意の値
*/
private handleError<T> (operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: リモート上のロギング基盤にエラーを送信する
console.error(error); // かわりにconsoleに出力
// TODO: ユーザーへの開示のためにエラーの変換処理を改善する
this.log(`${operation} failed: ${error.message}`);
// 空の結果を返して、アプリを持続可能にする
return of(result as T);
};
}
ここで予備知識をまとめます。
-
:?は、任意のパラメータ -
<T>は、ジェネリックと呼ばれる抽象的な型(TやUを使うのが習慣)
メッセージを表示
getHeroes()にRxJSのtap()を加えます。
getHeroes(): Observable<Hero[]>{
return this.http.get<Hero[]>(this.heroesUrl).pipe(
tap(heroes => this.log('fetched heroes')),
catchError(this.handleError<Hero[]>('getHeroes', []))
);
}
このようにメッセージが表示されます。
ヒーローの詳細
URLのidからヒーローの詳細を探して表示します。
getHero()を修正します。
getHero(id: number): Observable<Hero>{
const url = `${this.heroesUrl}/${id}`;
return this.http.get<Hero>(url).pipe(
tap(_ => this.log(`fetch hero id=${id}`)),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
ヒーローを編集
編集した情報が保存されるようにします。
まず、保存ボタンをHTMLに追加します。
<button (click)="save()">save</button>
次にここで出てきたsaveメソッドを定義します。
save(): void{
this.heroService.updateHero(this.hero).subscribe(() => this.goBack());
}
次にここで出てきたupdateHeroメソッドを定義します。
updateHero(hero: Hero): Observable<any>{
return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
tap(_ => this.log(`update hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);
}
最後にここで出てきたhttpOptionsというパラメータを定義します。
httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
};
これで編集した情報を保存することができます。
ヒーローを追加
まずHTMLにフォームとaddボタンを追加する。
<div>
<label>Hero name:
<input #heroName />
</label>
<!-- (click) 入力値をadd()に渡したあと、入力をクリアする -->
<button (click)="add(heroName.value); heroName.value=''">
add
</button>
</div>
次にaddメソッドを定義する。
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.addHero({ name } as Hero)
.subscribe(hero => {
this.heroes.push(hero);
});
}
最後に、addHeroメソッドを定義する。
/** POST: サーバーに新しいヒーローを登録する */
addHero (hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
}
http.post()でヒーローをポストサーバーに追加します。そのとき自動でidも追加されます。
これでnewheroが追加できるようになります。
ヒーローを削除
ヒーローリストのHTMLを以下のように変更します。
<ul class="heroes">
<li *ngFor="let hero of heroes">
<a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>
<button class="delete" title="delete hero"
(click)="delete(hero)">x</button>
</li>
</ul>
xがヒーローと被っていて見えないのでcssを書きます。公式サイトの最終的なコードを参考にします。
次にdelete()を定義していきます。
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
this.heroService.deleteHero(hero).subscribe();
}
ここで予備知識
- Observableを返すメソッドのあとは
subscribe()しないと何も起こらないので注意。
最後にdeleteHero()を定義します。
/** DELETE: サーバーからヒーローを削除 */
deleteHero (hero: Hero | number): Observable<Hero> {
const id = typeof hero === 'number' ? hero : hero.id;
const url = `${this.heroesUrl}/${id}`;
return this.http.delete<Hero>(url, this.httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`)),
catchError(this.handleError<Hero>('deleteHero'))
);
}
これで削除機能ができました。
検索機能
ダッシュボードでヒーローの名前を検索したら、指定されたヒーローを探す機能を作ります。
まずサービスで、searchHeroesを定義します。
/* 検索語を含むヒーローを取得する */
searchHeroes(term: string): Observable<Hero[]> {
if (!term.trim()) {
// 検索語がない場合、空のヒーロー配列を返す
return of([]);
}
return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
tap(_ => this.log(`found heroes matching "${term}"`)),
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
}
次にダッシュボードに検索欄を追加します。
<h3>Top Heroes</h3>
<div class="grid grid-pad">
<a *ngFor="let hero of heroes" class="col-1-4"
routerLink="/detail/{{hero.id}}">
<div class="module hero">
<h4>{{hero.name}}</h4>
</div>
</a>
</div>
<app-hero-search></app-hero-search>
そして、hero-searchコンポーネンtを作成します。
ng g c hero-search
作成したコンポーネントの中身を書いていきます。
<div id="search-component">
<h4><label for="search-box">Hero Search</label></h4>
<input #searchBox id="search-box" (input)="search(searchBox.value)" />
<ul class="search-result">
<li *ngFor="let hero of heroes$ | async" >
<a routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
</li>
</ul>
</div>
予備知識
-
$は変数がObservableであることを示している。
/* HeroSearch private styles */
.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;
}
import { Component, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import {
debounceTime, distinctUntilChanged, switchMap
} from 'rxjs/operators';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-hero-search',
templateUrl: './hero-search.component.html',
styleUrls: [ './hero-search.component.css' ]
})
export class HeroSearchComponent implements OnInit {
heroes$: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(private heroService: HeroService) {}
// 検索語をobservableストリームにpushする
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes$ = this.searchTerms.pipe(
// 各キーストロークの後、検索前に300ms待つ
debounceTime(300),
// 直前の検索語と同じ場合は無視する
distinctUntilChanged(),
// 検索語が変わる度に、新しい検索observableにスイッチする
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
}
}
すると以下のようにヒーローの候補リストが表示され、洗濯したら詳細画面に飛びます。
チュートリアル終了。