Gerenciamento de estado em Angular usando Firebase
Publicados: 2022-03-11O 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 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.
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.
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.
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.
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.
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:
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 é:
E o console terá a seguinte saída:
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:
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:
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.
Esta é a aparência da página depois de adicionar um novo funcionário:
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:
No console temos:
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.