Manajemen Status di Angular Menggunakan Firebase

Diterbitkan: 2022-03-11

Manajemen status adalah bagian arsitektur yang sangat penting untuk dipertimbangkan saat mengembangkan aplikasi web.

Dalam tutorial ini, kita akan membahas pendekatan sederhana untuk mengelola status dalam aplikasi Angular yang menggunakan Firebase sebagai bagian belakangnya.

Kami akan membahas beberapa konsep seperti negara bagian, toko, dan layanan. Mudah-mudahan, ini akan membantu Anda lebih memahami istilah-istilah ini dan juga lebih memahami perpustakaan manajemen negara bagian lainnya seperti NgRx dan NgXs.

Kami akan membangun halaman admin karyawan untuk mencakup beberapa skenario manajemen negara bagian yang berbeda dan pendekatan yang dapat menanganinya.

Komponen, Layanan, Firestore, dan Manajemen Status di Angular

Pada aplikasi Angular yang khas, kami memiliki komponen dan layanan. Biasanya, komponen akan berfungsi sebagai template tampilan. Layanan akan berisi logika bisnis dan/atau berkomunikasi dengan API eksternal atau layanan lain untuk menyelesaikan tindakan atau mengambil data.

Komponen terhubung ke layanan, yang terhubung ke layanan lain atau API HTTP.

Komponen biasanya akan menampilkan data dan memungkinkan pengguna berinteraksi dengan aplikasi untuk menjalankan tindakan. Saat melakukan ini, data dapat berubah dan aplikasi mencerminkan perubahan tersebut dengan memperbarui tampilan.

Mesin pendeteksi perubahan Angular menangani pemeriksaan ketika nilai dalam komponen yang terikat ke tampilan telah berubah dan memperbarui tampilan yang sesuai.

Seiring pertumbuhan aplikasi, kami akan mulai memiliki lebih banyak komponen dan layanan. Seringkali memahami bagaimana data berubah dan melacak di mana itu terjadi bisa jadi rumit.

Sudut dan Firebase

Saat kami menggunakan Firebase sebagai back-end kami, kami disediakan dengan API yang sangat rapi yang berisi sebagian besar operasi dan fungsionalitas yang kami butuhkan untuk membangun aplikasi waktu nyata.

@angular/fire adalah perpustakaan resmi Angular Firebase. Ini adalah lapisan di atas library Firebase JavaScript SDK yang menyederhanakan penggunaan Firebase SDK di aplikasi Angular. Ini memberikan kecocokan yang baik dengan praktik baik Angular seperti menggunakan Observables untuk mendapatkan dan menampilkan data dari Firebase ke komponen kami.

Komponen berlangganan ke Firebase JavaScript API melalui @angular/fire menggunakan Observables.

Toko dan Negara

Kita dapat menganggap "status" sebagai nilai yang ditampilkan pada titik waktu tertentu di aplikasi. Toko hanyalah pemegang status aplikasi itu.

Keadaan dapat dimodelkan sebagai objek polos tunggal atau serangkaian dari mereka, yang mencerminkan nilai-nilai aplikasi.

Status penyimpanan toko, yang memiliki objek contoh dengan beberapa pasangan nilai kunci sederhana untuk nama, kota, dan negara.

Aplikasi Sampel Angular/Firebase

Mari kita membangunnya: Pertama, kita akan membuat perancah aplikasi dasar menggunakan Angular CLI, dan menghubungkannya dengan proyek 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

Dan, di styles.scss :

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

Selanjutnya, kita akan menginstal @angular/fire :

 npm install firebase @angular/fire

Sekarang, kita akan membuat Proyek Firebase di konsol Firebase.

Dialog "Tambahkan proyek" konsol Firebase.

Kemudian kita siap untuk membuat database Firestore.

Untuk tutorial ini, saya akan mulai dalam mode uji. Jika Anda berencana untuk merilis ke produksi, Anda harus menegakkan aturan untuk melarang akses yang tidak pantas.

Dialog "Aturan keamanan untuk Cloud Firestore", dengan "Mulai dalam mode uji" dipilih, bukan "Mulai dalam mode terkunci".

Buka Ikhtisar Proyek → Pengaturan Proyek, dan salin konfigurasi web Firebase ke environments/environment.ts lokal Anda.

Cantuman aplikasi kosong untuk proyek Firebase baru.

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

Pada titik ini, kami memiliki perancah dasar untuk aplikasi kami. Jika kita ng serve , kita akan mendapatkan:

Perancah Angular, mengatakan "Selamat datang di karyawan-admin!"

