Zustandsverwaltung in Angular mit Firebase

Veröffentlicht: 2022-03-11

Die Zustandsverwaltung ist ein sehr wichtiger Teil der Architektur, der bei der Entwicklung einer Web-App berücksichtigt werden muss.

In diesem Tutorial gehen wir einen einfachen Ansatz zur Statusverwaltung in einer Angular-Anwendung durch, die Firebase als Backend verwendet.

Wir werden einige Konzepte wie Zustand, Geschäfte und Dienstleistungen durchgehen. Hoffentlich hilft Ihnen dies dabei, diese Begriffe besser zu verstehen und auch andere Zustandsverwaltungsbibliotheken wie NgRx und NgXs besser zu verstehen.

Wir werden eine Mitarbeiterverwaltungsseite erstellen, um einige verschiedene Zustandsverwaltungsszenarien und die Ansätze, die damit umgehen können, abzudecken.

Komponenten, Dienste, Firestore und Zustandsverwaltung in Angular

Bei einer typischen Angular-Anwendung haben wir Komponenten und Dienste. Normalerweise dienen Komponenten als Ansichtsvorlage. Dienste enthalten Geschäftslogik und/oder kommunizieren mit externen APIs oder anderen Diensten, um Aktionen auszuführen oder Daten abzurufen.

Komponenten stellen eine Verbindung zu Diensten her, die eine Verbindung zu anderen Diensten oder HTTP-APIs herstellen.

Komponenten zeigen normalerweise Daten an und ermöglichen es Benutzern, mit der App zu interagieren, um Aktionen auszuführen. Dabei können sich Daten ändern und die App spiegelt diese Änderungen wider, indem sie die Ansicht aktualisiert.

Die Änderungserkennungs-Engine von Angular prüft, wann sich ein Wert in einer an die Ansicht gebundenen Komponente geändert hat, und aktualisiert die Ansicht entsprechend.

Wenn die App wächst, werden wir immer mehr Komponenten und Dienste haben. Oft kann es schwierig sein, zu verstehen, wie sich Daten ändern, und zu verfolgen, wo dies geschieht.

Winkel und Firebase

Wenn wir Firebase als unser Backend verwenden, erhalten wir eine wirklich übersichtliche API, die die meisten Operationen und Funktionen enthält, die wir zum Erstellen einer Echtzeitanwendung benötigen.

@angular/fire ist die offizielle Angular Firebase-Bibliothek. Es ist eine Ebene über der Firebase JavaScript SDK-Bibliothek, die die Verwendung des Firebase SDK in einer Angular-App vereinfacht. Es passt gut zu bewährten Praktiken von Angular, wie z. B. die Verwendung von Observables zum Abrufen und Anzeigen von Daten von Firebase für unsere Komponenten.

Komponenten abonnieren die Firebase-JavaScript-API über @angular/fire mit Observables.

Geschäfte und Staat

Wir können uns „Status“ als die Werte vorstellen, die zu einem bestimmten Zeitpunkt in der App angezeigt werden. Das Geschäft ist einfach der Inhaber dieses Anwendungsstatus.

State kann als einzelnes einfaches Objekt oder als eine Reihe von Objekten modelliert werden, die die Werte der Anwendung widerspiegeln.

Ein Store Holding State, der ein Beispielobjekt mit einigen einfachen Schlüsselwertpaaren für Name, Stadt und Land enthält.

Beispiel-App für Angular/Firebase

Lassen Sie es uns erstellen: Zuerst erstellen wir ein einfaches App-Gerüst mit Angular CLI und verbinden es mit einem Firebase-Projekt.

 $ 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

Und auf styles.scss :

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

Als nächstes installieren wir @angular/fire :

 npm install firebase @angular/fire

Jetzt erstellen wir ein Firebase-Projekt in der Firebase-Konsole.

Das Dialogfeld „Projekt hinzufügen“ der Firebase-Konsole.

