Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

前回の振り返り

前回 は、AngularにおけるFirebaseのセットアップとAPIを使ってFirebaseからデータを取得する方法を学びました。
本記事は前回の続きですので、前回の内容は頭に入れておいてください。
わからなくなったら、前回の記事を見返しましょう。

この記事のソースコード

https://github.com/seteen/AngularGuides/tree/入門その11

データの扱い方と準備

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

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

{
    "-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

前回からしばらく時間が経っている場合は、トークンの有効期限が切れてアクセスエラーが発生します。
コンソールから 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://github.com/seteen/AngularGuides/commit/0d2c841bd6329299e9826bcffa54e7972f48f579

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

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

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

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

# 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://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
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のときと同様に認証のための文字列が入っています。

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

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

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

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

{"name":"-LL5YUmEu40kYQHYmdRr"}

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

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

product-new.component.ts

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

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

TIPS: Observable の実行
Observable は少し注意が必要です。 Observablesubscribe されて始めて実行されるため、
今回のようにまだ実行後に何かをする処理を書いていない場合でも、実行するために 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://github.com/seteen/AngularGuides/commit/6f11d7be0d8f1aae3503300710130e874c006975

まとめ

今回は、POST APIの説明をしました。
次回は、更新、削除のAPIの使い方を見ていきます。

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

入門記事一覧

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

seteen
Angularを広めたいエンジニア Ruby, Angularが好きです。
https://twitter.com/yazumoto
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away