水無瀬のプログラミング日記

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。

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

まとめ

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

参考リンク