Firebase Realtime DatabaseをAngularで使ってみる


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

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

# TL;DR.
[今回作ったコード](https://github.com/Tetsuya-Minase/program-samples/tree/master/realtime-db-sample-with-angular)

# 実装していく
## プロジェクト作成

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

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

## ライブラリのインストール
公式から提供されているライブラリである`[AngularFire](https://github.com/angular/angularfire)`があるのでそちらをインストールする。
ログイン周りを実装するときにSDKが必要になるので、こちらもインストールしておく。

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

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

```bash
$ npm run ng -- g library firebase-library
```

## 初期化処理
[Quick Start](https://github.com/angular/angularfire/blob/master/docs/install-and-setup.md)を参考に初期化処理周りをサクッと追加する。

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

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

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

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

```typescript
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周りの操作を行うサービスを作成。
こちらも[公式サンプル](https://github.com/angular/angularfire/blob/master/docs/rtdb/objects.md)を参考にしながら進めていく。
まずはCLIで生成するところから。

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

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

```typescript
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};
    }, {});
  }
}
```

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

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

こちらも[公式のサンプル](https://github.com/angular/angularfire/blob/master/docs/rtdb/objects.md)を参考にしながら進めていく。
まずはts側から。

```typescript
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情報が取得できていれば、ログアウトボタンを表示。
できていなければログインボタンを表示する。

```html
<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`に追記する。

```typescript
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を作るところから。

```bash
$ npm run ng -- g component components/list
```

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

```typescript
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パイプを使って表示させてあげる。

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

## 登録、削除ページ作成
サクッとcomponentを作るところから始める。

```bash
$ npm run ng -- g component components/form
```

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

> [Angularのリアクティブフォームまとめ - 水無瀬のプログラミング日記](https://minase-program.hatenablog.com/entry/2018/10/30/011324?_ga=2.210877713.587774258.1594163048-1961159496.1467993109)

```typescript
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側は下記の通り。
ここでログインボタンを読み込んでおく。

```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>
```

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

```typescript
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 { }
```

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

```bash
$ npm run build -- firebase-library
```

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

```bash
$ npm start
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176571/e66f0aa6-84ae-9926-b855-a3f95c3064cf.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176571/8829647d-e1f1-546e-24f6-e485d03818df.png)

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

公式ライブラリのおかげでとても触りやすかったので、今後なにか作っていきたいところ。

# 参考リンク
- [angular/angularfire: The official Angular library for Firebase.](https://github.com/angular/angularfire)
- [angularfire/install-and-setup.md at master · angular/angularfire](https://github.com/angular/angularfire/blob/master/docs/install-and-setup.md)
- [angularfire/objects.md at master · angular/angularfire](https://github.com/angular/angularfire/blob/master/docs/rtdb/objects.md)
- [angularfire/getting-started.md at master · angular/angularfire](https://github.com/angular/angularfire/blob/master/docs/auth/getting-started.md)