See the following repos as a replacement
ember-firebase-adapter
for Flexible Adapter and Serializer partember-firebaseui
for FirebaseUI Componentember-computed-query
forhasFiltered
relationship- No replacement for
firebase-util
service. Before, it was useful because of some power features but they've since been split over to the adapter side. What's left now of the service are merely just sugar syntax over firebase.
This addon provides some useful utilities on top of EmberFire.
ember install emberfire-utils
Your app needs to have EmberFire installed for this addon to work.
You can optionally specify what libraries you'd want to exclude in your build within your ember-cli-build.js
.
Here's how:
var app = new EmberApp(defaults, {
'emberfire-utils': {
exclude: [ 'firebase-flex', 'firebase-util', 'firebase-ui' ],
},
});
Possible exclusions are
firebase-flex
,firebase-util
, andfirebase-ui
.
This is a standard Ember Data adapter that supports: createRecord()
, destroyRecord()
, findRecord()
, findAll()
, queryRecord()
, and query()
. However, its extended to allow some power features that's available to Firebase users.
Setup your application adapter like this:
// app/adapters/application.js
import FirebaseFlexAdapter from 'emberfire-utils/adapters/firebase-flex';
export default FirebaseFlexAdapter.extend();
// Saving a new record with fan-out
this.get('store').createRecord('post', {
title: 'Foo',
message: 'Bar'
}).save({
adapterOptions: {
include: {
'/userFeeds/user_a/$id': true,
'/userFeeds/user_b/$id': true,
}
}
});
// Deleting a record with fan-out
this.get('store').findRecord('post', 'post_a').then((post) => {
post.deleteRecord();
post.save({
adapterOptions: {
include: {
'/userFeeds/user_a/post_a': null,
'/userFeeds/user_b/post_a': null,
}
}
});
});
// Alternatively, you can use `destroyRecord` with fan-out too
this.get('store').findRecord('post', 'post_a').then((post) => {
post.destroyRecord({
adapterOptions: {
include: {
'/userFeeds/user_a/post_a': null,
'/userFeeds/user_b/post_a': null,
}
}
});
});
Notice the
$id
. It's a keyword that will be replaced by the model's ID.
this.get('store').createRecord('comment', {
title: 'Foo',
message: 'Bar'
}).save({
adapterOptions: { path: 'comments/post_a' }
});
By default, only the changed attributes will be updated in Firebase whenever we call save()
. This way, we can now have rules that doesn't allow some attributes to be edited.
The query params here uses the same format as the one in EmberFire with the addition of supporting the following:
orderBy: '.value'
.path
to query the data fromisReference
to know if thepath
is just a reference to a model in a different node (see example below)cacheId
to prevent duplicate listeners and make the query result array update in realtime- Without
cacheId
, the query result array won't listen forchild_added
orchild_removed
changes. However, the models that are already inside of it will still update in realtime. cacheId
isn't available inqueryRecord
.
- Without
Let's assume the following data structure.
{
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
"timestamp": 1459361875666
},
"two": { ... },
"three": { ... }
},
"members": {
"one": {
"ghopper": true,
"alovelace": true,
"eclarke": true
},
"two": { ... },
"three": { ... }
},
"messages": {
"one": {
"m1": {
"name": "eclarke",
"message": "The relay seems to be malfunctioning.",
"timestamp": 1459361875337
},
"m2": { ... },
"m3": { ... }
},
"two": { ... },
"three": { ... }
},
"users": {
"ghopper": { ... },
"alovelace": { ... },
"eclarke": { ... }
}
}
To fetch the chat members, you need to set the path
and isReference
. The isReference
boolean indicates that the nodes under members/one
are simply references to the user
model which is represented by the users
node.
this.get('store').query('user', {
path: 'members/one',
isReference: true,
limitToFirst: 10
});
To fetch the chat messages, you just need to set the path
and leave out the isReference
. Without the isReference
boolean, it indicates that the messages/one/m1
, messages/one/m2
, etc. are a direct representation of the message
model.
this.get('store').query('message', {
path: 'messages/one',
limitToFirst: 10
});
this.get('store').query('post', {
cacheId: 'my_cache_id',
limitToFirst: 10
});
this.get('store').query('post', {
limitToFirst: 10
}).then((posts) => {
posts.get('firebase').next(10);
});
As explained above, only the changed attributes will be saved when we call it. Ember Data currently doesn't provide a way to check if a relationship has changed. As a workaround, we need to fan-out the relationship to save it.
e.g.
const store = this.get('store');
store.findRecord('comment', 'another_comment').then((comment) => {
store.findRecord('post', 'post_a').then((post) => {
post.get('comments').addObject(comment);
post.save({
adapterOptions: {
include: {
'posts/post_a/comments/another_comment': '<some_value_here>'
}
}
});
});
});
However, there's a good side to this. Now we can provide different values to those relationships rather than the default true
value in EmberFire.
Most of the time, we don't want to use the hasMany()
relationship in our models because:
- It's not flexible enough to fetch from paths we want.
- It loads all the data when we access it.
- Even if we don't access it, those array of IDs are still taking up internet data usage.
To solve those 2 problems above, use hasFiltered()
relationship. It has the same parameters as store.query()
and it also works with infinite scrolling as explained above.
// app/models/post
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import hasFiltered from 'emberfire-utils/utils/has-filtered';
export default Model.extend({
title: attr('string'),
_innerReferencePath: attr('string'),
comments: hasFiltered('comment', {
cacheId: '$id_comments',
path: '/comments/$innerReferencePath/$id',
limitToFirst: 10,
})
});
Notice the following:
$id
- This is a keyword that will be replaced by the model's ID.
- This works for both
cacheId
andpath
.
_innerReferencePath
- This will be replaced by the inner Firebase reference path of the model.
- If
post
model lives in/posts/forum_a/post_a
, the value would beforum_a
.- Another example,
/posts/foo/bar/post_a
->foo/bar
.
- Another example,
- This is a client-side only property. It won't be persisted in the DB when you save the record.
$innerReferencePath
- This is a keyword that will be replaced by
_innerReferencePath
. - This only works for
path
. - Won't work when
_innerReferencePath
isn't defined. - This is useful for when let's say your comments lives in
/comments/<forum_id>/<post_id>
and you know the value of the<post_id>
through$id
but don't know the value of<forum_id>
.
- This is a keyword that will be replaced by
hasFiltered()
are read only.
Simply inject the firebase-util
service.
To write on multiple paths atomically in Firebase, call update()
.
const fanoutObject = {};
fanoutObject['users/foo/firstName'] = 'Foo';
fanoutObject['users/bar/firstName'] = 'Bar';
this.get('firebaseUtil').update(fanoutObject).then(() => {
// Do something after a succesful update
}).catch(error => {
// Do something with `error`
});
Should you need to generate a Firebase push ID for your multi-path updates, you can use generateIdForRecord()
. This returns a unique ID generated by Firebase's push()
method.
const pushId = this.get('firebaseUtil').generateIdForRecord();
To upload files in Firebase storage, call uploadFile()
.
function onStateChange(snapshot) {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log('Upload is ' + progress + '% done');
}
this.get('firebaseUtil').uploadFile('images/foo.jpg', file, metadata, onStateChange).then(downloadURL => {
// Do something with `downloadURL`
}).catch(error => {
// Do something with `error`
});
file
should be aBlob
or aUint8Array
.metadata
andonStateChange
are optional params.
To delete files in Firebase storage, call deleteFile()
.
this.get('firebaseUtil').deleteFile(url).then(() => {
// Do something on success
}).catch(error => {
// Do something with `error`
});
url
should be the HTTPS URL representation of the file. e.g. https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg
For the examples below, assume we have the following Firebase data:
{
"users" : {
"foo" : {
"photoURL" : "foo.jpg",
"username" : "bar"
},
"hello" : {
"photoURL" : "hello.jpg",
"username" : "world"
}
}
}
To query a single record, call queryRecord()
. This will return a promise that fulfills with the requested record in a plain object format.
this.get('firebaseUtil').queryRecord('users', { equalTo: 'foo' }).then((record) => {
// Do something with `record`
}).catch(error => {
// Do something with `error`
});
Params:
path
- Firebase pathoptions
- An object that can contain the following:cacheId
- Prevents duplicate listeners and returns cached record if it already exists. When not provided, Firebase won't listen for changes returned by this function.- EmberFire queries with the addition of
.value
fororderBy
and forcing oflimitToFirst
orlimitToLast
to 1.
limitToFirst
andlimitToLast
is forced to 1 because this method will only return a single record. If you provided an option oflimitToFirst
, it will set it to 1 regardless of the value that you've set. Same goes forlimitToLast
respectively.
To query for multiple records, call query()
. This will return a promise that fulfills with the requested records; each one in a plain object format.
this.get('firebaseUtil').query('users', { limitToFirst: 10 }).then((records) => {
// Do something with `records`
}).catch(error => {
// Do something with `error`
});
Params:
path
- Firebase pathoptions
- An object that can contain the following:cacheId
- Prevents duplicate listeners and returns cached record if it already exists. When not provided, Firebase won't listen for changes returned by this function.- EmberFire queries with the addition of
.value
fororderBy
.
For queryRecord()
and query()
, the records are serialized in plain object. For the queryRecord()
example
above, the record will be serialized to:
record = {
id: 'foo',
photoURL: 'foo.jpg',
username: 'bar'
};
For query()
:
records = [{
id: 'foo',
photoURL: 'foo.jpg',
username: 'bar'
}, {
id: 'hello',
photoURL: 'hello.jpg',
username: 'world'
}];
Should we retrieve a record who's value isn't an object (e.g. users/foo/username
), the record will be
serialized to:
record = {
id: 'username',
value: 'bar'
};
To load more records in the query()
result, call next()
.
const firebaseUtil = this.get('firebaseUtil');
firebaseUtil.query('users', {
cacheId: 'cache_id',
limitToFirst: 10,
}).then(() => {
firebaseUtil.next('cache_id', 10);
});
To check if a record exists, call isRecordExisting()
. This returns a promise that fulfills to true
if the record exists. Otherwise, false
.
this.get('firebaseUtil').isRecordExisting('users/foo').then((result) => {
// Do something with `result`
}).catch(error => {
// Do something with `error`
});
A component is provided for rendering FirebaseUI Auth. Here's how:
First setup your uiConfig
which is exactly the same with Firebase UI Auth.
import firebase from 'firebase';
import firebaseui from 'firebaseui';
let uiConfig = {
credentialHelper: firebaseui.auth.CredentialHelper.NONE,
signInSuccessUrl: '<url-to-redirect-to-on-success>',
signInOptions: [
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.FacebookAuthProvider.PROVIDER_ID,
firebase.auth.TwitterAuthProvider.PROVIDER_ID,
firebase.auth.GithubAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID
],
};
Then pass that uiConfig
into the firebase-ui-auth
component.
{{firebase-ui-auth uiConfig=uiConfig}}
This addon is compatible with EmberFire 2.0.x.
git clone <repository-url>
this repositorycd emberfire-utils
npm install
ember serve
- Visit your app at http://localhost:4200.
npm test
(Runsember try:each
to test your addon against multiple Ember versions)ember test
ember test --server
ember build
For more information on using ember-cli, visit https://ember-cli.com/.