Gerenciamento de estado em Angular usando Firebase

Publicados: 2022-03-11

O gerenciamento de estado é uma peça de arquitetura muito importante a ser considerada ao desenvolver um aplicativo da web.

Neste tutorial, veremos uma abordagem simples para gerenciar o estado em um aplicativo Angular que usa o Firebase como back-end.

Veremos alguns conceitos como estado, lojas e serviços. Espero que isso ajude você a entender melhor esses termos e também a entender melhor outras bibliotecas de gerenciamento de estado, como NgRx e NgXs.

Construiremos uma página de administração de funcionários para cobrir alguns cenários diferentes de gerenciamento de estado e as abordagens que podem lidar com eles.

Componentes, Serviços, Firestore e Gerenciamento de Estado em Angular

Em uma aplicação Angular típica temos componentes e serviços. Normalmente, os componentes servirão como modelo de visualização. Os serviços conterão lógica de negócios e/ou se comunicarão com APIs externas ou outros serviços para concluir ações ou recuperar dados.

Os componentes se conectam a serviços, que se conectam a outros serviços ou APIs HTTP.

Os componentes geralmente exibem dados e permitem que os usuários interajam com o aplicativo para executar ações. Ao fazer isso, os dados podem mudar e o aplicativo reflete essas alterações atualizando a visualização.

O mecanismo de detecção de alterações do Angular cuida de verificar quando um valor em um componente vinculado à exibição foi alterado e atualiza a exibição de acordo.

À medida que o aplicativo cresce, começaremos a ter cada vez mais componentes e serviços. Muitas vezes, entender como os dados estão mudando e rastrear onde isso acontece pode ser complicado.

Angular e Firebase

Quando usamos o Firebase como nosso back-end, recebemos uma API muito legal que contém a maioria das operações e funcionalidades necessárias para criar um aplicativo em tempo real.

@angular/fire é a biblioteca oficial do Angular Firebase. É uma camada sobre a biblioteca do Firebase JavaScript SDK que simplifica o uso do Firebase SDK em um aplicativo Angular. Ele fornece um bom ajuste com as boas práticas do Angular, como o uso de Observables para obter e exibir dados do Firebase para nossos componentes.

Os componentes se inscrevem na API JavaScript do Firebase via @angular/fire usando Observables.

Lojas e Estado

Podemos pensar em “estado” como sendo os valores exibidos em um determinado momento no aplicativo. A loja é simplesmente a detentora desse estado do aplicativo.

O estado pode ser modelado como um único objeto simples ou uma série deles, refletindo os valores da aplicação.

Um estado de armazenamento de armazenamento, que tem um objeto de exemplo com alguns pares de valores-chave simples para nome, cidade e país.

Aplicativo de amostra Angular/Firebase

Vamos criá-lo: primeiro, criaremos um scaffold de aplicativo básico usando o Angular CLI e o conectaremos a um projeto do 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, em styles.scss :

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

Em seguida, instalaremos @angular/fire :

 npm install firebase @angular/fire

Agora, criaremos um projeto do Firebase no console do Firebase.

A caixa de diálogo "Adicionar um projeto" do console do Firebase.

Então estamos prontos para criar um banco de dados Firestore.

Para este tutorial, começarei no modo de teste. Se você planeja liberar para produção, deve impor regras para proibir o acesso inadequado.

A caixa de diálogo "Regras de segurança para Cloud Firestore", com "Iniciar no modo de teste" selecionado em vez de "Iniciar no modo bloqueado".

Vá para Visão geral do projeto → Configurações do projeto e copie a configuração da Web do Firebase para seu environments/environment.ts local.

Listagem de aplicativos vazia para o novo projeto do 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>" } };

Neste ponto, temos o scaffold básico para nosso aplicativo. Se ng serve , obteremos:

O andaime Angular, dizendo "Bem-vindo ao administrador de funcionários!"

Firestore e classes base da loja

Criaremos duas classes abstratas genéricas, das quais digitaremos e estenderemos para construir nossos serviços.

Os genéricos permitem que você escreva o comportamento sem um tipo vinculado. Isso adiciona capacidade de reutilização e flexibilidade ao seu código.

Serviço genérico do Firestore

Para aproveitar os genéricos do TypeScript, o que faremos é criar um wrapper genérico básico para o serviço @angular/fire firestore .

Vamos criar app/core/services/firestore.service.ts .

Aqui está o código:

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

Essa abstract class funcionará como um wrapper genérico para nossos serviços do Firestore.

Este deve ser o único lugar onde devemos injetar AngularFirestore . Isso minimizará o impacto quando a biblioteca @angular/fire for atualizada. Além disso, se em algum momento quisermos alterar a biblioteca, precisaremos apenas atualizar esta classe.

Eu adicionei doc$ , collection$ , create e delete . Eles envolvem os métodos @angular/fire e fornecem registro quando o Firebase transmite dados - isso será muito útil para depuração - e depois que um objeto é criado ou excluído.

Serviço de loja genérico

