ember install ember-cli-zuglet
Ember.js Octane addon for effortless Firebase integration.
what

ember-cli-zuglet is an Ember.js Octane Edition addon that lets you build complex apps with Google Firebase Firestore, Auth, Storage and Functions easy and fun.

how

Let's look at a not that small example.

Suppose you want to build an app which lets you have a list of messages. In Firestore each message is a document in messages collection and in app you want to have Messages and Message models for logic. Also you want to have remotely modified messages streamed to your browser by using Firestore's onSnapshot observer but only when people access /messages routes.

So, let's start with a route that creates messages model which will be responsible for subscription to onSnapshot observer:

// app/routes/messages.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { route } from 'zuglet/decorators';

// this "activates" model while this route is active
// activated documents and queries automatically subscribe to onSnapshot observers
// model trees are built by using @activate, @model, @models decorators
@route()
export default class MessagesRoute extends Route {

  @service
  store

  async model() {
    // create `app/models/message.js` instance
    return this.store.models.create('messages');
  }

  async load(model) {
    // at this point model is activated
    // model.load waits for 1st query onSnapshot event
    await model.load();
  }

}

Then let's add Messages model:

// app/models/messages.js
import { setOwner } from '@ember/application';
import { inject as service } from '@ember/service';
import { load } from 'zuglet/utils';
import { activate, models } from 'zuglet/decorators';
import { cached } from 'tracked-toolbox';

export default class Messages {

  @service
  store

  constructor(owner) {
    // or extend from `zuglet/object` which does this and makes toString pretty
    setOwner(this, owner);
  }

  @cached
  get coll() {
    return this.store.collection('messages');
  }

  // creates query on first access and activates it because Messages instance is activated by route
  // activated query subscribes to onSnapshot observer
  @activate().content(({ coll }) => coll.orderBy('createdAt', 'desc').query())
  query

  // creates models for each document and activates it
  // each document here is *not* independently subscribed to onSnapshot
  // because documents are created by query which observers
  @models().source(({ query }) => query.content).named(() => 'message').mapping(doc => ({ doc }))
  models

  async load() {
    // small helper which just awaits this.query.promise
    // which is resolved on 1st query onSnapshot event
    await load(this.query);
  }

  async add(text) {
    let { store, coll } = this;
    // create a new document with generated id and provide some data
    let doc = coll.doc().new({
      text,
      createdAt: store.serverTimestamp
    });
    // save document in Firestore
    await doc.save();
  }

  byId(id) {
    return this.models.find(model => model.id === id);
  }

}

Each model in messages.models array is a Message model that encapsulates Firestore document and provides app interface to that.

Let's create that too:

// app/models/message.js
import { setOwner } from '@ember/application';
import { read, alias } from 'macro-decorators';

const data = key => alias(`doc.data.${key}`);

export default class Message {

  doc

  @reads('doc.id')
  id

  @data('createdAt')
  createdAt

  @data('text')
  text

  constructor(owner, { doc }) {
    setOwner(this, owner);
    this.doc = doc; // comes from @models()…mapping(fn)
  }

  async save() {
    // saves document if doc.isDirty
    await this.doc.save();
  }

  async delete() {
    await this.doc.delete();
  }

}

And we're done with models.

Render messages.models array, do mesage property edits, message.save(). Everything works as you would expect.

{{#each @messages.models as |message|}}
  <div class="message">
    {{message.text}}
  </div>
{{/each}}
install
$ ember install ember-cli-zuglet

and provide your Firebase project configuration in app/store.js.

You might also want to remove ember-data from package-0fd1d00dfdd2a23ecfb60c54b7822484.json and enable experimental decorators in jsconfig.json

"devDependencies": {
-    "ember-data": "~3.22.0",
}
// jsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "experimentalDecorators": true
  },
  "exclude": [ "node_modules", ".git" ]
}
author

This addon is built and maintained by Arnis Vuskans, contact me at ampatspell@gmail.com for Ember.js and Firebase consulting.