Angularヒーローチュートリアル7

# HTTP
データの取得、追加、保存、編集、削除、検索をHTTPリクエストを通して行えるようにします。

## リモートサーバー
今回は、In-memory Web APIを利用してリモートサーバーとの通信をします。

まずはインメモリwebAPIをインストール

```terminal:ターミナル
$ npm install angular-in-memory-web-api --save
```
また、インメモリデータのサービスを作成します。

```terminal:ターミナル
$ cd src/spp
$ ng generate service InMemoryData
```

今作成した奴らを`app.module.ts`に加えます。

```ts: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ファイルを以下のように書き換えます。

```ts:in-memory-data.service.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`をこのように変更します。

```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()`は、<Hero[]>のように型を指定すると、その型で返す。デフォルトはJSON。

## エラーハンドリング

リモートサーバーからデータの受け取りに失敗した場合のエラー処理を記述します。

まずは必要なオペレーターをインポート

```ts:hero.service.ts
import {catchError, map, tap} from 'rxjs/operators';
```
次に`getHeroes()`に追記

```ts:hero.service.ts
  getHeroes(): Observable<Hero[]>{
    return this.http.get<Hero[]>(this.heroesUrl).pipe(
      catchError(this.handleError<Hero[]>('getHeroes', []))
    );
  }
```
ここで出てきた関数を定義します。

```ts:hero.service.ts
  /**
   * 失敗した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()`を加えます。

```ts:hero.service.ts
  getHeroes(): Observable<Hero[]>{
    return this.http.get<Hero[]>(this.heroesUrl).pipe(
      tap(heroes => this.log('fetched heroes')),
      catchError(this.handleError<Hero[]>('getHeroes', []))
    );
  }
```

<img width="1407" alt="スクリーンショット 2020-02-06 0.04.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519379/c57637e5-5324-e515-6934-e77bbb6651e0.png">
このようにメッセージが表示されます。

## ヒーローの詳細
URLのidからヒーローの詳細を探して表示します。
`getHero()`を修正します。

```ts:hero.service.ts
  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に追加します。

```html:hero-detail.component.html
    <button (click)="save()">save</button>
```
次にここで出てきたsaveメソッドを定義します。

```ts:hero-detail.component.ts
  save(): void{
    this.heroService.updateHero(this.hero).subscribe(() => this.goBack());
  }
```
次にここで出てきたupdateHeroメソッドを定義します。

```ts:hero.service.ts
  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`というパラメータを定義します。

```ts:hero.service.ts
  httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json'
    })
  };
```

これで編集した情報を保存することができます。

##ヒーローを追加
まずHTMLにフォームとaddボタンを追加する。

```html:hero.component.html
<div>
  <label>Hero name:
    <input #heroName />
  </label>
  <!-- (click) 入力値をadd()に渡したあと、入力をクリアする -->
  <button (click)="add(heroName.value); heroName.value=''">
    add
  </button>
</div>
```
次にaddメソッドを定義する。

```ts:hero.component.ts
add(name: string): void {
  name = name.trim();
  if (!name) { return; }
  this.heroService.addHero({ name } as Hero)
    .subscribe(hero => {
      this.heroes.push(hero);
    });
}
```
最後に、addHeroメソッドを定義する。

```ts:hero.service.ts
/** 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が追加できるようになります。

<img width="332" alt="スクリーンショット 2020-02-07 13.25.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519379/58627e7c-410a-8c49-8623-6bfa4a2a7255.png">

## ヒーローを削除
ヒーローリストのHTMLを以下のように変更します。

```html:hero.component.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を書きます。公式サイトの最終的なコードを参考にします。
> https://angular.jp/tutorial/toh-pt6

次に`delete()`を定義していきます。

```ts:hero.component.ts
delete(hero: Hero): void {
  this.heroes = this.heroes.filter(h => h !== hero);
  this.heroService.deleteHero(hero).subscribe();
}
```

ここで予備知識

- Observableを返すメソッドのあとは`subscribe()`しないと何も起こらないので注意。

最後に`deleteHero()`を定義します。

```ts:hero.service.ts
/** 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`を定義します。

```ts:hero.service.ts
/* 検索語を含むヒーローを取得する */
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', []))
  );
}
```

次にダッシュボードに検索欄を追加します。

```html:dashboard.component.html
<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を作成します。

```terminal:ターミナル
ng g c hero-search 
```
作成したコンポーネントの中身を書いていきます。

```html:hero-search.component.ts
<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であることを示している。

```css:hero-search.component.css
/* 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;
}
```

```ts:hero-search.compnent.ts
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)),
    );
  }
}
```

すると以下のようにヒーローの候補リストが表示され、洗濯したら詳細画面に飛びます。

<img width="1440" alt="スクリーンショット 2020-02-07 14.12.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519379/f186404e-2383-64e9-ee3c-1f7f9ee0db43.png">

チュートリアル終了。

>参考
https://angular.jp/