Nosso serviço de armazenamento genérico será construído usando RxJS' BehaviorSubject . BehaviorSubject permite que os assinantes obtenham o último valor emitido assim que se inscreverem. No nosso caso, isso é útil porque poderemos iniciar a loja com um valor inicial para todos os nossos componentes quando eles se inscreverem na loja.

A loja terá dois métodos, patch e set . (Criaremos métodos get mais tarde.)

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

Como uma classe genérica, adiaremos a digitação até que ela seja devidamente estendida.

O construtor receberá o valor inicial do tipo Partial<T> . Isso nos permitirá aplicar apenas valores a algumas propriedades do estado. O construtor também assinará as emissões internas do BehaviorSubject e manterá o estado interno atualizado após cada alteração.

patch() receberá o newValue do tipo Partial<T> e o mesclará com o valor this.state atual do armazenamento. Finalmente, fazemos next() o newState e emitimos o novo estado para todos os assinantes da loja.

set() funciona de maneira muito semelhante, só que em vez de corrigir o valor do estado, ele o definirá para o newValue que recebeu.

Registraremos os valores anterior e seguinte do estado à medida que as alterações ocorrerem, o que nos ajudará a depurar e rastrear facilmente as alterações de estado.

Juntando tudo

Ok, vamos ver tudo isso em ação. O que faremos é criar uma página de funcionários, que conterá uma lista de funcionários, além de um formulário para adicionar novos funcionários.

Vamos atualizar app.component.html para adicionar uma barra de navegação simples:

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

Em seguida, criaremos um módulo Core:

 ng gm Core

Em core/core.module.ts , adicionaremos os módulos necessários para nosso aplicativo:

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

Agora, vamos criar a página de funcionários, começando pelo módulo Funcionários:

 ng gm Employees --routing

Em employees-routing.module.ts , vamos adicionar a rota de employees :

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

E em employees.module.ts , importaremos ReactiveFormsModule :

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

Agora, vamos adicionar esses dois módulos no arquivo app.module.ts :

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

Por fim, vamos criar os componentes reais da nossa página de funcionários, além do modelo, serviço, loja e estado correspondentes.

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

Para nosso modelo, precisaremos de um arquivo chamado models/employee.ts :

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

Nosso serviço ficará em um arquivo chamado employees/services/employee.firestore.ts . Este serviço estenderá o FirestoreService<T> genérico criado anteriormente e apenas definiremos o basePath da coleção do 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'; }

Em seguida, criaremos o arquivo employees/states/employees-page.ts . Isso servirá como o estado da página dos funcionários:

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

O estado terá um valor de loading que determina se será exibida uma mensagem de carregamento na página, os próprios employees e uma variável formStatus para lidar com o status do formulário (por exemplo, Saving ou Saved .)

Precisaremos de um arquivo em employees/services/employees-page.store.ts . Aqui vamos estender o StoreService<T> criado antes. Definiremos o nome da loja, que será usado para identificá-la durante a depuração.

Este serviço irá inicializar e manter o estado da página de funcionários. Observe que o construtor chama super() com o estado inicial da página. Nesse caso, inicializaremos o estado com loading=true e uma matriz vazia de funcionários.

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

Agora vamos criar EmployeesService para integrar EmployeeFirestore e EmployeesPageStore :

 ng gs employees/services/Employees

Observe que estamos injetando EmployeeFirestore e EmployeesPageStore neste serviço. Isso significa que o EmployeesService conterá e coordenará as chamadas ao Firestore e à loja para atualizar o estado. Isso nos ajudará a criar uma única API para os componentes chamarem.

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

Vamos dar uma olhada em como o serviço vai funcionar.

No construtor, assinaremos a coleção de funcionários do Firestore. Assim que o Firestore emitir os dados da coleção, atualizaremos a loja, definindo loading=false e employees com a coleção retornada do Firestore. Como injetamos EmployeeFirestore , os objetos retornados do Firestore são digitados em Employee , o que habilita mais recursos do IntelliSense.

Esta assinatura estará ativa enquanto o aplicativo estiver ativo, ouvindo todas as alterações e atualizando a loja sempre que o Firestore transmitir dados.

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

As funções employees$() e loading$() selecionarão a parte do estado que queremos usar posteriormente no componente. employees$() retornará um array vazio quando o estado estiver carregando. Isso nos permitirá exibir mensagens adequadas na exibição.

 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, agora temos todos os serviços prontos e podemos construir nossos componentes de visualização. Mas antes de fazermos isso, uma rápida atualização pode ser útil…

RxJs Observables e o Pipe async

Os observáveis ​​permitem que os assinantes recebam emissões de dados como um fluxo. Isso, em combinação com o pipe async , pode ser muito poderoso.

O pipe async se encarrega de assinar um Observable e atualizar a exibição quando novos dados são emitidos. Mais importante, ele cancela automaticamente a assinatura quando o componente é destruído, protegendo-nos contra vazamentos de memória.

Você pode ler mais sobre a biblioteca Observables e RxJs em geral nos documentos oficiais.

Criando os componentes de visualização

Em employees/components/employees-page/employees-page.component.html , colocaremos este código:

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

Da mesma forma, employees/components/employees-list/employees-list.component.html terá isso, usando a técnica de pipe async mencionada acima:

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

