See the following repos as a replacement
ember-firebase-adapterfor Flexible Adapter and Serializer partember-firebaseuifor FirebaseUI Componentember-computed-queryforhasFilteredrelationship- No replacement for
firebase-utilservice. 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-utilsYour 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'.pathto query the data fromisReferenceto know if thepathis just a reference to a model in a different node (see example below)cacheIdto prevent duplicate listeners and make the query result array update in realtime- Without
cacheId, the query result array won't listen forchild_addedorchild_removedchanges. However, the models that are already inside of it will still update in realtime. cacheIdisn'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
cacheIdandpath.
_innerReferencePath- This will be replaced by the inner Firebase reference path of the model.
- If
postmodel 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
_innerReferencePathisn'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$idbut 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`
});fileshould be aBlobor aUint8Array.metadataandonStateChangeare 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`
});
urlshould 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
.valuefororderByand forcing oflimitToFirstorlimitToLastto 1.
limitToFirstandlimitToLastis 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 forlimitToLastrespectively.
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
.valuefororderBy.
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-utilsnpm install
ember serve- Visit your app at http://localhost:4200.
npm test(Runsember try:eachto test your addon against multiple Ember versions)ember testember test --server
ember build
For more information on using ember-cli, visit https://ember-cli.com/.