Gestione dello stato in Angular utilizzando Firebase

Pubblicato: 2022-03-11

La gestione dello stato è un elemento architettonico molto importante da considerare quando si sviluppa un'app Web.

In questo tutorial, esamineremo un approccio semplice per gestire lo stato in un'applicazione Angular che utilizza Firebase come back-end.

Esamineremo alcuni concetti come stato, negozi e servizi. Si spera che questo ti aiuti a comprendere meglio questi termini e anche a comprendere meglio altre librerie di gestione dello stato come NgRx e NgXs.

Creeremo una pagina di amministrazione dei dipendenti per coprire alcuni diversi scenari di gestione dello stato e gli approcci che possono gestirli.

Componenti, servizi, Firestore e gestione dello stato in Angular

In una tipica applicazione Angular abbiamo componenti e servizi. Di solito, i componenti fungeranno da modello di visualizzazione. I servizi conterranno la logica aziendale e/o comunicheranno con API esterne o altri servizi per completare azioni o recuperare dati.

I componenti si connettono ai servizi, che si connettono ad altri servizi o API HTTP.

I componenti di solito visualizzeranno i dati e consentiranno agli utenti di interagire con l'app per eseguire azioni. Durante questa operazione, i dati potrebbero cambiare e l'app riflette tali modifiche aggiornando la vista.

Il motore di rilevamento delle modifiche di Angular si occupa di controllare quando un valore in un componente associato alla vista è cambiato e aggiorna la vista di conseguenza.

Man mano che l'app cresce, inizieremo ad avere sempre più componenti e servizi. Capire spesso come stanno cambiando i dati e tenere traccia di dove ciò accade può essere complicato.

Angular e Firebase

Quando utilizziamo Firebase come back-end, ci viene fornita un'API davvero accurata che contiene la maggior parte delle operazioni e delle funzionalità di cui abbiamo bisogno per creare un'applicazione in tempo reale.

@angular/fire è la libreria ufficiale di Angular Firebase. È un livello sopra la libreria dell'SDK JavaScript Firebase che semplifica l'uso dell'SDK Firebase in un'app Angular. Si adatta bene alle buone pratiche di Angular come l'utilizzo di Observables per ottenere e visualizzare i dati da Firebase ai nostri componenti.

I componenti si iscrivono all'API JavaScript Firebase tramite @angular/fire utilizzando Observables.

Negozi e Stato

Possiamo pensare allo "stato" come ai valori visualizzati in un dato momento nell'app. Il negozio è semplicemente il titolare di quello stato della domanda.

Lo stato può essere modellato come un singolo oggetto semplice o una serie di essi, riflettendo i valori dell'applicazione.

Uno stato di conservazione del negozio, che ha un oggetto di esempio con alcune semplici coppie chiave-valore per nome, città e paese.

App di esempio angolare/Firebase

Costruiamolo: in primo luogo, creeremo uno scaffold per app di base utilizzando Angular CLI e lo collegheremo a un progetto 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

E, su styles.scss :

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

Successivamente, installeremo @angular/fire :

 npm install firebase @angular/fire

Ora creeremo un progetto Firebase sulla console Firebase.

La finestra di dialogo "Aggiungi un progetto" della console Firebase.

Quindi siamo pronti per creare un database Firestore.

Per questo tutorial, inizierò in modalità test. Se prevedi di rilasciare in produzione, dovresti applicare regole per vietare l'accesso inappropriato.

La finestra di dialogo "Regole di sicurezza per Cloud Firestore", con "Avvia in modalità test" selezionato invece di "Avvia in modalità bloccata".

Vai a Panoramica del progetto → Impostazioni del progetto e copia la configurazione web di Firebase nei tuoi environments/environment.ts locali.

Elenco delle app vuoto per il nuovo progetto 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>" } };

A questo punto, abbiamo l'impalcatura di base in atto per la nostra app. Se ng serve , otterremo:

L'impalcatura angolare, dicendo "Benvenuti in dipendenti-admin!"

Classi Firestore e Store Base

Creeremo due classi astratte generiche, che poi digiteremo ed estenderemo per costruire i nostri servizi.

