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](https://github.com/angular/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を使ってみた。
公式からライブラリが提供されているのもあり、結構簡単に実装できたと思う。
さっくり作りすぎてページ分けとか微妙だったので、もうちょっときれいに作っても良かった気がする。
公式ライブラリのおかげでとても触りやすかったので、今後なにか作っていきたいところ。