1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2020-02-07

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に加えます。

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

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をこのように変更します。

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。

エラーハンドリング

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

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

hero.service.ts
import {catchError, map, tap} from 'rxjs/operators';

次にgetHeroes()に追記

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

ここで出てきた関数を定義します。

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()を加えます。

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', []))
    );
  }
スクリーンショット 2020-02-06 0.04.47.png このようにメッセージが表示されます。

ヒーローの詳細

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

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に追加します。

hero-detail.component.html
    <button (click)="save()">save</button>

次にここで出てきたsaveメソッドを定義します。

hero-detail.component.ts
  save(): void{
    this.heroService.updateHero(this.hero).subscribe(() => this.goBack());
  }

次にここで出てきたupdateHeroメソッドを定義します。

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というパラメータを定義します。

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

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

ヒーローを追加

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

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

次にaddメソッドを定義する。

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メソッドを定義する。

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が追加できるようになります。

スクリーンショット 2020-02-07 13.25.28.png

ヒーローを削除

ヒーローリストの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()を定義していきます。

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

ここで予備知識

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

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

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を定義します。

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', []))
  );
}

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

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を作成します。

ターミナル
ng g c hero-search 

作成したコンポーネントの中身を書いていきます。

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であることを示している。
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;
}
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)),
    );
  }
}

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

スクリーンショット 2020-02-07 14.12.09.png

チュートリアル終了。

参考
https://angular.jp/

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?