Angular入門 未経験から1ヶ月でサービス作れるようにする その11. Firebaseを使ったAPI通信2

Angularでの HTTP 通信のやり方を解説していきます。
FirebaseのAPIを使ってデータの作成をやっていきます。

## 前回の振り返り
[前回](https://qiita.com/seteen/items/aa62787d3d47033d3b4c) は、AngularにおけるFirebaseのセットアップとAPIを使ってFirebaseからデータを取得する方法を学びました。
本記事は前回の続きですので、前回の内容は頭に入れておいてください。
わからなくなったら、前回の記事を見返しましょう。

## この記事のソースコード
[https://github.com/seteen/AngularGuides/tree/入門その11](https://github.com/seteen/AngularGuides/tree/%E5%85%A5%E9%96%80%E3%81%9D%E3%81%AE11)

## データの扱い方と準備

### product.ts のデータ構造の変更

Firebaseでは、下記のような Key => Data のデータ構造になっています。

``` .json
{
    "-LKX29TybTT3bEv5sIyy": {
        "description": "神は云った。「Angularあれ」。するとAngularが出来た。",
        "id": 1,
        "name": "Angular入門書「天地創造の章」",
        "price": 3800
    },
    "-LKX2PIcjVrwhEwtjfOE": {
        "description": "年収300万のSEが、Angularと出会う。それは、小さな会社の社畜が始めた、最初の抵抗だった。",
        "id": 2,
        "name": "Angularを覚えたら、年収も上がって、女の子にももてて、人生が変わりました！",
        "price": 410
    },
    "-LKX2VYs92mQcjaqhI6i": {
        "description": "スパゲッティの沼でデスマーチ真っ最中の田中。過酷な日々からの現実逃避か彼は、異世界に放り出され、そこでAngularの入門書を拾う。現実逃避でさえ、プログラミングをするしかない彼に待ち受けるのは！？",
        "id": 3,
        "name": "異世界転生から始めるAngular生活(1)",
        "price": 680
    }
}
```

`Product` クラスには、 この `key` を扱う値がないので、 `key` という項目を追加します。
また、同じような用途で用意していた `id` に対しては、不要なので消してしまいましょう。

``` src/app/shared/models/product.ts
export class Product {
  key: string;
  name: string;
  price: number;
  description: string;

  constructor(key, name, price, description) {
    this.key = key;
    this.name = name;
    this.price = price;
    this.description = description;
  }
}
```

また、これに伴い、参照しているところを修正していきます。
Typescript では、このあたりをエラーにしてくれるので助かりますね。

```　src/app/shared/services/product.service.ts
... 略
  list(): Observable<Product[]> {
    return this.http.get(`https://angular-guide-firebase.firebaseio.com/products.json`, { params: { auth: this.TOKEN } }).pipe(
      map((response: any) =>
        Object.keys(response).map((key: string) => {
          const prd = response[key];
          return new Product(key, prd.name, prd.price, prd.description); // <= 変更
        })
      )

... 略

  update(product: Product): void {
    const index = this.products.findIndex((prd: Product) => prd.key === product.key); // <= 変更
    this.products[index] = product;
  }
... 略
```

``` src/app/product/product-edit/product-edit.component.ts
... 略
export class ProductEditComponent implements OnInit {
  productForm = this.fb.group({
    key: [''], // <= 変更
    name: [''],
    price: ['', Validators.min(100)],
    description: [''],
  });

... 略

  ngOnInit() {
    this.route.params.subscribe((params: Params) => {
      this.productService.get(params['id']).subscribe((product: Product) => {
        this.productForm.setValue({
          key: product.key, // <= 変更
          name: product.name,
          price: product.price,
          description: product.description,
        });
      });
    });
  }
```

``` src/app/product/product-edit/product-edit.component.html
<div class="container">
  <div class="title">商品編集</div>
  <form (ngSubmit)="saveProduct()" [formGroup]="productForm">
    <div class="edit-form">
      <div class="edit-line">
        <label>Key</label>  // <= 変更
        <span>{{ productForm.controls.key.value }}</span> // <= 変更
      </div>
... 略
```

### Firebaseのアクセスパスの変更

前回の記事では、 `/products.json` というパスを利用しました。
今回からは、ログインしているユーザ専用のデータ管理という意味で、 `/users/:uid/products.json` ( `:uid` はユーザごとに変動 ) というパスを利用していきます。

このあたりの修正をやっていきましょう。
まずは、自分の `uid` を Firebase から取得します。

``` src/app/shared/services/firebase.service.ts
... 略
  signInOrCreateUser(email, password): void {
    firebase.auth().signInWithEmailAndPassword(email, password).then((userCredential: UserCredential) => {
      console.log(userCredential.user.uid); // <= 追加
      firebase.auth().currentUser.getIdToken().then((token: string) => {
        console.log(token);
      });
    }).catch(() => {
      return firebase.auth().createUserWithEmailAndPassword(email, password).then((userCredential: UserCredential) => {
        firebase.auth().currentUser.getIdToken().then((token: string) => {
          console.log(token);
        });
      });
... 略
```

これにより、 `http://localhost:4200/products` にアクセスすることで、 `uid` が取得できます。

![001.gif](https://qiita-image-store.s3.amazonaws.com/0/67424/c409bff1-1a05-446e-58bb-44bfffba777f.gif)


前回からしばらく時間が経っている場合は、トークンの有効期限が切れてアクセスエラーが発生します。
コンソールから uid と一緒に新しいトークンもコピーして、コードの値を変更しましょう。


次に、下記を修正します。

- URLを色々なメソッドでいちいち書くのは面倒なのと、ケアレスミスの温床になるので定数化する
- `list()` メソッドを変更して `/users/:uid/products.json` にする
- 現状ではアクセスすると `null` が返ってきてしまうので、レスポンスが `null` のときは、空の配列を返すようにする

``` src/app/shared/services/product.service.ts
... 略
export class ProductService {
  BASE_URL = 'https://angular-guide-firebase.firebaseio.com'; // <= 追加
  UID = 'iNURmEqjH6R5POfr271nK2OBJ5G3';  // <= 追加
  TOKEN = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjMxNjAwMjk1MjI3ODA5M2RmODA3YzkxMGNjYTBmODE3YmI4ODcxY2YifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vYW5ndWxhci1ndWlkZS1maXJlYmFzZSIsImF1ZCI6ImFuZ3VsYXItZ3VpZGUtZmlyZWJhc2UiLCJhdXRoX3RpbWUiOjE1MzU1NTEwOTMsInVzZXJfaWQiOiJpTlVSbUVxakg2UjVQT2ZyMjcxbksyT0JKNUczIiwic3ViIjoiaU5VUm1FcWpINlI1UE9mcjI3MW5LMk9CSjVHMyIsImlhdCI6MTUzNTU1MTA5MywiZXhwIjoxNTM1NTU0NjkzLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsidGVzdEBleGFtcGxlLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.N3-zy-5KLB72T9O839rQJ4u94749j8KceOh-WLs5tCuv1lZ4S6Xw6nh8wFkzdt55Fj2cprKmPZNzWVlirWDTJr1sg4vGQclSPsz31AXUIbkD9WqLPsCtphW3tIdaJA7gTX5PQLtQXm1BlvX55i-UWGe9U7t07lNwmaOD-Ezue30-ObKON5B2g0JO1gV3p8Y5zK4Z3KoLWrTzjJ1NZEPQRPEtkuO3R9-OOWnQJByXoAyvLFrXZOULJK0BlpYDTouWxe04Ao099RsammtqVG2LmLXnj99FfhhVqoEZn4YvyG0KZpXdlgazVLTI_ssN_6XXNBtZXAvib_QGTJi-1BEarA';

  products = [
    new Product(1, 'Angular入門書「天地創造の章」', 3800, '神は云った。「Angularあれ」。するとAngularが出来た。'),

... 略

  list(): Observable<Product[]> {
    return this.http.get(`${this.BASE_URL}/users/${this.UID}/products.json`, { params: { auth: this.TOKEN } }).pipe( // <= 変更
      map((response: any) => {
          if (response) {
            return Object.keys(response).map((key: string) => {
              const prd = response[key];
              return new Product(key, prd.name, prd.price, prd.description);
            });
          } else {
            return [];
          }
        }
      )
    );
  }

... 略
```

#### 動作確認

起動して動作確認すると、下記のように商品がなくなった状態になるはずです。

![002.png](https://qiita-image-store.s3.amazonaws.com/0/67424/b4d4fb50-c66f-de49-b5e9-278455f8497a.png)

#### ここまでのコミット

https://github.com/seteen/AngularGuides/commit/0d2c841bd6329299e9826bcffa54e7972f48f579

## Firebaseにデータを追加する (POST)

Firebaseに対してデータを追加してみましょう。

まず、データを追加するために、商品追加のページを用意します。

`ng g component` でコンポーネントを作成しましょう。

``` shell
# ng g component product/product-new
CREATE src/app/product/product-new/product-new.component.scss (0 bytes)
CREATE src/app/product/product-new/product-new.component.html (30 bytes)
CREATE src/app/product/product-new/product-new.component.spec.ts (657 bytes)
CREATE src/app/product/product-new/product-new.component.ts (289 bytes)
UPDATE src/app/app.module.ts (1158 bytes)
```

### 商品作成ページの見た目を整える

`product-edit.component.ts` に倣って作成用のコンポーネントを作成していきましょう。
（コンポーネントの共通化については，今後やっていく想定なので、今のところはほとんどコピーです）

``` src/app/product/product-new/product-new.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { FormBuilder, Validators } from '@angular/forms';
import { ProductService } from '../../shared/services/product.service';

@Component({
  selector: 'app-product-newj',
  templateUrl: './product-new.component.html',
  styleUrls: ['../product-edit/product-edit.component.scss']
})
export class ProductNewComponent implements OnInit {
  productForm = this.fb.group({ // <= key はなし
    name: [''],
    price: ['', Validators.min(100)],
    description: [''],
  });

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private fb: FormBuilder,
    private productService: ProductService,
  ) {}

  get name() { return this.productForm.get('name'); }
  get price() { return this.productForm.get('price'); }

  ngOnInit() {}

  saveProduct(): void {} // <= 内容をいったん削除
}
```

``` src/app/product/product-new/product-new.component.html
<div class="container">
  <div class="title">商品作成</div>  // <= 名前を変更
  <form (ngSubmit)="saveProduct()" [formGroup]="productForm">
    <div class="edit-form">
      // Key の表示を削除
      <div class="edit-line">
        <label>名前</label>
        <div>
          <input id="name" type="text" formControlName="name" required maxlength="50" appForbiddenWord="ぬるぽ">
          <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert">
            <div *ngIf="name.errors.required">入力してください</div>
            <div *ngIf="name.errors.maxlength">50文字以内で入力してください</div>
            <div *ngIf="name.errors.forbiddenWord">ガッ</div>
          </div>
        </div>
      </div>
      <div class="edit-line">
        <label>価格</label>
        <div>
          <input type="number" formControlName="price" required>
          <div *ngIf="price.invalid && (price.dirty || price.touched)" class="alert">
            <div *ngIf="price.errors.required">入力してください</div>
            <div *ngIf="price.errors.min">100円以上を入力してください</div>
          </div>
        </div>
      </div>
      <div class="edit-line">
        <label>説明</label>
        <input type="text" formControlName="description">
      </div>
    </div>
    <div class="footer">
      <span class="button white" routerLink="/products">キャンセル</span> // <= パスを変更
      <button class="button black" [class.disabled]="productForm.invalid">作成</button> // <= 作成に変更
    </div>
  </form>
</div>
```

最後に、ルートを追加しましょう。

``` src/app/app-routing.module.ts
... 略
const routes: Routes = [
  { path: 'products', component: ProductListComponent },
  { path: 'products/new', component: ProductNewComponent }, // <= 追加
  { path: 'products/:id', component: ProductDetailComponent },
  { path: 'products/:id/edit', component: ProductEditComponent },
  { path: '', redirectTo: '/products', pathMatch: 'prefix' },
];
... 略
```

また、 `ng g component` コマンドで自動で追記されていますが、 `app.module.ts` も変更されています。

``` src/app/app.module.ts
... 略
@NgModule({
  declarations: [
    AppComponent,
    ProductListComponent,
    ProductDetailComponent,
    ProductEditComponent,
    ForbiddenWordValidatorDirective,
    ProductNewComponent, // <= 追加
  ],

... 略
```

#### 動作確認

`http://localhost:4200/products/new` にアクセスすると、フォームが表示されます。

![003.png](https://qiita-image-store.s3.amazonaws.com/0/67424/2bfe709b-aa17-8bfb-a476-205077a68b55.png)


#### ここまでのコミット

https://github.com/seteen/AngularGuides/commit/889bffcdc62bb8a921d0b3186935a457a386b943

### APIを使ってデータを作成する

POST APIを使ってデータを作ってみましょう。

`ProductService` に商品作成用のメソッドを用意します。

``` src/app/shared/services/product.service.ts
... 略
  create(product: Product): Observable<void> { // <= 追加
    return this.http.post(`${this.BASE_URL}/users/${this.UID}/products.json`, product, { params: { auth: this.TOKEN } }).pipe(
      map((response: any) => product.key = response.name),
    );
  }
... 略
```

作成画面で、商品作成メソッドを呼ぶようにします。

``` src/app/product/product-new/product-new.component.ts
... 略
  saveProduct(): void {
    if (this.productForm.valid) { // <= 追加
      const { name, price, description } = this.productForm.getRawValue();
      this.productService.create(new Product(null, name, price, description)).subscribe(() => {});
    }
  }
... 略
```

#### 解説

##### `ProductService`

``` .ts
this.http.post(`${this.BASE_URL}/users/${this.UID}/products.json`, product, { params: { auth: this.TOKEN } })
```

`http.post` メソッドは、 `URL` , `body` , `option` を引数に持ち、引数で指定されたURLに対して `body` を POST するメソッドです。

これは、 `Observable<Object>` 型を返します。

つまりここは、 `/users/:uid/products.json` のURLに、 `product` をPOSTするという処理です。 `params` の部分はGETのときと同様に認証のための文字列が入っています。


このコードは下記に続きます。

``` .ts
.pipe(
  map((response: any) => product.key = response.name),
);
```

これは、GETのメソッドでも合ったように、 `Observable` から返ってくるレスポンスを `map` メソッドによって変換する処理です。

なお、FirebaseのPOSTのAPIのレスポンスの形は、

``` .json
{"name":"-LL5YUmEu40kYQHYmdRr"}
```

のような形で返ってきます。

今回定義した `create` メソッドは、 `Observable<void>` を返すようにしているため、変換してなにかを返すのではなく、入力された `product` の `key` に `response.name` を入れる処理だけを記載しています。

##### `product-new.component.ts`

`saveProduct` メソッドに処理を追加しました。

内容としては、 `edit` と同じように、 `ProductService` の `create` メソッドを呼び出しているだけです。
本来はここでページ遷移させたいのですが、まだ 編集画面でAPIを使うようにしていないため、ここではまだ記載していません。

> TIPS: Observable の実行
> `Observable` は少し注意が必要です。 `Observable` は `subscribe` されて始めて実行されるため、
> 今回のようにまだ実行後に何かをする処理を書いていない場合でも、実行するために `subscribe` する必要があります。

##### ここまでのコミット

https://github.com/seteen/AngularGuides/commit/2fba888e11a5a075c8acb92e2fc5867875986d30

## 一覧画面から商品作成画面へのリンクを作る

現状、商品作成画面への導線がありませんので、商品一覧画面に導線を追加します。

``` src/app/product/product-list/product-list.component.html
<div class="container">
  <div class="title">商品一覧</div>
  <div class="product-list-container">
    <div class="waiting-for-products" *ngIf="products === null; else productList">
      商品を取得しています...
    </div>
    <ng-template #productList>
      <div class="product-list-table">
        <div class="product-line header">
          <div class="product-id">ID</div>
          <div class="product-name">名前</div>
          <div class="product-price">価格</div>
        </div>
        <div class="product-line" *ngFor="let product of products" (mouseenter)="hovered(product)" (mouseleave)="unhovered(product)">
          <div class="product-id">{{ product.id }} </div>
          <div class="product-name">{{ product.name }}</div>
          <div class="product-price">
            <span *ngIf="!product.hovered; else Unhovered">{{ product.price }}</span>
            <ng-template #Unhovered><span class="button white" [routerLink]="[product.id]">詳細</span></ng-template>
          </div>
        </div>
      </div>
      <div class="footer"> // <= 追加
        <button class="button black" routerLink="/products/new">商品を追加する</button>
      </div>
    </ng-template>
  </div>
</div>
```

``` src/app/product/product-list/product-list.component.scss
.container {
  ... 略

  .footer {
    display: flex;
    justify-content: flex-end;
    padding: 12px;
  }
}

```

#### 動作確認

![004.gif](https://qiita-image-store.s3.amazonaws.com/0/67424/7dff2bdc-7836-3f75-2a50-013521a3027d.gif)

作成を実行して、一覧画面に戻ると作成した商品が追加されていますね。

#### ここまでのコミット
https://github.com/seteen/AngularGuides/commit/6f11d7be0d8f1aae3503300710130e874c006975

## まとめ

今回は、POST APIの説明をしました。
[次回](https://qiita.com/seteen/items/7d7028ac4ab8bb754662)は、更新、削除のAPIの使い方を見ていきます。

[Angular入門 未経験から1ヶ月でサービス作れるようにする その12. Firebaseを使ったAPI通信3](https://qiita.com/seteen/items/7d7028ac4ab8bb754662)

## 入門記事一覧
「Angular入門 未経験から1ヶ月でサービス作れるようにする」は、記事数が多いため、まとめ記事 を作っています。
https://qiita.com/seteen/items/43908e33e08a39612a07