Dann können wir eine Firestore-Datenbank erstellen.

Für dieses Tutorial beginne ich im Testmodus. Wenn Sie eine Freigabe für die Produktion planen, sollten Sie Regeln durchsetzen, um unangemessenen Zugriff zu verbieten.

Das Dialogfeld "Sicherheitsregeln für Cloud Firestore", in dem "Im Testmodus starten" anstelle von "Im gesperrten Modus starten" ausgewählt ist.

Gehen Sie zu Projektübersicht → Projekteinstellungen und kopieren Sie die Firebase-Webkonfiguration in Ihre lokale environments/environment.ts .

Leere App-Liste für das neue Firebase-Projekt.

 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>" } };

An diesem Punkt haben wir das grundlegende Gerüst für unsere App eingerichtet. Wenn wir ng serve , bekommen wir:

Das Angular Scaffold sagt "Willkommen bei Mitarbeiter-Admin!"

Firestore- und Store-Basisklassen

Wir erstellen zwei generische abstrakte Klassen, die wir dann eingeben und erweitern, um unsere Dienste zu erstellen.

Mit Generika können Sie Verhalten ohne gebundenen Typ schreiben. Dies verleiht Ihrem Code Wiederverwendbarkeit und Flexibilität.

Allgemeiner Firestore-Dienst

Um die TypeScript-Generika zu nutzen, erstellen wir einen grundlegenden generischen Wrapper für den firestore -Dienst @angular/fire .

Lassen Sie uns app/core/services/firestore.service.ts erstellen.

Hier ist der Code:

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

Diese abstract class fungiert als generischer Wrapper für unsere Firestore-Dienste.

Dies sollte der einzige Ort sein, an dem wir AngularFirestore injizieren sollten. Dadurch werden die Auswirkungen minimiert, wenn die @angular/fire Bibliothek aktualisiert wird. Wenn wir irgendwann die Bibliothek ändern möchten, müssen wir nur diese Klasse aktualisieren.

Ich habe doc$ , collection$ , create und delete hinzugefügt. Sie umschließen die Methoden von @angular/fire und bieten eine Protokollierung, wenn Firebase Daten streamt – dies wird für das Debugging sehr praktisch – und nachdem ein Objekt erstellt oder gelöscht wurde.

Generika-Shop-Service

Unser generischer Store-Service wird mit BehaviorSubject von RxJS erstellt. BehaviorSubject erhalten Abonnenten den zuletzt ausgegebenen Wert, sobald sie sich anmelden. In unserem Fall ist dies hilfreich, da wir den Shop mit einem Anfangswert für alle unsere Komponenten starten können, wenn sie den Shop abonnieren.

Der Store wird zwei Methoden haben, patch und set . ( get Methoden erstellen wir später.)

Lassen Sie uns 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) } }

Als generische Klasse verschieben wir die Eingabe, bis sie richtig erweitert ist.

Der Konstruktor erhält den Anfangswert vom Typ Partial<T> . Dadurch können wir Werte nur auf einige Eigenschaften des Zustands anwenden. Der Konstruktor abonniert auch die internen BehaviorSubject -Emissionen und hält den internen Status nach jeder Änderung auf dem neuesten Stand.

patch() erhält den newValue vom Typ Partial<T> und führt ihn mit dem aktuellen this.state Wert des Stores zusammen. Schließlich next() den newState und geben den neuen Zustand an alle Store-Abonnenten aus.

set() funktioniert sehr ähnlich, nur dass es den state-Wert nicht patcht, sondern auf den newValue .

Wir protokollieren die vorherigen und nächsten Werte des Zustands, wenn Änderungen auftreten, was uns beim Debuggen und einfachen Nachverfolgen von Zustandsänderungen hilft.

Alles zusammenfügen

Okay, sehen wir uns das alles mal in Aktion an. Wir erstellen eine Mitarbeiterseite, die eine Liste der Mitarbeiter sowie ein Formular zum Hinzufügen neuer Mitarbeiter enthält.