Kelas Firestore dan Store Base

Kami akan membuat dua kelas abstrak generik, yang kemudian akan kami ketik dan kembangkan untuk membangun layanan kami.

Generik memungkinkan Anda menulis perilaku tanpa tipe terikat. Ini menambahkan penggunaan kembali dan fleksibilitas pada kode Anda.

Layanan Firestore Umum

Untuk memanfaatkan generik TypeScript, yang akan kita lakukan adalah membuat pembungkus generik dasar untuk layanan @angular/fire firestore .

Mari kita buat app/core/services/firestore.service.ts .

Berikut kodenya:

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

abstract class ini akan berfungsi sebagai pembungkus generik untuk layanan Firestore kami.

Ini harus menjadi satu-satunya tempat di mana kita harus menyuntikkan AngularFirestore . Ini akan meminimalkan dampak ketika perpustakaan @angular/fire diperbarui. Juga, jika suatu saat kita ingin mengubah perpustakaan, kita hanya perlu memperbarui kelas ini.

Saya menambahkan doc$ , collection$ , create , dan delete . Mereka membungkus metode @angular/fire dan menyediakan logging ketika Firebase mengalirkan data—ini akan menjadi sangat berguna untuk debugging—dan setelah sebuah objek dibuat atau dihapus.

Layanan Toko Umum

Layanan toko generik kami akan dibangun menggunakan BehaviorSubject RxJS. BehaviorSubject memungkinkan pelanggan mendapatkan nilai yang dipancarkan terakhir segera setelah mereka berlangganan. Dalam kasus kami, ini membantu karena kami akan dapat memulai toko dengan nilai awal untuk semua komponen kami ketika mereka berlangganan ke toko.

Toko akan memiliki dua metode, patch dan set . (Kami akan membuat metode get nanti.)

Mari kita buat 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) } }

Sebagai kelas generik, kami akan menunda pengetikan hingga diperpanjang dengan benar.

Konstruktor akan menerima nilai awal tipe Partial<T> . Ini akan memungkinkan kita untuk hanya menerapkan nilai ke beberapa properti negara bagian. Konstruktor juga akan berlangganan emisi BehaviorSubject internal dan terus memperbarui status internal setelah setiap perubahan.

patch() akan menerima newValue dari tipe Partial<T> dan akan menggabungkannya dengan nilai this.state store saat ini. Terakhir, kita next() newState dan memancarkan status baru ke semua pelanggan toko.

set() bekerja sangat mirip, hanya saja alih-alih menambal nilai status, itu akan mengaturnya ke newValue yang diterimanya.

Kami akan mencatat nilai status sebelumnya dan berikutnya saat terjadi perubahan, yang akan membantu kami men-debug dan melacak perubahan status dengan mudah.

Menyatukan Semuanya

Oke, mari kita lihat semua ini beraksi. Yang akan kita lakukan adalah membuat halaman karyawan, yang akan berisi daftar karyawan, ditambah formulir untuk menambah karyawan baru.

Mari perbarui app.component.html untuk menambahkan bilah navigasi sederhana:

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

Selanjutnya, kita akan membuat modul Core:

 ng gm Core

Di core/core.module.ts , kami akan menambahkan modul yang diperlukan untuk aplikasi kami:

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

Sekarang, mari kita buat halaman karyawan, dimulai dengan modul Karyawan:

 ng gm Employees --routing

Di employees-routing.module.ts , mari tambahkan rute employees :

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

Dan di employees.module.ts , kita akan mengimpor ReactiveFormsModule :

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

Sekarang, mari tambahkan dua modul ini di file app.module.ts :

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

Terakhir, mari kita buat komponen aktual dari halaman karyawan kita, ditambah model, layanan, toko, dan status yang sesuai.

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

Untuk model kita, kita membutuhkan file bernama models/employee.ts :

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

Layanan kami akan hidup dalam file bernama employees/services/employee.firestore.ts . Layanan ini akan memperluas FirestoreService<T> generik yang dibuat sebelumnya, dan kami hanya akan mengatur basePath dari koleksi 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'; }

Kemudian kita akan membuat file employees/states/employees-page.ts . Ini akan berfungsi sebagai status halaman karyawan:

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

Status akan memiliki nilai loading yang menentukan apakah akan menampilkan pesan pemuatan pada halaman, employees itu sendiri, dan variabel formStatus untuk menangani status formulir (mis. Saving atau Saved .)