I generici ti consentono di scrivere il comportamento senza un tipo associato. Questo aggiunge riusabilità e flessibilità al tuo codice.

Servizio Firestore generico

Per sfruttare i generici TypeScript, ciò che faremo è creare un wrapper generico di base per il servizio firestore @angular/fire .

Creiamo app/core/services/firestore.service.ts .

Ecco il codice:

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

Questa abstract class funzionerà come un wrapper generico per i nostri servizi Firestore.

Questo dovrebbe essere l'unico posto in cui dovremmo iniettare AngularFirestore . Ciò ridurrà al minimo l'impatto quando la libreria @angular/fire viene aggiornata. Inoltre, se a un certo punto vogliamo cambiare la libreria, dovremo solo aggiornare questa classe.

Ho aggiunto doc$ , collection$ , create e delete . Racchiudono i metodi di @angular/fire e forniscono la registrazione quando Firebase trasmette i dati (questo diventerà molto utile per il debug) e dopo che un oggetto è stato creato o eliminato.

Servizio negozio generico

Il nostro servizio di negozio generico verrà creato utilizzando BehaviorSubject di RxJS. BehaviorSubject consente agli abbonati di ottenere l'ultimo valore emesso non appena si iscrivono. Nel nostro caso, questo è utile perché saremo in grado di iniziare il negozio con un valore iniziale per tutti i nostri componenti quando si iscriveranno al negozio.

Il negozio avrà due metodi, patch e set . (Creeremo metodi get in seguito.)

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

Come classe generica, rinvieremo la digitazione fino a quando non sarà adeguatamente estesa.

Il costruttore riceverà il valore iniziale di tipo Partial<T> . Questo ci consentirà di applicare valori solo ad alcune proprietà dello stato. Il costruttore sottoscriverà anche le emissioni interne di BehaviorSubject e manterrà aggiornato lo stato interno dopo ogni modifica.

patch() riceverà il newValue di tipo Partial<T> e lo unirà al valore this.state corrente del negozio. Infine, eseguiamo next() the newState ed emettiamo il nuovo stato a tutti gli abbonati del negozio.

set() funziona in modo molto simile, solo che invece di correggere il valore di stato, lo newValue sul nuovo valore ricevuto.

Registreremo i valori precedenti e successivi dello stato al verificarsi delle modifiche, il che ci aiuterà a eseguire il debug e a tenere traccia facilmente delle modifiche di stato.

Mettere tutto insieme

Ok, vediamo tutto questo in azione. Quello che faremo è creare una pagina dei dipendenti, che conterrà un elenco di dipendenti, oltre a un modulo per aggiungere nuovi dipendenti.

Aggiorniamo app.component.html per aggiungere una semplice barra di navigazione:

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

Successivamente, creeremo un modulo Core:

 ng gm Core

In core/core.module.ts aggiungeremo i moduli necessari per la nostra app:

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

Ora creiamo la pagina dei dipendenti, partendo dal modulo Dipendenti:

 ng gm Employees --routing

In employees-routing.module.ts , aggiungiamo il percorso dei employees :

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

E in employees.module.ts importeremo ReactiveFormsModule :

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

Ora aggiungiamo questi due moduli nel file app.module.ts :

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

Infine, creiamo i componenti effettivi della pagina dei nostri dipendenti, oltre al modello, al servizio, al negozio e allo stato corrispondenti.

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

Per il nostro modello, avremo bisogno di un file chiamato models/employee.ts :

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

Il nostro servizio vivrà in un file chiamato employees/services/employee.firestore.ts . Questo servizio estenderà il generico FirestoreService<T> creato in precedenza e imposteremo semplicemente il basePath della raccolta Firestore:

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

Quindi creeremo il file employees/states/employees-page.ts . Questo servirà come pagina dello stato dei dipendenti:

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

Lo stato avrà un valore di loading che determina se visualizzare un messaggio di caricamento sulla pagina, i employees stessi e una variabile formStatus per gestire lo stato del modulo (es. Saving o Saved .)

Avremo bisogno di un file in employees/services/employees-page.store.ts . Qui estenderemo lo StoreService<T> creato in precedenza. Imposteremo il nome del negozio, che verrà utilizzato per identificarlo durante il debug.