Aktualisieren app.component.html , um eine einfache Navigationsleiste hinzuzufügen:

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

Als Nächstes erstellen wir ein Core-Modul:

 ng gm Core

In core/core.module.ts fügen wir die für unsere App erforderlichen Module hinzu:

 // ... 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 { }

Lassen Sie uns nun die Mitarbeiterseite erstellen, beginnend mit dem Mitarbeitermodul:

 ng gm Employees --routing

Fügen wir in employees-routing.module.ts die employees hinzu:

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

Und in employees.module.ts importieren wir ReactiveFormsModule :

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

Fügen wir nun diese beiden Module zur Datei app.module.ts :

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

Lassen Sie uns abschließend die eigentlichen Komponenten unserer Mitarbeiterseite sowie das entsprechende Modell, den Dienst, das Geschäft und den Status erstellen.

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

Für unser Modell benötigen wir eine Datei namens models/employee.ts :

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

Unser Dienst befindet sich in einer Datei namens employees/services/employee.firestore.ts . Dieser Dienst erweitert den zuvor erstellten generischen FirestoreService<T> , und wir legen einfach den basePath der Firestore-Sammlung fest:

 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'; }

Dann erstellen wir die Datei employees/states/employees-page.ts . Dies dient als Status der Mitarbeiterseite:

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

Der Status hat einen loading , der bestimmt, ob eine Lademeldung auf der Seite angezeigt wird, die employees selbst und eine formStatus Variable, um den Status des Formulars zu behandeln (z. B. Saving oder Saved .)

Wir benötigen eine Datei unter employees/services/employees-page.store.ts . Hier erweitern wir den zuvor erstellten StoreService<T> . Wir legen den Speichernamen fest, der zur Identifizierung beim Debuggen verwendet wird.

Dieser Dienst initialisiert und hält den Status der Mitarbeiterseite. Beachten Sie, dass der Konstruktor super() mit dem Anfangszustand der Seite aufruft. In diesem Fall initialisieren wir den Zustand mit loading=true und einem leeren Array von Angestellten.

 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: [], }) } }

Lassen Sie uns nun EmployeesPageStore erstellen, um EmployeeFirestore und EmployeesService zu integrieren:

 ng gs employees/services/Employees

Beachten Sie, dass wir EmployeeFirestore und EmployeesPageStore in diesen Dienst einfügen. Das bedeutet, dass der EmployeesService Aufrufe an Firestore und den Store enthält und koordiniert, um den Status zu aktualisieren. Dies wird uns helfen, eine einzelne API für die aufzurufenden Komponenten zu erstellen.

 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") }) } }

Werfen wir einen Blick darauf, wie der Dienst funktionieren wird.

Im Konstruktor abonnieren wir die Mitarbeitersammlung von Firestore. Sobald Firestore Daten aus der Sammlung ausgibt, aktualisieren wir den Speicher, setzen loading=false und employees mit der zurückgegebenen Sammlung von Firestore. Da wir EmployeeFirestore eingefügt haben, werden die von Firestore zurückgegebenen Objekte in Employee typisiert, wodurch weitere IntelliSense-Funktionen aktiviert werden.

Dieses Abonnement bleibt aktiv, während die App aktiv ist, auf alle Änderungen lauscht und den Store jedes Mal aktualisiert, wenn Firestore Daten streamt.

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

Die Funktionen employees$() und loading$() wählen den Zustand aus, den wir später für die Komponente verwenden möchten. employees$() gibt ein leeres Array zurück, wenn der Status geladen wird. Dadurch können wir in der Ansicht die richtigen Nachrichten anzeigen.

 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)) }

Okay, jetzt haben wir alle Dienste fertig und können unsere Ansichtskomponenten erstellen. Aber bevor wir das tun, könnte eine kurze Auffrischung nützlich sein …