Kami membutuhkan file di employees/services/employees-page.store.ts . Di sini kita akan memperluas StoreService<T> yang dibuat sebelumnya. Kami akan menetapkan nama toko, yang akan digunakan untuk mengidentifikasinya saat debugging.

Layanan ini akan menginisialisasi dan menahan status halaman karyawan. Perhatikan bahwa konstruktor memanggil super() dengan status awal halaman. Dalam hal ini, kami akan menginisialisasi status dengan loading=true dan array kosong dari karyawan.

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

Sekarang mari kita buat EmployeesService untuk mengintegrasikan EmployeeFirestore dan EmployeesPageStore :

 ng gs employees/services/Employees

Perhatikan bahwa kami menyuntikkan EmployeeFirestore dan EmployeesPageStore dalam layanan ini. Ini berarti bahwa EmployeesService akan memuat dan mengoordinasikan panggilan ke Firestore dan toko untuk memperbarui status. Ini akan membantu kami membuat satu API untuk komponen yang akan dipanggil.

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

Mari kita lihat bagaimana layanan ini akan bekerja.

Di konstruktor, kami akan berlangganan koleksi karyawan Firestore. Segera setelah Firestore mengeluarkan data dari koleksi, kami akan memperbarui toko, menyetel loading=false dan employees dengan koleksi yang dikembalikan Firestore. Karena kami telah menyuntikkan EmployeeFirestore , objek yang dikembalikan dari Firestore diketik ke Employee , yang memungkinkan lebih banyak fitur IntelliSense.

Langganan ini akan hidup saat aplikasi aktif, mendengarkan semua perubahan dan memperbarui toko setiap kali Firestore mengalirkan data.

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

Fungsi employees$() dan loading$() akan memilih bagian dari status yang ingin kita gunakan nanti pada komponen. employees$() akan mengembalikan array kosong saat status sedang dimuat. Ini akan memungkinkan kita untuk menampilkan pesan yang tepat pada tampilan.

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

Oke, jadi sekarang kita sudah menyiapkan semua layanan, dan kita bisa membangun komponen tampilan kita. Tetapi sebelum kita melakukannya, penyegaran cepat mungkin berguna…

RxJs Observables dan Pipa async

Observables memungkinkan pelanggan untuk menerima emisi data sebagai aliran. Ini, dalam kombinasi dengan pipa async , bisa sangat kuat.

Pipa async menangani berlangganan Observable dan memperbarui tampilan saat data baru dipancarkan. Lebih penting lagi, itu secara otomatis berhenti berlangganan ketika komponen dihancurkan, melindungi kita dari kebocoran memori.

Anda dapat membaca lebih lanjut tentang Observables dan perpustakaan RxJs secara umum di dokumen resmi.

Membuat Komponen Tampilan

Di employees/components/employees-page/employees-page.component.html , kami akan memasukkan kode ini:

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

Demikian juga, employees/components/employees-list/employees-list.component.html akan memiliki ini, menggunakan teknik pipa async yang disebutkan di atas:

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

Tetapi dalam hal ini kita juga memerlukan beberapa kode TypeScript untuk komponen tersebut. File employees/components/employees-list/employees-list.component.ts akan membutuhkan ini:

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

Jadi, pergi ke browser, yang akan kita miliki sekarang adalah:

Daftar karyawan kosong, dan pesan "formulir karyawan berfungsi!"

Dan konsol akan memiliki output berikut:

Menambal peristiwa yang menunjukkan perubahan dengan nilai sebelum dan sesudah.

Melihat ini, kami dapat mengatakan bahwa Firestore mengalirkan koleksi employees dengan nilai kosong, dan toko employees-page telah ditambal, menyetel loading dari true ke false .

Oke, mari kita buat formulir untuk menambahkan karyawan baru ke Firestore:

Formulir Karyawan

Di employees/components/employees-form/employees-form.component.html kita akan menambahkan kode ini:

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

Kode TypeScript yang sesuai akan tinggal di 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() } }

Formulir akan memanggil metode create() dari EmployeesService . Sekarang halamannya terlihat seperti ini:

Daftar karyawan kosong yang sama seperti sebelumnya, kali ini dengan formulir untuk menambahkan karyawan baru.

Mari kita lihat apa yang terjadi ketika kita menambahkan karyawan baru.

Menambahkan Karyawan Baru

Setelah menambahkan karyawan baru, kita akan melihat output get berikut ke konsol:

Peristiwa tambalan bercampur dengan peristiwa Firestore, bernomor satu hingga enam (pembuatan lokal, streaming koleksi Firestore, langganan koleksi lokal, pembuatan Firestore, keberhasilan pembuatan lokal, dan pengaturan ulang status formulir batas waktu pembuatan lokal.)

