Gestion des états dans Angular à l'aide de Firebase
Publié: 2022-03-11La gestion des états est un élément d'architecture très important à prendre en compte lors du développement d'une application Web.
Dans ce tutoriel, nous allons passer en revue une approche simple pour gérer l'état dans une application Angular qui utilise Firebase comme back-end.
Nous passerons en revue certains concepts tels que l'état, les magasins et les services. J'espère que cela vous aidera à mieux comprendre ces termes et à mieux comprendre d'autres bibliothèques de gestion d'état telles que NgRx et NgXs.
Nous allons créer une page d'administration des employés afin de couvrir différents scénarios de gestion d'état et les approches qui peuvent les gérer.
Gestion des composants, des services, du Firestore et de l'état dans Angular
Sur une application angulaire typique, nous avons des composants et des services. Habituellement, les composants serviront de modèle de vue. Les services contiendront une logique métier et/ou communiqueront avec des API externes ou d'autres services pour effectuer des actions ou récupérer des données.
Les composants affichent généralement des données et permettent aux utilisateurs d'interagir avec l'application pour exécuter des actions. Ce faisant, les données peuvent changer et l'application reflète ces changements en mettant à jour la vue.
Le moteur de détection de changement d'Angular se charge de vérifier quand une valeur dans un composant lié à la vue a changé et met à jour la vue en conséquence.
Au fur et à mesure que l'application se développera, nous commencerons à avoir de plus en plus de composants et de services. Souvent, comprendre comment les données changent et suivre où cela se produit peut être délicat.
Angular et Firebase
Lorsque nous utilisons Firebase comme back-end, nous disposons d'une API vraiment soignée qui contient la plupart des opérations et fonctionnalités dont nous avons besoin pour créer une application en temps réel.
@angular/fire
est la bibliothèque officielle d'Angular Firebase. Il s'agit d'une couche au-dessus de la bibliothèque Firebase JavaScript SDK qui simplifie l'utilisation du Firebase SDK dans une application Angular. Il s'intègre parfaitement aux bonnes pratiques angulaires telles que l'utilisation d'Observables pour obtenir et afficher des données de Firebase sur nos composants.
Magasins et État
Nous pouvons considérer "l'état" comme étant les valeurs affichées à un moment donné dans l'application. Le magasin est simplement le détenteur de cet état d'application.
L'état peut être modélisé comme un seul objet simple ou une série d'objets, reflétant les valeurs de l'application.
Exemple d'application angulaire/Firebase
Construisons-le : tout d'abord, nous allons créer un échafaudage d'application de base à l'aide de la CLI angulaire et le connecter à un projet 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
Et, sur styles.scss
:
// ... @import "~bootstrap/scss/bootstrap";
Ensuite, nous allons installer @angular/fire
:
npm install firebase @angular/fire
Maintenant, nous allons créer un projet Firebase sur la console Firebase.
Ensuite, nous sommes prêts à créer une base de données Firestore.
Pour ce tutoriel, je vais commencer en mode test. Si vous envisagez de passer en production, vous devez appliquer des règles pour interdire tout accès inapproprié.
Accédez à Présentation du projet → Paramètres du projet et copiez la configuration Web Firebase dans votre environnement local environments/environment.ts
.
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>" } };
À ce stade, nous avons l'échafaudage de base en place pour notre application. Si nous ng serve
, nous obtiendrons :
Classes de base Firestore et Store
Nous allons créer deux classes abstraites génériques, que nous allons ensuite taper et étendre pour construire nos services.
Les génériques vous permettent d'écrire un comportement sans type lié. Cela ajoute de la réutilisabilité et de la flexibilité à votre code.
Service Firestore générique
Afin de tirer parti des génériques TypeScript, nous allons créer un wrapper générique de base pour le service firestore
@angular/fire
.
Créons app/core/services/firestore.service.ts
.
Voici le 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}`); } }
Cette abstract class
fonctionnera comme un wrapper générique pour nos services Firestore.
Cela devrait être le seul endroit où nous devrions injecter AngularFirestore
. Cela minimisera l'impact lorsque la bibliothèque @angular/fire
sera mise à jour. De plus, si à un moment donné nous voulons changer de bibliothèque, nous n'aurons qu'à mettre à jour cette classe.
J'ai ajouté doc$
, collection$
, create
et delete
. Ils enveloppent les méthodes de @angular/fire
et fournissent une journalisation lorsque Firebase diffuse des données - cela deviendra très pratique pour le débogage - et après la création ou la suppression d'un objet.
Service de magasin générique
Notre service de magasin générique sera construit à l'aide de BehaviorSubject
de RxJS. BehaviorSubject
permet aux abonnés d'obtenir la dernière valeur émise dès qu'ils s'abonnent. Dans notre cas, cela est utile car nous pourrons commencer le magasin avec une valeur initiale pour tous nos composants lorsqu'ils s'abonnent au magasin.
Le magasin aura deux méthodes, patch
et set
. (Nous créerons des méthodes get
plus tard.)
Créons 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) } }
En tant que classe générique, nous différerons le typage jusqu'à ce qu'il soit correctement étendu.
Le constructeur recevra la valeur initiale de type Partial<T>
. Cela nous permettra de n'appliquer des valeurs qu'à certaines propriétés de l'état. Le constructeur s'abonnera également aux émissions BehaviorSubject
internes et maintiendra l'état interne à jour après chaque modification.
patch()
recevra la newValue
de type Partial<T>
et la fusionnera avec la valeur this.state
actuelle du magasin. Enfin, nous suivons next()
le newState
et émettons le nouvel état à tous les abonnés du magasin.
set()
fonctionne de manière très similaire, sauf qu'au lieu de corriger la valeur d'état, il la newValue
sur la nouvelle valeur qu'elle a reçue.
Nous enregistrerons les valeurs précédentes et suivantes de l'état au fur et à mesure que des changements se produisent, ce qui nous aidera à déboguer et à suivre facilement les changements d'état.
Mettre tous ensemble
Bon, voyons tout cela en action. Ce que nous allons faire, c'est créer une page d'employés, qui contiendra une liste d'employés, ainsi qu'un formulaire pour ajouter de nouveaux employés.
Mettons à jour app.component.html
pour ajouter une barre de navigation simple :
<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>
Ensuite, nous allons créer un module Core :
ng gm Core
Dans core/core.module.ts
, nous ajouterons les modules requis pour notre application :
// ... 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 { }
Créons maintenant la page des employés, en commençant par le module Employés :
ng gm Employees --routing
Dans employees-routing.module.ts
, ajoutons la route des employees
:
// ... import { EmployeesPageComponent } from './components/employees-page/employees-page.component'; // ... const routes: Routes = [ { path: 'employees', component: EmployeesPageComponent } ]; // ...
Et dans employees.module.ts
, nous importerons ReactiveFormsModule
:
// ... import { ReactiveFormsModule } from '@angular/forms'; // ... @NgModule({ // ... imports: [ // ... ReactiveFormsModule ] }) export class EmployeesModule { }
Ajoutons maintenant ces deux modules dans le fichier app.module.ts
:
// ... import { EmployeesModule } from './employees/employees.module'; import { CoreModule } from './core/core.module'; imports: [ // ... CoreModule, EmployeesModule ],
Enfin, créons les composants réels de notre page d'employés, ainsi que le modèle, le service, le magasin et l'état correspondants.
ng gc employees/components/EmployeesPage ng gc employees/components/EmployeesList ng gc employees/components/EmployeesForm
Pour notre modèle, nous aurons besoin d'un fichier appelé models/employee.ts
:
export interface Employee { id: string; name: string; location: string; hasDriverLicense: boolean; }
Notre service vivra dans un fichier nommé employees/services/employee.firestore.ts
. Ce service étendra le FirestoreService<T>
générique créé auparavant, et nous définirons simplement le basePath
de la collection 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'; }
Ensuite, nous allons créer le fichier employees/states/employees-page.ts
. Cela servira d'état de la page des employés :
import { Employee } from '../models/employee'; export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; }
L'état aura une valeur de loading
qui détermine s'il faut afficher un message de chargement sur la page, les employees
eux-mêmes et une variable formStatus
pour gérer l'état du formulaire (par exemple Saving
ou Saved
.)
Nous aurons besoin d'un fichier sur employees/services/employees-page.store.ts
. Ici, nous allons étendre le StoreService<T>
créé auparavant. Nous allons définir le nom du magasin, qui sera utilisé pour l'identifier lors du débogage.
Ce service initialisera et maintiendra l'état de la page des employés. Notez que le constructeur appelle super()
avec l'état initial de la page. Dans ce cas, nous initialiserons l'état avec loading=true
et un tableau vide d'employés.
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: [], }) } }
Créons maintenant EmployeesService
pour intégrer EmployeeFirestore
et EmployeesPageStore
:
ng gs employees/services/Employees
Notez que nous injectons EmployeeFirestore
et EmployeesPageStore
dans ce service. Cela signifie que EmployeesService
contiendra et coordonnera les appels à Firestore et au magasin pour mettre à jour l'état. Cela nous aidera à créer une API unique pour les composants à appeler.
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") }) } }
Voyons comment le service fonctionnera.

Dans le constructeur, nous nous abonnerons à la collection des employés de Firestore. Dès que Firestore émettra les données de la collection, nous mettrons à jour la boutique, en définissant loading=false
et employees
avec la collection renvoyée par Firestore. Depuis que nous avons injecté EmployeeFirestore
, les objets renvoyés par Firestore sont typés dans Employee
, ce qui active davantage de fonctionnalités IntelliSense.
Cet abonnement sera actif pendant que l'application est active, écoutant toutes les modifications et mettant à jour le magasin chaque fois que Firestore diffuse des données.
this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, }, `employees collection subscription`) }) ).subscribe()
Les employees$()
et loading$()
sélectionneront l'élément d'état que nous voulons utiliser plus tard sur le composant. employees$()
renverra un tableau vide lors du chargement de l'état. Cela nous permettra d'afficher des messages appropriés sur la vue.
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)) }
Bon, maintenant nous avons tous les services prêts, et nous pouvons créer nos composants de vue. Mais avant de faire cela, un bref rappel pourrait être utile…
Observables RxJs et le tuyau async
Les observables permettent aux abonnés de recevoir des émissions de données sous forme de flux. Ceci, en combinaison avec le tuyau async
, peut être très puissant.
Le tube async
s'occupe de s'abonner à un Observable et de mettre à jour la vue lorsque de nouvelles données sont émises. Plus important encore, il se désabonne automatiquement lorsque le composant est détruit, nous protégeant ainsi des fuites de mémoire.
Vous pouvez en savoir plus sur les bibliothèques Observables et RxJs en général dans les documents officiels.
Création des composants de la vue
Dans employees/components/employees-page/employees-page.component.html
, nous mettrons ce code :
<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>
De même, employees/components/employees-list/employees-list.component.html
aura ceci, en utilisant la technique de pipe async
mentionnée ci-dessus :
<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>
Mais dans ce cas, nous aurons également besoin de code TypeScript pour le composant. Le fichier employees/components/employees-list/employees-list.component.ts
aura besoin de ceci :
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); } }
Donc, en allant dans le navigateur, nous aurons maintenant :
Et la console aura la sortie suivante :
En regardant cela, nous pouvons dire que Firestore a diffusé la collection d' employees
avec des valeurs vides et que le magasin employees-page
a été corrigé, en définissant le loading
de true
à false
.
OK, créons le formulaire pour ajouter de nouveaux employés à Firestore :
Le formulaire des employés
Dans employees/components/employees-form/employees-form.component.html
nous ajouterons ce code :
<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>
Le code TypeScript correspondant vivra dans 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() } }
Le formulaire appellera la méthode create()
de EmployeesService
. Pour le moment, la page ressemble à ceci :
Voyons ce qui se passe lorsque nous ajoutons un nouvel employé.
Ajouter un nouvel employé
Après avoir ajouté un nouvel employé, nous verrons la sortie suivante sur la console :
Ce sont tous les événements qui se déclenchent lors de l'ajout d'un nouvel employé. Regardons de plus près.
Lorsque nous appelons create()
, nous exécutons le code suivant, en définissant loading=true
, formStatus='Saving...'
et le tableau des employees
à vide ( (1)
dans l'image ci-dessus).
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") })
Ensuite, nous appelons le service Firestore de base pour créer l'employé, qui se connecte (4)
. Lors du rappel de la promesse, nous définissons formStatus='Saved!'
et bûche (5)
. Enfin, nous définissons un délai d'attente pour remettre formStatus
à vide, logging (6)
.
Les événements de journal (2)
et (3)
sont les événements déclenchés par l'abonnement Firestore à la collection d'employés. Lorsque EmployeesService
est instancié, nous nous abonnons à la collection et recevons la collection à chaque changement qui se produit.
Cela définit un nouvel état pour le magasin avec loading=false
en définissant le tableau des employees
sur les employés provenant de Firestore.
Si nous développons les groupes de journaux, nous verrons des données détaillées de chaque événement et mise à jour du magasin, avec la valeur précédente et suivante, ce qui est utile pour le débogage.
Voici à quoi ressemble la page après l'ajout d'un nouvel employé :
Ajout d'un composant de synthèse
Disons que nous voulons maintenant afficher des données récapitulatives sur notre page. Disons que nous voulons le nombre total d'employés, combien sont des chauffeurs et combien sont de Rosario.
Nous commencerons par ajouter les nouvelles propriétés d'état au modèle d'état de la page dans employees/states/employees-page.ts
:
// ... export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; totalEmployees: number; totalDrivers: number; totalRosarioEmployees: number; }
Et nous les initialiserons en magasin dans employees/services/emplyees-page.store.ts
:
// ... constructor() { super({ loading: true, employees: [], totalDrivers: 0, totalEmployees: 0, totalRosarioEmployees: 0 }) } // ...
Ensuite, nous allons calculer les valeurs des nouvelles propriétés et ajouter leurs sélecteurs respectifs dans le 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)) } // ...
Maintenant, créons le composant récapitulatif :
ng gc employees/components/EmployeesSummary
Nous allons mettre ceci dans 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>
Et dans 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$; } }
Nous ajouterons ensuite le composant à employees/employees-page/employees-page.component.html
:
// ... <div class="col-12 mb-3"> <h4> Employees </h4> <app-employees-summary></app-employees-summary> </div> // ...
Le résultat est le suivant :
Dans la console nous avons :
Le service des employés calcule le total totalEmployees
, totalDrivers
et totalRosarioEmployees
sur chaque émission et met à jour l'état.
Le code complet de ce tutoriel est disponible sur GitHub, et il y a aussi une démo en direct.
Gestion de l'état de l'application angulaire à l'aide d'observables… Vérifiez !
Dans ce didacticiel, nous avons couvert une approche simple pour gérer l'état dans les applications angulaires à l'aide d'un back-end Firebase.
Cette approche correspond parfaitement aux directives angulaires d'utilisation d'Observables. Il facilite également le débogage en fournissant un suivi de toutes les mises à jour de l'état de l'application.
Le service de magasin générique peut également être utilisé pour gérer l'état des applications qui n'utilisent pas les fonctionnalités de Firebase, soit pour gérer uniquement les données de l'application, soit les données provenant d'autres API.
Mais avant d'appliquer cela sans discernement, une chose à considérer est que EmployeesService
s'abonne à Firestore sur le constructeur et continue d'écouter pendant que l'application est active. Cela peut être utile si nous utilisons la liste des employés sur plusieurs pages de l'application, pour éviter d'obtenir des données de Firestore lors de la navigation entre les pages.
Mais ce n'est peut-être pas la meilleure option dans d'autres scénarios, par exemple si vous avez juste besoin d'extraire les valeurs initiales une fois, puis de déclencher manuellement des rechargements de données à partir de Firebase. En fin de compte, il est toujours important de comprendre les exigences de votre application afin de choisir de meilleures méthodes de mise en œuvre.