RxJs Observables und die async Pipe

Observables ermöglichen Abonnenten, Emissionen von Daten als Stream zu empfangen. Dies kann in Kombination mit der async Pipe sehr leistungsfähig sein.

Die async -Pipe kümmert sich um das Abonnieren eines Observable und aktualisiert die Ansicht, wenn neue Daten ausgegeben werden. Noch wichtiger ist, dass es sich automatisch abmeldet, wenn die Komponente zerstört wird, und uns so vor Speicherlecks schützt.

Sie können mehr über Observables und die RxJs-Bibliothek im Allgemeinen in den offiziellen Dokumenten lesen.

Erstellen der Ansichtskomponenten

In employees/components/employees-page/employees-page.component.html wir diesen Code ein:

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

Ebenso wird employees/components/employees-list/employees-list.component.html dies haben, indem die oben erwähnte async -Pipe-Technik verwendet wird:

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

Aber in diesem Fall brauchen wir auch etwas TypeScript-Code für die Komponente. Die Datei employees/components/employees-list/employees-list.component.ts benötigt Folgendes:

 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); } }

Wenn wir also zum Browser gehen, haben wir jetzt Folgendes:

Eine leere Mitarbeiterliste und die Meldung "Mitarbeiterformular funktioniert!"

Und die Konsole hat die folgende Ausgabe:

Patch-Ereignisse, die Änderungen mit Vorher- und Nachher-Werten anzeigen.

Wenn wir uns das ansehen, können wir feststellen, dass Firestore die employees mit leeren Werten gestreamt hat und der employees-page gepatcht wurde, wobei das loading von true auf false wurde.

OK, erstellen wir das Formular, um neue Mitarbeiter zu Firestore hinzuzufügen:

Das Mitarbeiterformular

In employees/components/employees-form/employees-form.component.html fügen wir diesen Code hinzu:

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

Der entsprechende TypeScript-Code befindet sich in 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() } }

Das Formular ruft die Methode create() von EmployeesService auf. Im Moment sieht die Seite so aus:

Die gleiche leere Mitarbeiterliste wie zuvor, diesmal mit einem Formular zum Hinzufügen eines neuen Mitarbeiters.

Schauen wir uns an, was passiert, wenn wir einen neuen Mitarbeiter hinzufügen.

Hinzufügen eines neuen Mitarbeiters

Nachdem Sie einen neuen Mitarbeiter hinzugefügt haben, sehen wir die folgende Ausgabe auf der Konsole:

Patch-Ereignisse gemischt mit Firestore-Ereignissen, nummeriert von eins bis sechs (lokale Erstellung, Firestore-Sammlungs-Streaming, lokales Sammlungsabonnement, Firestore-Erstellung, lokaler Erstellungserfolg und lokales Erstellungs-Timeout-Formularstatus zurückgesetzt).

Dies sind alle Ereignisse, die beim Hinzufügen eines neuen Mitarbeiters ausgelöst werden. Lasst uns genauer hinschauen.

Wenn wir create() aufrufen, führen wir den folgenden Code aus, setzen loading=true , formStatus='Saving...' und das Array employees auf leer ( (1) im obigen Bild).

 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") })

Als Nächstes rufen wir den Firestore-Basisdienst auf, um den Mitarbeiter zu erstellen, der (4) protokolliert. Beim Promise-Callback setzen wir formStatus='Saved!' und Protokoll (5) . Schließlich setzen wir ein Timeout, um formStatus wieder auf leer zu setzen, Protokollierung (6) .

Protokollereignisse (2) und (3) sind die Ereignisse, die durch das Firestore-Abonnement der Mitarbeitersammlung ausgelöst werden. Wenn der EmployeesService instanziiert wird, abonnieren wir die Sammlung und erhalten die Sammlung bei jeder auftretenden Änderung.