Questo servizio inizializzerà e manterrà lo stato della pagina dei dipendenti. Nota che il costruttore chiama super() con lo stato iniziale della pagina. In questo caso, inizializzeremo lo stato con loading=true e un array vuoto di dipendenti.

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

Ora creiamo EmployeesService per integrare EmployeeFirestore e EmployeesPageStore :

 ng gs employees/services/Employees

Tieni presente che stiamo inserendo EmployeeFirestore e EmployeesPageStore in questo servizio. Ciò significa che EmployeesService conterrà e coordinerà le chiamate a Firestore e al negozio per aggiornare lo stato. Questo ci aiuterà a creare un'unica API per i componenti da chiamare.

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

Diamo un'occhiata a come funzionerà il servizio.

Nel costruttore, ci iscriveremo alla raccolta dei dipendenti di Firestore. Non appena Firestore emetterà i dati dalla raccolta, aggiorneremo il negozio, impostando loading=false e employees con la raccolta restituita da Firestore. Poiché abbiamo inserito EmployeeFirestore , gli oggetti restituiti da Firestore vengono digitati in Employee , che abilita più funzionalità di IntelliSense.

Questo abbonamento sarà attivo mentre l'app è attiva, ascoltando tutte le modifiche e aggiornando lo store ogni volta che Firestore trasmette i dati in streaming.

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

Le funzioni employees$() e loading$() selezioneranno il pezzo di stato che vogliamo utilizzare in seguito sul componente. employees$() restituirà un array vuoto durante il caricamento dello stato. Questo ci consentirà di visualizzare i messaggi corretti sulla vista.

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

Ok, ora abbiamo tutti i servizi pronti e possiamo creare i nostri componenti di visualizzazione. Ma prima di farlo, un rapido aggiornamento potrebbe tornare utile...

RxJs Observables e la pipe async

Gli osservabili consentono agli abbonati di ricevere emissioni di dati come flusso. Questo, in combinazione con il tubo async , può essere molto potente.

La pipe async si occupa della sottoscrizione di un Observable e dell'aggiornamento della vista quando vengono emessi nuovi dati. Ancora più importante, annulla automaticamente l'iscrizione quando il componente viene distrutto, proteggendoci dalle perdite di memoria.

Puoi leggere di più su Observables e sulla libreria RxJs in generale nei documenti ufficiali.

Creazione dei componenti della vista

In employees/components/employees-page/employees-page.component.html , inseriremo questo codice:

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

Allo stesso modo, employees/components/employees-list/employees-list.component.html avranno questo, usando la tecnica della pipe async menzionata sopra:

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

Ma in questo caso avremo bisogno anche del codice TypeScript per il componente. Il file employees/components/employees-list/employees-list.component.ts avrà bisogno di questo:

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

Quindi, andando al browser, quello che avremo ora è:

Un elenco di dipendenti vuoto e il messaggio "il modulo per i dipendenti funziona!"

E la console avrà il seguente output:

Eventi di patch che mostrano le modifiche con i valori prima e dopo.

Osservando questo, possiamo dire che Firestore ha trasmesso in streaming la raccolta dei employees con valori vuoti e l'archivio employees-page è stato corretto, impostando loading da true a false .

OK, costruiamo il modulo per aggiungere nuovi dipendenti a Firestore:

Il modulo dei dipendenti

In employees/components/employees-form/employees-form.component.html aggiungeremo questo codice:

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

Il codice TypeScript corrispondente vivrà 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() } }

Il modulo chiamerà il metodo create() di EmployeesService . In questo momento la pagina si presenta così:

Lo stesso elenco di dipendenti vuoto di prima, questa volta con un modulo per l'aggiunta di un nuovo dipendente.

Diamo un'occhiata a cosa succede quando aggiungiamo un nuovo dipendente.

Aggiunta di un nuovo dipendente

Dopo aver aggiunto un nuovo dipendente, vedremo quanto segue ottenere l'output sulla console:

Eventi patch mescolati con eventi Firestore, numerati da uno a sei (creazione locale, streaming raccolta Firestore, sottoscrizione raccolta locale, creazione Firestore, successo della creazione locale e ripristino dello stato del modulo di timeout creazione locale).

