Firebase Realtime DatabaseをAngularで使ってみる

はじめに

前回Reactで使ってみたので今回はAngularでやってみる。

Firebaseの設定は前回と同じため、今回はプロジェクト作るところから始めていく。
(Firebaseの設定については前回参照)

TL;DR.

今回作ったコード

実装していく

プロジェクト作成

angular cliからスタート。
せっかくなのでv10から追加されたstrictモードを使ってみる。

$ npx @angular/cli new realtime-db-sample-with-angular --strict

ライブラリのインストール

公式から提供されているライブラリであるAngularFire があるのでそちらをインストールする。
ログイン周りを実装するときにSDKが必要になるので、こちらもインストールしておく。

$ npm run ng -- add @angular/fire
$ npm install --save firebase

Firebase周り

Firebaseの操作に関する部分は表示系とは直接関係無い+汎用的なものになると思うのでライブラリ化しておく。

$ npm run ng -- g library firebase-library

初期化処理

Quick Start を参考に初期化処理周りをサクッと追加する。

コンフィグ周りを個別のファイルに切り出して、

import { FirebaseOptions } from '@angular/fire';

export const firebaseConfig: FirebaseOptions = {
  production: false,
  firebase: {
    // コピペ
  }
};

それをfirebase-library.module.tsで初期化のときに読み込む。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AngularFireModule } from '@angular/fire';
import { AngularFireDatabaseModule } from '@angular/fire/database';
import { FirebaseUsecaseService } from './service/firebase-usecase.service';
// ↑で作ったconfigファイル
import { firebaseConfig } from './config/config';
import { LoginComponent } from './components/login/login.component';
import { AngularFireAuthModule } from '@angular/fire/auth';
import { FirebaseFormatterService } from './service/firebase-formatter.service';

@NgModule({
  declarations: [],
  imports: [
    // 初期化のときにconfigを渡してあげる
    AngularFireModule.initializeApp(firebaseConfig.firebase),
    // RealtimeDatabaseを使うので必要なmoduleをimport
    AngularFireDatabaseModule,
    // ログイン周りに必要なmoduleをimport
    AngularFireAuthModule
  ],
  providers: [],
  exports: []
})
export class FirebaseLibraryModule { }

読み書きするserviceを作成

CRUD周りの操作を行うサービスを作成。
こちらも公式サンプル を参考にしながら進めていく。
まずはCLIで生成するところから。

$ npm run ng -- g service firebse-usecase --project=firebase

CRUDを進めていくが基本的にはReactでやった時と同じため、
今回は全件取得、登録、削除のみを実装することにする。

import { Injectable } from '@angular/core';
import { AngularFireDatabase, SnapshotAction } from '@angular/fire/database';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { FirebaseFormatterService } from './firebase-formatter.service';
import { FirebaseKeyValue } from '../types/firebase-types';

@Injectable({
  providedIn: 'root'
})
export class FirebaseUsecaseService {
  private items: Observable<SnapshotAction<string>[]>;
  constructor(private readonly db: AngularFireDatabase, private readonly formatter: FirebaseFormatterService) {
    // 指定したURL以下の値がリスト形式で取得される
    // 絞りたければ渡すパスを絞っていけば良い    
    this.items = this.db.list<string>('/sample').snapshotChanges();
  }

  /**
   * 全件取得する
   */
  fetchDocumentAll() {
    // 余分なパラメータが多いので、使いやすい形式に整形する
    return this.items.pipe(map(this.documentToResponse));
  }

  /**
   * 登録を行う
   */
  async setDocument(registerKeyValue: FirebaseKeyValue) {
    // setでの更新は上書きになってしまうので、登録済みのデータを取得しておく
    // 今回は値を取得してから後続処理に移りたいため、Promiseに変換して取得を待つ
    const item = await this.fetchDocumentAll().pipe(take(1)).toPromise();
    // 登録したいデータと登録されているデータをマージしつつ整形する
    const registerDocument = this.objectToDocument([...item, registerKeyValue]);
    // sampleのデータを上書き登録
    this.db.object('/sample').set(registerDocument);
  }

  /**
   * 指定したパス配下のデータを全て削除する
   */
  deleteAll() {
    this.db.object('/sample').remove();
  }

  /**
   * 取得したデータからkey,valueの値のみを取り出す
   */
  private documentToResponse(document: SnapshotAction<string>[]): FirebaseKeyValue[] {
    return document.map(item => ({ key: item.key, value: item.payload.val() }));
  }

  /**
   * 登録用のデータに整形する
   */
  private objectToDocument(keyValues: FirebaseKeyValue[]): FirebaseDocument {
    // 登録したいデータは{[key: string]: value}形式なので整形
    return keyValues.reduce((previous, current) => {
      // keyが無い場合は今回考慮しない
      const registereData = {[current.key!]: current.value};
      return {...previous, ...registereData};
    }, {});
  }
}

ログインコンポーネント

登録、削除はログイン済みの場合のみ可能としているので、ログイン処理を実装していく。
ログイン処理を行うコンポーネントは作成したライブラリ側に用意する。

$ npm run ng -- g component components/login --project=firebase-library

こちらも公式のサンプルを参考にしながら進めていく。
まずはts側から。

import { Component } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { Observable } from 'rxjs';
import { auth, User } from 'firebase/app';