Dies setzt einen neuen Status für das Geschäft mit loading=false , indem das Array employees auf die Mitarbeiter gesetzt wird, die von Firestore kommen.

Wenn wir die Protokollgruppen erweitern, sehen wir detaillierte Daten zu jedem Ereignis und jeder Aktualisierung des Speichers, mit dem vorherigen und dem nächsten Wert, was für das Debugging nützlich ist.

Die vorherige Protokollausgabe mit allen Statusverwaltungsdetails wurde erweitert.

So sieht die Seite nach dem Hinzufügen eines neuen Mitarbeiters aus:

Die Mitarbeiterliste mit einer Mitarbeiterkarte darin und dem noch ausgefüllten Formular durch das Hinzufügen.

Hinzufügen einer Zusammenfassungskomponente

Angenommen, wir möchten jetzt einige zusammenfassende Daten auf unserer Seite anzeigen. Angenommen, wir möchten die Gesamtzahl der Mitarbeiter, wie viele Fahrer und wie viele von Rosario.

Wir beginnen mit dem Hinzufügen der neuen Zustandseigenschaften zum Seitenzustandsmodell in employees/states/employees-page.ts :

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

Und wir initialisieren sie im Geschäft in employees/services/emplyees-page.store.ts :

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

Als Nächstes berechnen wir die Werte für die neuen Eigenschaften und fügen ihre jeweiligen Selektoren im EmployeesService hinzu:

 // ... 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)) } // ...

Lassen Sie uns nun die Zusammenfassungskomponente erstellen:

 ng gc employees/components/EmployeesSummary

Wir fügen dies in 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>

Und in 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$; } }

Wir fügen die Komponente dann zu employees/employees-page/employees-page.component.html :

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

Das Ergebnis ist folgendes:

Seite "Mitarbeiter", jetzt mit einer Zusammenfassung über der Liste, die die Anzahl der Mitarbeiter insgesamt, derjenigen, die Fahrer sind, und derjenigen, die aus Rosario stammen, anzeigt.

In der Konsole haben wir:

Konsolenausgabe, die ein Patch-Ereignis zeigt, das die Zusammenfassungswerte ändert.

Der Mitarbeiterdienst berechnet die Gesamtzahl totalEmployees , totalDrivers und totalRosarioEmployees für jede Emission und aktualisiert den Status.

Der vollständige Code dieses Tutorials ist auf GitHub verfügbar, und es gibt auch eine Live-Demo.

Verwalten des Angular-App-Zustands mithilfe von Observables ... Überprüfen Sie!

In diesem Tutorial haben wir einen einfachen Ansatz zum Verwalten des Status in Angular-Apps mit einem Firebase-Backend behandelt.

Dieser Ansatz passt gut zu den Angular-Richtlinien zur Verwendung von Observables. Es erleichtert auch das Debuggen, indem es die Nachverfolgung aller Aktualisierungen des Status der App ermöglicht.

Der generische Store-Dienst kann auch verwendet werden, um den Status von Apps zu verwalten, die keine Firebase-Funktionen verwenden, um entweder nur die Daten der App oder Daten zu verwalten, die von anderen APIs stammen.

Aber bevor Sie dies wahllos anwenden, sollten Sie bedenken, dass EmployeesService Firestore auf dem Konstruktor abonniert und weiterhin zuhört, während die App aktiv ist. Dies kann nützlich sein, wenn wir die Mitarbeiterliste auf mehreren Seiten in der App verwenden, um zu vermeiden, dass beim Navigieren zwischen den Seiten Daten von Firestore abgerufen werden.

Dies ist jedoch möglicherweise nicht die beste Option in anderen Szenarien, z. B. wenn Sie nur einmal Anfangswerte abrufen und dann das erneute Laden von Daten aus Firebase manuell auslösen müssen. Unter dem Strich ist es immer wichtig, die Anforderungen Ihrer App zu verstehen, um bessere Implementierungsmethoden zu wählen.