loading...
Cover image for Getting Started with Angular, Akita & Firebase

Getting Started with Angular, Akita & Firebase

arielgueta profile image Ariel Gueta ・4 min read

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

Discussion

markdown guide
 

Hi! I'm learning how to use Akita and Firebase and I found a problem in the books service while trying to implement this. It appears that the addBook() function has a problem with the types of the input and output data, and get an error such as:

ERROR in src/app/state/books.service.ts:30:1 - error TS1128: Declaration or statement expected.
30 }
   ~
    ERROR in src/app/state/books.service.ts:19:25 - error TS2345: Argument of type '{ title: string; }' is not assignable to parameter of type 'Book'.
      Property 'id' is missing in type '{ title: string; }' but required in type 'Book'.

    19     this.collection.add({title});
                               ~~~~~~~
      src/app/state/book.model.ts:4:2
        4  id: ID;
           ~~
        'id' is declared here.

If I assigned a type of Book to the title parameter angular stops complaining about that but has problems elsewhere. I know it's asking for the id: property to get passed so I don't know if there's
anything I'm missing ? Thank you.

 

One little thing for:

you have to install ngx-take-until-destroy
from: npmjs.com/package/ngx-take-until-d...