Firebaseを使用したAngularの状態管理

公開: 2022-03-11

状態管理は、Webアプリを開発するときに考慮すべき非常に重要なアーキテクチャです。

このチュートリアルでは、Firebaseをバックエンドとして使用するAngularアプリケーションで状態を管理するための簡単なアプローチについて説明します。

州、店舗、サービスなどのいくつかの概念について説明します。 うまくいけば、これにより、これらの用語をよりよく理解し、NgRxやNgXなどの他の状態管理ライブラリをよりよく理解できるようになります。

いくつかの異なる状態管理シナリオとそれらを処理できるアプローチをカバーするために、従業員管理ページを作成します。

Angularのコンポーネント、サービス、Firestore、および状態管理

典型的なAngularアプリケーションには、コンポーネントとサービスがあります。 通常、コンポーネントはビューテンプレートとして機能します。 サービスにはビジネスロジックが含まれるか、外部APIまたは他のサービスと通信して、アクションを完了したり、データを取得したりします。

コンポーネントは、他のサービスまたはHTTPAPIに接続するサービスに接続します。

コンポーネントは通常、データを表示し、ユーザーがアプリを操作してアクションを実行できるようにします。 これを行っている間、データが変更される可能性があり、アプリはビューを更新することでそれらの変更を反映します。

Angularの変更検出エンジンは、ビューにバインドされたコンポーネントの値が変更されたときのチェックを処理し、それに応じてビューを更新します。

アプリが成長するにつれて、私たちはますます多くのコンポーネントとサービスを持ち始めるでしょう。 多くの場合、データがどのように変化しているかを理解し、それがどこで発生したかを追跡するのは難しい場合があります。

AngularとFirebase

Firebaseをバックエンドとして使用すると、リアルタイムアプリケーションを構築するために必要なほとんどの操作と機能を含む非常に優れたAPIが提供されます。

@angular/fireは、公式のAngularFirebaseライブラリです。 これは、Firebase JavaScript SDKライブラリの上位にあるレイヤーであり、AngularアプリでのFirebaseSDKの使用を簡素化します。 これは、Firebaseからコンポーネントへのデータの取得と表示にObservablesを使用するなど、Angularのグッドプラクティスにぴったりです。

コンポーネントは、Observablesを使用して@ angle/fireを介してFirebaseJavaScriptAPIにサブスクライブします。

店舗と州

「状態」は、アプリ内の任意の時点で表示される値と考えることができます。 ストアは、単にそのアプリケーション状態の所有者です。

状態は、アプリケーションの値を反映して、単一のプレーンオブジェクトまたは一連のオブジェクトとしてモデル化できます。

名前、都市、国のいくつかの単純なキーと値のペアを持つサンプルオブジェクトを持つストア保持状態。

Angular/Firebaseサンプルアプリ

