Getting Started with Angular, Akita & Firebase

arielgueta

Ariel Gueta

Posted on June 5, 2019

Getting Started with Angular, Akita & Firebase

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:

💖 💪 🙅 🚩
arielgueta
Ariel Gueta

Posted on June 5, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related