Mas, neste caso, também precisaremos de algum código TypeScript para o componente. O arquivo employees/components/employees-list/employees-list.component.ts precisará disso:

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

Então, indo para o navegador, o que teremos agora é:

Uma lista de funcionários vazia e a mensagem "employees-form works!"

E o console terá a seguinte saída:

Eventos de patch mostrando alterações com valores antes e depois.

Observando isso, podemos dizer que o Firestore transmitiu a coleção de employees com valores vazios e o armazenamento employees-page foi corrigido, definindo loading de true para false .

OK, vamos criar o formulário para adicionar novos funcionários ao Firestore:

O Formulário de Empregados

Em employees/components/employees-form/employees-form.component.html adicionaremos este código:

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

O código TypeScript correspondente ficará em 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() } }

O formulário chamará o método create() de EmployeesService . No momento a página está assim:

A mesma lista de funcionários vazia de antes, desta vez com um formulário para adicionar um novo funcionário.

Vamos dar uma olhada no que acontece quando adicionamos um novo funcionário.

Adicionando um novo funcionário

Depois de adicionar um novo funcionário, veremos a seguinte saída para o console:

Eventos de patch misturados com eventos do Firestore, numerados de um a seis (criação local, streaming de coleção do Firestore, assinatura de coleção local, criação do Firestore, sucesso da criação local e redefinição do status do formulário de tempo limite de criação local).

Esses são todos os eventos que são acionados ao adicionar um novo funcionário. Vamos olhar mais de perto.

Quando chamamos create() , executaremos o seguinte código, definindo loading=true , formStatus='Saving...' e o array employees para vazio ( (1) na imagem acima).

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

Em seguida, estamos chamando o serviço base do Firestore para criar o funcionário, que registra (4) . No retorno de chamada da promessa, definimos formStatus='Saved!' e log (5) . Por fim, definimos um tempo limite para definir formStatus de volta para vazio, logging (6) .

Os eventos de log (2) e (3) são os eventos acionados pela assinatura do Firestore para a coleção de funcionários. Quando o EmployeesService é instanciado, nós assinamos a coleção e recebemos a coleção a cada mudança que acontece.

Isso define um novo estado para a loja com loading=false definindo a matriz de employees para os funcionários provenientes do Firestore.

Se expandirmos os grupos de logs, veremos dados detalhados de cada evento e atualização da loja, com o valor anterior e o próximo, o que é útil para depuração.

A saída de log anterior com todos os detalhes de gerenciamento de estado expandidos.

Esta é a aparência da página depois de adicionar um novo funcionário:

A lista de funcionários com um cartão de funcionário e o formulário ainda preenchido ao adicioná-lo.

Adicionando um componente de resumo

Digamos que agora queremos exibir alguns dados de resumo em nossa página. Digamos que queremos o número total de funcionários, quantos são motoristas e quantos são de Rosário.

Começaremos adicionando as novas propriedades de estado ao modelo de estado da página em employees/states/employees-page.ts :

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

E vamos inicializá-los na loja em employees/services/emplyees-page.store.ts :

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

Em seguida, calcularemos os valores das novas propriedades e adicionaremos seus respectivos seletores no 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)) } // ...

Agora, vamos criar o componente de resumo:

 ng gc employees/components/EmployeesSummary

Vamos colocar isso em 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 em 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$; } }

Em seguida, adicionaremos o 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> // ...

O resultado é o seguinte:

Página de funcionários, agora com um resumo acima da lista, mostrando a contagem do total de funcionários, motoristas e rosários.

No console temos:

Saída do console mostrando um evento de patch alterando os valores de resumo.

O serviço de funcionários calcula o total totalEmployees , totalDrivers e totalRosarioEmployees em cada emissão e atualiza o estado.

O código completo deste tutorial está disponível no GitHub, e também há uma demonstração ao vivo.

Gerenciando o estado do aplicativo angular usando observáveis… Confira!

Neste tutorial, abordamos uma abordagem simples para gerenciar o estado em aplicativos Angular usando um back-end do Firebase.

Essa abordagem se encaixa perfeitamente com as diretrizes Angular de uso de Observables. Ele também facilita a depuração fornecendo rastreamento para todas as atualizações do estado do aplicativo.

O serviço de armazenamento genérico também pode ser usado para gerenciar o estado de aplicativos que não usam recursos do Firebase, seja para gerenciar apenas os dados do aplicativo ou dados provenientes de outras APIs.

Mas antes de aplicar isso indiscriminadamente, uma coisa a considerar é que EmployeesService assina o Firestore no construtor e continua ouvindo enquanto o aplicativo está ativo. Isso pode ser útil se usarmos a lista de funcionários em várias páginas do aplicativo, para evitar obter dados do Firestore ao navegar entre as páginas.

Mas essa pode não ser a melhor opção em outros cenários, como se você só precisar extrair os valores iniciais uma vez e acionar manualmente as recargas de dados do Firebase. A conclusão é que é sempre importante entender os requisitos do seu aplicativo para escolher os melhores métodos de implementação.