構築しましょう:まず、Angular CLIを使用して基本的なアプリのスキャフォールドを作成し、Firebaseプロジェクトに接続します。

 $ npm install -g @angular/cli $ ng new employees-admin` Would you like to add Angular routing? Yes Which stylesheet format would you like to use? SCSS $ cd employees-admin/ $ npm install bootstrap # We'll add Bootstrap for the UI

そして、 styles.scssで:

 // ... @import "~bootstrap/scss/bootstrap";

次に、 @angular/fireをインストールします。

 npm install firebase @angular/fire

次に、FirebaseコンソールでFirebaseプロジェクトを作成します。

Firebaseコンソールの[プロジェクトの追加]ダイアログ。

これで、Firestoreデータベースを作成する準備が整いました。

このチュートリアルでは、テストモードから始めます。 本番環境へのリリースを計画している場合は、不適切なアクセスを禁止するルールを適用する必要があります。

[ロックモードで開始]ではなく[テストモードで開始]が選択された[CloudFirestoreのセキュリティルール]ダイアログ。

[プロジェクトの概要]→[プロジェクトの設定]に移動し、FirebaseWeb設定をローカルのenvironments/environment.tsにコピーします。

新しいFirebaseプロジェクトの空のアプリリスト。

 export const environment = { production: false, firebase: { apiKey: "<api-key>", authDomain: "<auth-domain>", databaseURL: "<database-url>", projectId: "<project-id>", storageBucket: "<storage-bucket>", messagingSenderId: "<messaging-sender-id>" } };

この時点で、アプリの基本的なスキャフォールドが用意されています。 ng serveと、次のようになります。

「従業員へようこそ-管理者!」と言っているAngularの足場。

ファイヤーストアとストアの基本クラス

2つの汎用抽象クラスを作成し、それらを入力して拡張し、サービスを構築します。

ジェネリックスを使用すると、バインドされた型なしで動作を記述できます。 これにより、コードに再利用性と柔軟性が追加されます。

ジェネリックFirestoreサービス

TypeScriptジェネリックを利用するために、 @angular/fire firestoreサービスのベースジェネリックラッパーを作成します。

app/core/services/firestore.service.tsを作成しましょう。

コードは次のとおりです。

 import { Inject } from "@angular/core"; import { AngularFirestore, QueryFn } from "@angular/fire/firestore"; import { Observable } from "rxjs"; import { tap } from "rxjs/operators"; import { environment } from "src/environments/environment"; export abstract class FirestoreService<T> { protected abstract basePath: string; constructor( @Inject(AngularFirestore) protected firestore: AngularFirestore, ) { } doc$(id: string): Observable<T> { return this.firestore.doc<T>(`${this.basePath}/${id}`).valueChanges().pipe( tap(r => { if (!environment.production) { console.groupCollapsed(`Firestore Streaming [${this.basePath}] [doc$] ${id}`) console.log(r) console.groupEnd() } }), ); } collection$(queryFn?: QueryFn): Observable<T[]> { return this.firestore.collection<T>(`${this.basePath}`, queryFn).valueChanges().pipe( tap(r => { if (!environment.production) { console.groupCollapsed(`Firestore Streaming [${this.basePath}] [collection$]`) console.table(r) console.groupEnd() } }), ); } create(value: T) { const id = this.firestore.createId(); return this.collection.doc(id).set(Object.assign({}, { id }, value)).then(_ => { if (!environment.production) { console.groupCollapsed(`Firestore Service [${this.basePath}] [create]`) console.log('[Id]', id, value) console.groupEnd() } }) } delete(id: string) { return this.collection.doc(id).delete().then(_ => { if (!environment.production) { console.groupCollapsed(`Firestore Service [${this.basePath}] [delete]`) console.log('[Id]', id) console.groupEnd() } }) } private get collection() { return this.firestore.collection(`${this.basePath}`); } }

このabstract classは、Firestoreサービスのジェネリックラッパーとして機能します。

これは、 AngularFirestoreを注入する必要がある唯一の場所である必要があります。 これにより、 @angular/fireライブラリが更新されたときの影響が最小限に抑えられます。 また、ある時点でライブラリを変更したい場合は、このクラスを更新するだけで済みます。

doc$collection$createdeleteを追加しました。 これらは@angular/fireのメソッドをラップし、Firebaseがデータをストリーミングするとき(これはデバッグに非常に便利です)、およびオブジェクトが作成または削除された後にログを提供します。

ジェネリックストアサービス

ジェネリックストアサービスは、RxJSのBehaviorSubjectを使用して構築されます。 BehaviorSubjectを使用すると、サブスクライバーは、サブスクライブするとすぐに最後に発行された値を取得できます。 私たちの場合、これは、すべてのコンポーネントがストアにサブスクライブするときに、すべてのコンポーネントの初期値でストアを開始できるため、役立ちます。

ストアには、 patchsetの2つのメソッドがあります。 ( getメソッドは後で作成します。)

app/core/services/store.service.tsを作成しましょう:

 import { BehaviorSubject, Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; export abstract class StoreService<T> { protected bs: BehaviorSubject<T>; state$: Observable<T>; state: T; previous: T; protected abstract store: string; constructor(initialValue: Partial<T>) { this.bs = new BehaviorSubject<T>(initialValue as T); this.state$ = this.bs.asObservable(); this.state = initialValue as T; this.state$.subscribe(s => { this.state = s }) } patch(newValue: Partial<T>, event: string = "Not specified") { this.previous = this.state const newState = Object.assign({}, this.state, newValue); if (!environment.production) { console.groupCollapsed(`[${this.store} store] [patch] [event: ${event}]`) console.log("change", newValue) console.log("prev", this.previous) console.log("next", newState) console.groupEnd() } this.bs.next(newState) } set(newValue: Partial<T>, event: string = "Not specified") { this.previous = this.state const newState = Object.assign({}, newValue) as T; if (!environment.production) { console.groupCollapsed(`[${this.store} store] [set] [event: ${event}]`) console.log("change", newValue) console.log("prev", this.previous) console.log("next", newState) console.groupEnd() } this.bs.next(newState) } }

ジェネリッククラスとして、適切に拡張されるまで入力を延期します。

コンストラクターは、 Partial<T>型の初期値を受け取ります。 これにより、状態の一部のプロパティにのみ値を適用できるようになります。 コンストラクターは、内部のBehaviorSubjectエミッションもサブスクライブし、変更のたびに内部状態を更新し続けます。

patch()Partial<T>タイプのnewValueを受け取り、それをストアの現在のthis.state値とマージします。 最後に、 next() newStateを実行し、すべてのストアサブスクライバーに新しい状態を送信します。

set()は非常によく似ていますが、状態値にパッチを適用する代わりに、受け取ったnewValueに設定します。

変更が発生したときに状態の前の値と次の値をログに記録します。これにより、状態の変更をデバッグして簡単に追跡できます。

すべてを一緒に入れて

さて、これらすべての動作を見てみましょう。 従業員のリストと新しい従業員を追加するためのフォームを含む従業員ページを作成します。

app.component.htmlを更新して、簡単なナビゲーションバーを追加しましょう。

 <nav class="navbar navbar-expand-lg navbar-light bg-light mb-3"> <span class="navbar-brand mb-0 h1">Angular + Firebase + State Management</span> <ul class="navbar-nav mr-auto"> <li class="nav-item" [routerLink]="['/employees']" routerLinkActive="active"> <a class="nav-link">Employees</a> </li> </ul> </nav> <router-outlet></router-outlet>

次に、コアモジュールを作成します。

 ng gm Core

core/core.module.tsに、アプリに必要なモジュールを追加します。

 // ... import { AngularFireModule } from '@angular/fire' import { AngularFirestoreModule } from '@angular/fire/firestore' import { environment } from 'src/environments/environment'; import { ReactiveFormsModule } from '@angular/forms' @NgModule({ // ... imports: [ // ... AngularFireModule.initializeApp(environment.firebase), AngularFirestoreModule, ReactiveFormsModule, ], exports: [ CommonModule, AngularFireModule, AngularFirestoreModule, ReactiveFormsModule ] }) export class CoreModule { }

それでは、Employeesモジュールから始めて、employeesページを作成しましょう。

 ng gm Employees --routing

employees-routing.module.tsに、 employeesルートを追加しましょう。

 // ... import { EmployeesPageComponent } from './components/employees-page/employees-page.component'; // ... const routes: Routes = [ { path: 'employees', component: EmployeesPageComponent } ]; // ...

そしてemployees.module.tsReactiveFormsModuleをインポートします:

 // ... import { ReactiveFormsModule } from '@angular/forms'; // ... @NgModule({ // ... imports: [ // ... ReactiveFormsModule ] }) export class EmployeesModule { }

次に、これら2つのモジュールをapp.module.tsファイルに追加しましょう。

 // ... import { EmployeesModule } from './employees/employees.module'; import { CoreModule } from './core/core.module'; imports: [ // ... CoreModule, EmployeesModule ],

最後に、従業員ページの実際のコンポーネントに加えて、対応するモデル、サービス、ストア、および状態を作成しましょう。

 ng gc employees/components/EmployeesPage ng gc employees/components/EmployeesList ng gc employees/components/EmployeesForm

このモデルでは、 models/employee.tsというファイルが必要です。

 export interface Employee { id: string; name: string; location: string; hasDriverLicense: boolean; }

私たちのサービスは、 employees/services/employee.firestore.tsというファイルに保存されます。 このサービスは、以前に作成された汎用FirestoreService<T>を拡張し、FirestoreコレクションのbasePathを設定するだけです。

 import { Injectable } from '@angular/core'; import { FirestoreService } from 'src/app/core/services/firestore.service'; import { Employee } from '../models/employee'; @Injectable({ providedIn: 'root' }) export class EmployeeFirestore extends FirestoreService<Employee> { protected basePath: string = 'employees'; }

次に、ファイルemployees/states/employees-page.tsを作成します。 これは、従業員ページの状態として機能します。

 import { Employee } from '../models/employee'; export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; }

状態には、ページに読み込みメッセージを表示するかどうかを決定するloading値、 employees自身、およびフォームのステータス( SavingまたはSavedなど)を処理するためのformStatus変数が含まれます。

employees/services/employees-page.store.tsにファイルが必要です。 ここでは、前に作成したStoreService<T>を拡張します。 ストア名を設定します。これは、デバッグ時にストアを識別するために使用されます。

このサービスは、従業員ページの状態を初期化して保持します。 コンストラクターがページの初期状態でsuper()を呼び出すことに注意してください。 この場合、 loading=trueと空のemployees配列を使用して状態を初期化します。

 import { EmployeesPage } from '../states/employees-page'; import { StoreService } from 'src/app/core/services/store.service'; import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class EmployeesPageStore extends StoreService<EmployeesPage> { protected store: string = 'employees-page'; constructor() { super({ loading: true, employees: [], }) } }

それでは、 EmployeesPageStoreを作成して、 EmployeeFirestoreEmployeesServiceを統合しましょう。

 ng gs employees/services/Employees

このサービスにEmployeeFirestoreEmployeesPageStoreを注入していることに注意してください。 これは、 EmployeesServiceがFirestoreとストアへの呼び出しを含み、調整して状態を更新することを意味します。 これは、コンポーネントが呼び出す単一のAPIを作成するのに役立ちます。

 import { EmployeesPageStore } from './employees-page.store'; import { EmployeeFirestore } from './employee.firestore'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Employee } from '../models/employee'; import { tap, map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class EmployeesService { constructor( private firestore: EmployeeFirestore, private store: EmployeesPageStore ) { this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, }, `employees collection subscription`) }) ).subscribe() } get employees$(): Observable<Employee[]> { return this.store.state$.pipe(map(state => state.loading ? [] : state.employees)) } get loading$(): Observable<boolean> { return this.store.state$.pipe(map(state => state.loading)) } get noResults$(): Observable<boolean> { return this.store.state$.pipe( map(state => { return !state.loading && state.employees && state.employees.length === 0 }) ) } get formStatus$(): Observable<string> { return this.store.state$.pipe(map(state => state.formStatus)) } create(employee: Employee) { this.store.patch({ loading: true, employees: [], formStatus: 'Saving...' }, "employee create") return this.firestore.create(employee).then(_ => { this.store.patch({ formStatus: 'Saved!' }, "employee create SUCCESS") setTimeout(() => this.store.patch({ formStatus: '' }, "employee create timeout reset formStatus"), 2000) }).catch(err => { this.store.patch({ loading: false, formStatus: 'An error ocurred' }, "employee create ERROR") }) } delete(id: string): any { this.store.patch({ loading: true, employees: [] }, "employee delete") return this.firestore.delete(id).catch(err => { this.store.patch({ loading: false, formStatus: 'An error ocurred' }, "employee delete ERROR") }) } }

サービスがどのように機能するかを見てみましょう。

コンストラクターでは、Firestoreの従業員コレクションをサブスクライブします。 Firestoreがコレクションからデータを出力するとすぐに、ストアを更新し、 loading=falseを設定して、Firestoreの返されたコレクションを使用してemployeesを設定します。 EmployeeFirestoreを挿入したため、Firestoreから返されるオブジェクトはEmployeeに入力されます。これにより、より多くのIntelliSense機能が有効になります。

このサブスクリプションは、アプリがアクティブである間有効であり、Firestoreがデータをストリーミングするたびに、すべての変更をリッスンし、ストアを更新します。

 this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, }, `employees collection subscription`) }) ).subscribe()

employees$() )関数とloading$()関数は、後でコンポーネントで使用する状態を選択します。 状態がロードされているとき、 employees$()は空の配列を返します。 これにより、ビューに適切なメッセージを表示できるようになります。

 get employees$(): Observable<Employee[]> { return this.store.state$.pipe(map(state => state.loading ? [] : state.employees)) } get loading$(): Observable<boolean> { return this.store.state$.pipe(map(state => state.loading)) }

これで、すべてのサービスの準備が整い、ビューコンポーネントを構築できるようになりました。 しかし、それを行う前に、簡単な復習が役立つかもしれません…

RxJsObservablesとasyncパイプ

オブザーバブルを使用すると、サブスクライバーはデータの放出をストリームとして受信できます。 これは、 asyncパイプと組み合わせると、非常に強力になります。

asyncパイプは、Observableのサブスクライブと、新しいデータが発行されたときのビューの更新を処理します。 さらに重要なことに、コンポーネントが破棄されると自動的にサブスクライブを解除し、メモリリークから保護します。

ObservablesとRxJsライブラリの詳細については、公式ドキュメントをご覧ください。

ビューコンポーネントの作成

employees/components/employees-page/employees-page.component.htmlに、次のコードを配置します。

 <div class="container"> <div class="row"> <div class="col-12 mb-3"> <h4> Employees </h4> </div> </div> <div class="row"> <div class="col-6"> <app-employees-list></app-employees-list> </div> <div class="col-6"> <app-employees-form></app-employees-form> </div> </div> </div>

同様に、 employees/components/employees-list/employees-list.component.htmlは、上記のasyncパイプ手法を使用してこれを取得します。

 <div *ngIf="loading$ | async"> Loading... </div> <div *ngIf="noResults$ | async"> No results </div> <div class="card bg-light mb-3" *ngFor="let employee of employees$ | async"> <div class="card-header">{{employee.location}}</div> <div class="card-body"> <h5 class="card-title">{{employee.name}}</h5> <p class="card-text">{{employee.hasDriverLicense ? 'Can drive': ''}}</p> <button (click)="delete(employee)" class="btn btn-danger">Delete</button> </div> </div>

ただし、この場合、コンポーネント用のTypeScriptコードも必要になります。 ファイルemployees/components/employees-list/employees-list.component.tsには、次のものが必要です。

 import { Employee } from '../../models/employee'; import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { EmployeesService } from '../../services/employees.service'; @Component({ selector: 'app-employees-list', templateUrl: './employees-list.component.html', styleUrls: ['./employees-list.component.scss'] }) export class EmployeesListComponent implements OnInit { loading$: Observable<boolean>; employees$: Observable<Employee[]>; noResults$: Observable<boolean>; constructor( private employees: EmployeesService ) {} ngOnInit() { this.loading$ = this.employees.loading$; this.noResults$ = this.employees.noResults$; this.employees$ = this.employees.employees$; } delete(employee: Employee) { this.employees.delete(employee.id); } }

したがって、ブラウザに移動すると、次のようになります。

空の従業員リスト、および「employees-formworks!」というメッセージ。

また、コンソールには次の出力があります。

変更前と変更後の値を示すパッチイベント。

これを見ると、Firestoreがemployeesコレクションを空の値でストリーミングし、 employees-pageストアにパッチが適用され、 loadingtrueからfalseに設定されていることがわかります。

OK、Firestoreに新しい従業員を追加するためのフォームを作成しましょう。

従業員フォーム

employees/components/employees-form/employees-form.component.htmlに、次のコードを追加します。

 <form [formGroup]="form" (ngSubmit)="submit()"> <div class="form-group"> <label for="name">Name</label> <input type="string" class="form-control" formControlName="name" [class.is-invalid]="isInvalid('name')"> <div class="invalid-feedback"> Please enter a Name. </div> </div> <div class="form-group"> <select class="custom-select" formControlName="location" [class.is-invalid]="isInvalid('location')"> <option value="" selected>Choose location</option> <option *ngFor="let loc of locations" [ngValue]="loc">{{loc}}</option> </select> <div class="invalid-feedback"> Please select a Location. </div> </div> <div class="form-group form-check"> <input type="checkbox" class="form-check-input" formControlName="hasDriverLicense"> <label class="form-check-label" for="hasDriverLicense">Has driver license</label> </div> <button [disabled]="form.invalid" type="submit" class="btn btn-primary d-inline">Add</button> <span class="ml-2">{{ status$ | async }}</span> </form>

対応するTypeScriptコードはemployees/components/employees-form/employees-form.component.tsます:

 import { EmployeesService } from './../../services/employees.service'; import { AngularFirestore } from '@angular/fire/firestore'; import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; import { Observable } from 'rxjs'; @Component({ selector: 'app-employees-form', templateUrl: './employees-form.component.html', styleUrls: ['./employees-form.component.scss'] }) export class EmployeesFormComponent implements OnInit { form: FormGroup = new FormGroup({ name: new FormControl('', Validators.required), location: new FormControl('', Validators.required), hasDriverLicense: new FormControl(false) }); locations = [ 'Rosario', 'Buenos Aires', 'Bariloche' ] status$: Observable < string > ; constructor( private employees: EmployeesService ) {} ngOnInit() { this.status$ = this.employees.formStatus$; } isInvalid(name) { return this.form.controls[name].invalid && (this.form.controls[name].dirty || this.form.controls[name].touched) } async submit() { this.form.disable() await this.employees.create({ ...this.form.value }) this.form.reset() this.form.enable() } }

フォームは、 EmployeesServicecreate()メソッドを呼び出します。 現在、ページは次のようになっています。

以前と同じ空の従業員リストですが、今回は新しい従業員を追加するためのフォームがあります。

新しい従業員を追加するとどうなるか見てみましょう。

新しい従業員の追加

新しい従業員を追加すると、次のget出力がコンソールに表示されます。

1から6までの番号が付けられたFirestoreイベントと混合されたパッチイベント(ローカル作成、Firestoreコレクションストリーミング、ローカルコレクションサブスクリプション、Firestore作成、ローカル作成の成功、およびローカル作成タイムアウトフォームのステータスリセット)。

これらはすべて、新しい従業員を追加するときにトリガーされるイベントです。 よく見てみましょう。

create()を呼び出すと、次のコードを実行し、 loading=trueformStatus='Saving...'を設定し、 employees配列を空にします(上の画像の(1) )。

 this.store.patch({ loading: true, employees: [], formStatus: 'Saving...' }, "employee create") return this.firestore.create(employee).then(_ => { this.store.patch({ formStatus: 'Saved!' }, "employee create SUCCESS") setTimeout(() => this.store.patch({ formStatus: '' }, "employee create timeout reset formStatus"), 2000) }).catch(err => { this.store.patch({ loading: false, formStatus: 'An error ocurred' }, "employee create ERROR") })

次に、ベースのFirestoreサービスを呼び出して、 (4)をログに記録する従業員を作成します。 promiseコールバックで、 formStatus='Saved!'を設定します。 およびログ(5) 。 最後に、タイムアウトを設定してformStatusを空に戻し、logging (6)します。

ログイベント(2)および(3)は、従業員コレクションへのFirestoreサブスクリプションによってトリガーされるイベントです。 EmployeesServiceがインスタンス化されると、コレクションをサブスクライブし、変更が発生するたびにコレクションを受け取ります。

これにより、 employees配列をFirestoreからの従業員に設定することにより、 loading=falseのストアに新しい状態が設定されます。

ロググループを展開すると、すべてのイベントとストアの更新の詳細データが、デバッグに役立つ前の値と次の値とともに表示されます。

すべての状態管理の詳細が拡張された以前のログ出力。

新しい従業員を追加した後のページは次のようになります。

従業員カードが含まれている従業員リスト、およびフォームはそれを追加することからまだ記入されています。

サマリーコンポーネントの追加

ここで、ページにいくつかの要約データを表示したいとします。 従業員の総数、ドライバーの数、Rosarioの従業員の数が必要だとします。

まず、 employees/states/employees-page.tsのページ状態モデルに新しい状態プロパティを追加します。

 // ... export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; totalEmployees: number; totalDrivers: number; totalRosarioEmployees: number; }

そして、 employees/services/emplyees-page.store.tsのストアでそれらを初期化します。

 // ... constructor() { super({ loading: true, employees: [], totalDrivers: 0, totalEmployees: 0, totalRosarioEmployees: 0 }) } // ...

次に、新しいプロパティの値を計算し、それぞれのセレクターをEmployeesServiceに追加します。

 // ... this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, totalEmployees: employees.length, totalDrivers: employees.filter(employee => employee.hasDriverLicense).length, totalRosarioEmployees: employees.filter(employee => employee.location === 'Rosario').length, }, `employees collection subscription`) }) ).subscribe() // ... get totalEmployees$(): Observable < number > { return this.store.state$.pipe(map(state => state.totalEmployees)) } get totalDrivers$(): Observable < number > { return this.store.state$.pipe(map(state => state.totalDrivers)) } get totalRosarioEmployees$(): Observable < number > { return this.store.state$.pipe(map(state => state.totalRosarioEmployees)) } // ...

それでは、サマリーコンポーネントを作成しましょう。

 ng gc employees/components/EmployeesSummary

これをemployees/components/employees-summary/employees-summary.html配置します:

 <p> <span class="font-weight-bold">Total:</span> {{total$ | async}} <br> <span class="font-weight-bold">Drivers:</span> {{drivers$ | async}} <br> <span class="font-weight-bold">Rosario:</span> {{rosario$ | async}} <br> </p>

そして、 employees/components/employees-summary/employees-summary.ts

 import { Component, OnInit } from '@angular/core'; import { EmployeesService } from '../../services/employees.service'; import { Observable } from 'rxjs'; @Component({ selector: 'app-employees-summary', templateUrl: './employees-summary.component.html', styleUrls: ['./employees-summary.component.scss'] }) export class EmployeesSummaryComponent implements OnInit { total$: Observable < number > ; drivers$: Observable < number > ; rosario$: Observable < number > ; constructor( private employees: EmployeesService ) {} ngOnInit() { this.total$ = this.employees.totalEmployees$; this.drivers$ = this.employees.totalDrivers$; this.rosario$ = this.employees.totalRosarioEmployees$; } }

次に、コンポーネントをemployees/employees-page/employees-page.component.htmlに追加します。

 // ... <div class="col-12 mb-3"> <h4> Employees </h4> <app-employees-summary></app-employees-summary> </div> // ...

結果は次のとおりです。

従業員ページ。リストの上に要約が表示され、総従業員数、運転手である従業員、およびロザリオ出身の従業員の数が表示されます。

コンソールには次のものがあります。

サマリー値を変更するパッチイベントを示すコンソール出力。

従業員サービスは、各排出量の合計totalEmployeestotalDrivers 、およびtotalRosarioEmployeesを計算し、状態を更新します。

このチュートリアルの完全なコードはGitHubで入手でき、ライブデモもあります。

Observablesを使用したAngularアプリの状態の管理…チェックしてください!

このチュートリアルでは、Firebaseバックエンドを使用してAngularアプリの状態を管理するための簡単なアプローチについて説明しました。

このアプローチは、Observablesを使用するAngularガイドラインにうまく適合します。 また、アプリの状態に対するすべての更新の追跡を提供することにより、デバッグを容易にします。

ジェネリックストアサービスを使用して、Firebase機能を使用しないアプリの状態を管理することもできます。これは、アプリのデータのみ、または他のAPIからのデータを管理するためです。

ただし、これを無差別に適用する前に、考慮すべきことの1つは、 EmployeesServiceがコンストラクターでFirestoreにサブスクライブし、アプリがアクティブである間リッスンし続けることです。 これは、アプリの複数のページで従業員リストを使用して、ページ間を移動するときにFirestoreからデータを取得しないようにする場合に役立つことがあります。

ただし、これは、初期値を1回プルしてから、Firebaseからデータのリロードを手動でトリガーする必要がある場合など、他のシナリオでは最適なオプションではない可能性があります。 肝心なのは、より良い実装方法を選択するためには、アプリの要件を理解することが常に重要です。