Questi sono tutti gli eventi che vengono attivati ​​quando si aggiunge un nuovo dipendente. Diamo un'occhiata più da vicino.

Quando chiamiamo create() eseguiremo il codice seguente, impostando loading=true , formStatus='Saving...' e l'array employees su vuoto ( (1) nell'immagine sopra).

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

Successivamente, chiamiamo il servizio Firestore di base per creare il dipendente, che registra (4) . Nella callback della promessa, impostiamo formStatus='Saved!' e log (5) . Infine, impostiamo un timeout per riportare formStatus su vuoto, registrando (6) .

Gli eventi di registro (2) e (3) sono gli eventi attivati ​​dall'abbonamento Firestore alla raccolta dipendenti. Quando viene creata un'istanza di EmployeesService , ci iscriviamo alla raccolta e riceviamo la raccolta ad ogni modifica che si verifica.

Questo imposta un nuovo stato per il negozio con loading=false impostando l'array employees sui dipendenti provenienti da Firestore.

Se espandiamo i gruppi di log, vedremo i dati dettagliati di ogni evento e aggiornamento dello store, con il valore precedente e successivo, utile per il debug.

L'output del registro precedente con tutti i dettagli della gestione dello stato ampliato.

Ecco come appare la pagina dopo aver aggiunto un nuovo dipendente:

L'elenco dei dipendenti con una scheda del dipendente e il modulo ancora compilato dopo averlo aggiunto.

Aggiunta di un componente di riepilogo

Diciamo che ora vogliamo visualizzare alcuni dati di riepilogo sulla nostra pagina. Diciamo che vogliamo il numero totale dei dipendenti, quanti sono gli autisti e quanti sono di Rosario.

Inizieremo aggiungendo le nuove proprietà dello stato al modello dello stato della pagina in employees/states/employees-page.ts :

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

E li inizializzeremo nel negozio in employees/services/emplyees-page.store.ts :

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

Successivamente, calcoleremo i valori per le nuove proprietà e aggiungeremo i rispettivi selettori in 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)) } // ...

Ora creiamo il componente di riepilogo:

 ng gc employees/components/EmployeesSummary

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

E 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$; } }

Aggiungeremo quindi il componente a employees/employees-page/employees-page.component.html :

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

Il risultato è il seguente:

Pagina Dipendenti, ora con un riepilogo sopra l'elenco, che mostra i conteggi dei dipendenti totali, quelli che sono conducenti e quelli che sono di Rosario.

Nella console abbiamo:

Output della console che mostra un evento patch che modifica i valori di riepilogo.

Il servizio dipendenti calcola il totale totalEmployees , totalDrivers e totalRosarioEmployees su ciascuna emissione e aggiorna lo stato.

Il codice completo di questo tutorial è disponibile su GitHub e c'è anche una demo dal vivo.

Gestione dello stato angolare dell'app utilizzando gli osservabili... Verifica!

In questo tutorial, abbiamo trattato un approccio semplice per la gestione dello stato nelle app Angular utilizzando un back-end Firebase.

Questo approccio si adatta perfettamente alle linee guida angolari sull'utilizzo di Observables. Facilita inoltre il debug fornendo il monitoraggio di tutti gli aggiornamenti allo stato dell'app.

Il servizio negozio generico può essere utilizzato anche per gestire lo stato delle app che non utilizzano le funzionalità di Firebase, sia per gestire solo i dati dell'app sia per i dati provenienti da altre API.

Ma prima di applicarlo indiscriminatamente, una cosa da considerare è che EmployeesService si iscrive a Firestore sul costruttore e continua ad ascoltare mentre l'app è attiva. Questo potrebbe essere utile se utilizziamo l'elenco dei dipendenti su più pagine dell'app, per evitare di ottenere dati da Firestore durante la navigazione tra le pagine.

Ma questa potrebbe non essere l'opzione migliore in altri scenari, ad esempio se devi solo estrarre i valori iniziali una volta e quindi attivare manualmente i ricaricamenti di dati da Firebase. La conclusione è che è sempre importante comprendere i requisiti della tua app per scegliere metodi di implementazione migliori.