Ini semua adalah peristiwa yang dipicu saat menambahkan karyawan baru. Mari kita lihat lebih dekat.

Saat kita memanggil create() kita akan mengeksekusi kode berikut, menyetel loading=true , formStatus='Saving...' dan array employees menjadi kosong ( (1) pada gambar di atas).

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

Selanjutnya, kami memanggil layanan Firestore dasar untuk membuat karyawan, yang mencatat (4) . Pada panggilan balik janji, kami menyetel formStatus='Saved!' dan log (5) . Akhirnya, kami menetapkan batas waktu untuk mengatur formStatus kembali ke kosong, logging (6) .

Peristiwa log (2) dan (3) adalah peristiwa yang dipicu oleh langganan Firestore ke koleksi karyawan. Ketika EmployeesService dipakai, kami berlangganan koleksi dan menerima koleksi pada setiap perubahan yang terjadi.

Ini menetapkan status baru ke toko dengan loading=false dengan mengatur array employees ke karyawan yang berasal dari Firestore.

Jika kami memperluas grup log, kami akan melihat data terperinci dari setiap peristiwa dan pembaruan toko, dengan nilai sebelumnya dan berikutnya, yang berguna untuk debugging.

Keluaran log sebelumnya dengan semua detail manajemen status diperluas.

Beginilah tampilan halaman setelah menambahkan karyawan baru:

Daftar karyawan dengan kartu karyawan di dalamnya, dan formulir masih diisi dari menambahkannya.

Menambahkan Komponen Ringkasan

Katakanlah kita sekarang ingin menampilkan beberapa data ringkasan di halaman kita. Katakanlah kita ingin jumlah total karyawan, berapa banyak pengemudi, dan berapa banyak yang berasal dari Rosario.

Kami akan mulai dengan menambahkan properti status baru ke model status halaman di employees/states/employees-page.ts :

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

Dan kami akan menginisialisasinya di toko di employees/services/emplyees-page.store.ts :

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

Selanjutnya, kita akan menghitung nilai untuk properti baru dan menambahkan pemilihnya masing-masing di 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)) } // ...

Sekarang, mari kita buat komponen ringkasan:

 ng gc employees/components/EmployeesSummary

Kami akan menempatkan ini di 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>

Dan di 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$; } }

Kami kemudian akan menambahkan komponen ke employees/employees-page/employees-page.component.html :

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

Hasilnya adalah sebagai berikut:

Halaman karyawan, sekarang dengan ringkasan di atas daftar, menunjukkan jumlah total karyawan, mereka yang menjadi pengemudi, dan mereka yang berasal dari Rosario.

Di konsol kami memiliki:

Keluaran konsol menampilkan peristiwa tambalan yang mengubah nilai ringkasan.

Layanan karyawan menghitung total totalEmployees , totalDrivers , dan totalRosarioEmployees pada setiap emisi dan memperbarui status.

Kode lengkap dari tutorial ini tersedia di GitHub, dan ada juga demo langsung.

Mengelola Status Aplikasi Angular Menggunakan Observables… Periksa!

Dalam tutorial ini, kami membahas pendekatan sederhana untuk mengelola status di aplikasi Angular menggunakan back end Firebase.

Pendekatan ini sangat cocok dengan pedoman Angular menggunakan Observables. Ini juga memfasilitasi debugging dengan menyediakan pelacakan untuk semua pembaruan ke status aplikasi.

Layanan toko generik juga dapat digunakan untuk mengelola status aplikasi yang tidak menggunakan fitur Firebase, baik untuk mengelola hanya data aplikasi atau data yang berasal dari API lain.

Tetapi sebelum Anda menerapkan ini tanpa pandang bulu, satu hal yang perlu dipertimbangkan adalah bahwa EmployeesService berlangganan Firestore di konstruktor dan terus mendengarkan saat aplikasi aktif. Ini mungkin berguna jika kita menggunakan daftar karyawan di beberapa halaman di aplikasi, untuk menghindari mendapatkan data dari Firestore saat menavigasi antar halaman.

Tetapi ini mungkin bukan opsi terbaik dalam skenario lain seperti jika Anda hanya perlu menarik nilai awal sekali dan kemudian secara manual memicu pemuatan ulang data dari Firebase. Intinya adalah, selalu penting untuk memahami persyaratan aplikasi Anda untuk memilih metode implementasi yang lebih baik.