Ariel Gueta
Posted on June 5, 2019
So you are a big fan of Firebase, but you also want to start working with Akita (or vise versa). How do the two play together? As it turns out, very well 😁 This is due to the fact that both have a lot in common: They are both observable-based, robust, well-documented solutions in their respective areas.
In this article, I will show an example of managing a bookstore inventory using Akita with AngularFire, the official Angular library for Firebase.
It assumes that you have some working knowledge of Akita and Firebase. If not, please start with the Akita basics / AngularFire basics.
Setting Up AngularFire
First, we need to install the AngularFire library:
npm install @angular/fire
And set our firebase
settings in the environment file:
// environment.ts
export const environment = {
production: false,
firebase: {
apiKey: 'yourkey',
projectId: 'yourid',
}
};
Next, we need to import the AngularFireModule
into our application and call the initializeApp
method passing the configuration object we set before.
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
AngularFireModule.initializeApp(environment.firebase),
AngularFirestoreModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Now that we have firebase in our application, let's add Akita.
Setting Up Akita
Adding Akita to our project is easy. We can use the NG add schematic by running the following command:
ng add @datorama/akita
The above command adds Akita, Akita's dev-tools, and Akita's schematics into our project. The next step is to create a store. We need to maintain a collection of books, so we scaffold a new entity feature:
ng g af books
This command generates a books store, a books query, a books service, and a book model for us:
// book.model.ts
import { ID } from '@datorama/akita';
export interface Book {
id: ID;
title: string;
}
// books.store.ts
export interface BooksState extends EntityState<Book> {}
@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'books' })
export class BooksStore extends EntityStore<BooksState, Book> {
constructor() {
super();
}
}
// books.query.ts
@Injectable({ providedIn: 'root' })
export class BooksQuery extends QueryEntity<BooksState, Book> {
constructor(protected store: BooksStore) {
super(store);
}
}
The next thing I will do is create a reusable abstraction around the collection stateChanges
API. stateChanges
returns an observable which emits collection changes as they occur. We can leverage it to update our stores transparently:
import { AngularFirestoreCollection } from '@angular/fire/firestore';
import { EntityStore, withTransaction } from '@datorama/akita';
export function syncCollection<T>(collection: AngularFirestoreCollection<T>, store: EntityStore<any, any>) {
function updateStore(actions) {
if(actions.length === 0) {
store.setLoading(false);
return;
}
for ( const action of actions ) {
const id = action.payload.doc.id;
const entity = action.payload.doc.data();
switch( action.type ) {
case 'added':
store.add({ id, ...entity });
break;
case 'removed':
store.remove(id);
break;
case 'modified':
store.update(id, entity);
}
}
}
return collection.stateChanges().pipe(withTransaction(updateStore));
}
The syncCollection
function takes the collection and the store, listens for any state changes in the collection, and update the store based on the emitted action. We also use the withTransaction
as we want to dispatch one action when we finish with the updates.
Now, we can use it in our books service:
import { Injectable } from '@angular/core';
import { BooksStore } from './books.store';
import { AngularFirestore } from '@angular/fire/firestore';
import { syncCollection } from '../syncCollection';
@Injectable({ providedIn: 'root' })
export class BooksService {
private collection = this.db.collection('books');
constructor(private booksStore: BooksStore, private db: AngularFirestore) {
}
connect() {
return syncCollection(this.collection, this.booksStore);
}
addBook(title: string) {
this.collection.add({ title });
}
removeBook(id: string) {
this.collection.doc(id).delete();
}
editBook(id: string) {
this.collection.doc(id).update({ title: Math.random().toFixed(2).toString() });
}
}
We use the firebase API to create methods for add, edit, and remove books. Let's use them in our books component:
import { Component, OnDestroy, OnInit } from '@angular/core';
import { BooksQuery } from './state/books.query';
import { BooksService } from './state/books.service';
import { untilDestroyed } from 'ngx-take-until-destroy';
@Component({
selector: 'app-books'
})
export class AppComponent implements OnInit, OnDestroy {
books$ = this.booksQuery.selectAll();
loading$ = this.booksQuery.selectLoading();
constructor(private booksQuery: BooksQuery, private booksService: BooksService) {
}
ngOnInit() {
this.booksService.connect().pipe(untilDestroyed(this)).subscribe();
}
addBook(input: HTMLInputElement) {
this.booksService.addBook(input.value);
input.value = '';
}
removeBook(id: string) {
this.booksService.removeBook(id);
}
editBook(id: string) {
this.booksService.editBook(id);
}
trackByFn(i, book) {
return book.id;
}
ngOnDestroy() {
}
}
And here is the component's template:
<ng-container *ngIf="loading$ | async; else books">
<h1>Loading...</h1>
</ng-container>
<ng-template #books>
<input placeholder="Add Book..." #input (keyup.enter)="addBook(input)">
<ul>
<li *ngFor="let book of books$ | async; trackBy: trackByFn">
{{ book.title }}
<button (click)="editBook(book.id)">Edit</button>
<button (click)="removeBook(book.id)">Delete</button>
</li>
</ul>
</ng-template>
And that's all. The only thing we need to do is to update the firebase collection as we usually do, and let the syncCollection
functionality take care of everything.
Let's see the result:
Posted on June 5, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.