@Component({
  selector: 'lib-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent {
  private _user: Observable<User | null>

  constructor(private auth: AngularFireAuth) {
    this._user = this.auth.user;
  }

  login() {
    // Googleログインを行う
    this.auth.signInWithPopup(new auth.GoogleAuthProvider());
  }
  logout() {
    // ログアウト
    this.auth.signOut();
  }

  get user() {
    return this._user;
  }
}

次にHTML側を書いていく。
user情報が取得できていれば、ログアウトボタンを表示。
できていなければログインボタンを表示する。

<ng-container *ngIf="user | async; else showLoginButton">
  <button (click)="logout()">ログアウト</button>
</ng-container>
<ng-template #showLoginButton>
  <button (click)="login()">ログイン</button>
</ng-template>

app.moduleに追記

ここまでで作ったlibraryを使えるようにapp.moduleに追記する。

import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ListComponent } from './components/list/list.component';
import { FirebaseLibraryModule } from 'firebase-library';
import { FormComponent } from './components/form/form.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    // これを追加する
    FirebaseLibraryModule,
    // ルーティングを使いたいのでimportしておく
    AppRoutingModule,
    // 後でリアクティブフォームを使いたいので、importしておく
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

取得結果表示ページ作成

ここから表示系を準備していく。
まずは取得結果を表示するページを作成する。
CLIでcomponentを作るところから。

$ npm run ng -- g component components/list

ts側は下記の通り。
作ったlibraryを使い全件取得→結果を表示するだけのcomponentにする。

import { Component } from '@angular/core';
import { FirebaseUsecaseService } from 'firebase-library';
import { BehaviorSubject } from 'rxjs';
@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.scss']
})
export class ListComponent {
  private items$ = new BehaviorSubject<any[]>([]);
  constructor(private readonly firebase: FirebaseUsecaseService) {
    firebase.fetchDocumentAll().subscribe(res => {
      this.items$.next(res);
    }, err => {
      console.log('error', err);
    });
  }

  get items() {
    return this.items$;
  }
}

HTML側は↓の通り。
itemsはBehaviorSubjectなので、asyncパイプを使って表示させてあげる。

<dl>
  <ng-container *ngFor="let item of items | async">
    <dt>key: {{item.key}}</dt>
    <dt>value: {{item.value}}</dt>
  </ng-container>
</dl>

登録、削除ページ作成

サクッとcomponentを作るところから始める。

$ npm run ng -- g component components/form

登録するデータを入力する箇所はリアクティブフォームで作ってみる。
リアクティブフォームについては昔メモしたのでそちらを参考に。

import { FormGroup, FormControl, Validators } from '@angular/forms';
import { Component } from '@angular/core';
import { FirebaseUsecaseService } from 'firebase-library';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.scss']
})
export class FormComponent {
  // key、valueはどちらも必須とする
  readonly formGroup = new FormGroup({
    key: new FormControl('', [Validators.required]),
    value: new FormControl('', [Validators.required])
  });

  constructor(private readonly firebase: FirebaseUsecaseService) { }

  registerData() {
    // setDocumentはasync functionだけど、今回は待つ必要が無いので呼び出しっぱなし
    this.firebase.setDocument({key: this.key?.value, value: this.value?.value});
  }
  deleteAll() {
    this.firebase.deleteAll();
  }
  
  get key() {
    return this.formGroup.get('key');
  }
  get value() {
    return this.formGroup.get('value');

  }
}

HTML側は下記の通り。
ここでログインボタンを読み込んでおく。

<div>
  <!-- 作成したログイン/ログアウトボタンコンポーネント -->
  <lib-login></lib-login>
</div>
<form [formGroup]="formGroup">
  <label>key: <input type="text" formControlName="key"/></label>
  <!-- 必須項目未入力のときのエラーメッセージ -->
  <div *ngIf="key?.invalid && (key?.dirty || key?.touched)">
    <span *ngIf="key?.hasError('required')">必須です。</span>
  </div>
  <label>value: <input type="text" formControlName="value"/></label>
  <!-- 必須項目未入力のときのエラーメッセージ -->
  <div *ngIf="value?.invalid && (value?.dirty || value?.touched)">
    <span *ngIf="value?.hasError('required')">必須です。</span>
  </div>
  <button (click)="registerData()">登録</button>
</form>
<button (click)="deleteAll()">全消し</button>

ルーティングの設定

一覧表示と登録、削除を一応ページ分ける。
ルーティングの設定が必要なのでサクッと実装しておく。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ListComponent } from './components/list/list.component';
import { FormComponent } from './components/form/form.component';

const routes: Routes = [
  {path: 'list', component: ListComponent},
  {path: 'form', component: FormComponent}
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

動作確認

実装が終わったので動作確認していく。
先にライブラリのビルドが必要なのでまずはそこから。

$ npm run build -- firebase-library

ビルドが無事成功したらアプリを起動する。

$ npm start

起動できたらhttp://localhost:4200/listにアクセスしてみる。
画像の取得結果を表示できていればOK。

ss20200711-02

次にhttp://localhost:4200/formにアクセスしてみる。
画像の様にログインボタン、登録フォーム、削除ボタンが表示されていればOK。

まとめ

今回はAngularでRealtimeDatabaseを使ってみた。
公式からライブラリが提供されているのもあり、結構簡単に実装できたと思う。
さっくり作りすぎてページ分けとか微妙だったので、もうちょっときれいに作っても良かった気がする。

参考リンク