diff --git a/README.md b/README.md
index dfc8234..12a3fb0 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ In order to use BackFire in your project, you need to include the following file
-
+
```
Use the URL above to download both the minified and non-minified versions of BackFire from the
diff --git a/bower.json b/bower.json
index c2aca43..f834c34 100644
--- a/bower.json
+++ b/bower.json
@@ -1,7 +1,7 @@
{
"name": "backfire",
"description": "The officially supported Backbone binding for Firebase",
- "version": "0.0.0",
+ "version": "0.4.0",
"authors": [
"Firebase (https://www.firebase.com/)"
],
diff --git a/dist/backfire.js b/dist/backfire.js
new file mode 100644
index 0000000..f6d6c9a
--- /dev/null
+++ b/dist/backfire.js
@@ -0,0 +1,543 @@
+/*!
+ * BackFire is the officially supported Backbone binding for Firebase. The
+ * bindings let you use special model and collection types that will
+ * automatically synchronize with Firebase, and also allow you to use regular
+ * Backbone.Sync based synchronization methods.
+ *
+ * BackFire 0.4.0
+ * https://github.com/firebase/backfire/
+ * License: MIT
+ */
+
+"use strict";
+
+(function() {
+
+ var _ = window._;
+ var Backbone = window.Backbone;
+
+ Backbone.Firebase = function(ref) {
+ this._fbref = ref;
+ this._children = [];
+ if (typeof ref == "string") {
+ this._fbref = new Firebase(ref);
+ }
+
+ this._fbref.on("child_added", this._childAdded, this);
+ this._fbref.on("child_moved", this._childMoved, this);
+ this._fbref.on("child_changed", this._childChanged, this);
+ this._fbref.on("child_removed", this._childRemoved, this);
+ };
+
+ _.extend(Backbone.Firebase.prototype, {
+
+ /**
+ * A utility for retrieving the key name of a Firebase ref or
+ * DataSnapshot. This is backwards-compatible with `name()`
+ * from Firebase 1.x.x and `key()` from Firebase 2.0.0+. Once
+ * support for Firebase 1.x.x is dropped in BackFire, this
+ * helper can be removed.
+ */
+ _getKey: function(refOrSnapshot) {
+ return (typeof refOrSnapshot.key === 'function') ? refOrSnapshot.key() : refOrSnapshot.name();
+ },
+
+ _childAdded: function(childSnap, prevChild) {
+ var model = childSnap.val();
+ model.id = this._getKey(childSnap);
+ if (prevChild) {
+ var item = _.find(this._children, function(child) {
+ return child.id == prevChild;
+ });
+ this._children.splice(this._children.indexOf(item) + 1, 0, model);
+ } else {
+ this._children.unshift(model);
+ }
+ },
+
+ _childMoved: function(childSnap, prevChild) {
+ var model = childSnap.val();
+ this._children = _.reject(this._children, function(child) {
+ return child.id == model.id;
+ });
+ this._childAdded(childSnap, prevChild);
+ },
+
+ _childChanged: function(childSnap) {
+ var model = childSnap.val();
+ model.id = this._getKey(childSnap);
+ var item = _.find(this._children, function(child) {
+ return child.id == model.id;
+ });
+ this._children[this._children.indexOf(item)] = model;
+ },
+
+ _childRemoved: function(oldChildSnap) {
+ var model = oldChildSnap.val();
+ this._children = _.reject(this._children, function(child) {
+ return child.id == model.id;
+ });
+ },
+
+ create: function(model, cb) {
+ if (!model.id) {
+ model.id = this._getKey(this._fbref.ref().push());
+ }
+
+ var val = model.toJSON();
+ this._fbref.ref().child(model.id).set(val, _.bind(function(err) {
+ if (!err) {
+ cb(null, val);
+ } else {
+ cb("Could not create model " + model.id);
+ }
+ }, this));
+ },
+
+ read: function(model, cb) {
+ if (!model.id) {
+ _.defer(cb, "Invalid model ID provided to read");
+ return;
+ }
+
+ var index = _.find(this._children, function(child) {
+ return child.id == model.id;
+ });
+
+ _.defer(cb, null, this._children[index]);
+ },
+
+ readAll: function(model, cb) {
+ _.defer(cb, null, this._children);
+ },
+
+ update: function(model, cb) {
+ var val = model.toJSON();
+ this._fbref.ref().child(model.id).update(val, function(err) {
+ if (!err) {
+ cb(null, val);
+ } else {
+ cb("Could not update model " + model.id, null);
+ }
+ });
+ },
+
+ "delete": function(model, cb) {
+ this._fbref.ref().child(model.id).remove(function(err) {
+ if (!err) {
+ cb(null, model);
+ } else {
+ cb("Could not delete model " + model.id);
+ }
+ });
+ },
+
+ ref: function() {
+ return this._fbref;
+ }
+ });
+
+
+ Backbone.Firebase.sync = function(method, model, options, error) {
+ var store = model.firebase || model.collection.firebase;
+
+ // Backwards compatibility with Backbone <= 0.3.3
+ if (typeof options == "function") {
+ options = {
+ success: options,
+ error: error
+ };
+ }
+
+ if (method == "read" && model.id === undefined) {
+ method = "readAll";
+ }
+
+ store[method].apply(store, [model, function(err, val) {
+ if (err) {
+ model.trigger("error", model, err, options);
+ if (Backbone.VERSION === "0.9.10") {
+ options.error(model, err, options);
+ } else {
+ options.error(err);
+ }
+ } else {
+ model.trigger("sync", model, val, options);
+ if (Backbone.VERSION === "0.9.10") {
+ options.success(model, val, options);
+ } else {
+ options.success(val);
+ }
+ }
+ }]);
+ };
+
+ Backbone.oldSync = Backbone.sync;
+
+ // Override "Backbone.sync" to default to Firebase sync.
+ // the original "Backbone.sync" is still available in "Backbone.oldSync"
+ Backbone.sync = function(method, model, options, error) {
+ var syncMethod = Backbone.oldSync;
+ if (model.firebase || (model.collection && model.collection.firebase)) {
+ syncMethod = Backbone.Firebase.sync;
+ }
+ return syncMethod.apply(this, [method, model, options, error]);
+ };
+
+ // Custom Firebase Collection.
+ Backbone.Firebase.Collection = Backbone.Collection.extend({
+ sync: function() {
+ this._log("Sync called on a Firebase collection, ignoring.");
+ },
+
+ fetch: function() {
+ this._log("Fetch called on a Firebase collection, ignoring.");
+ },
+
+ constructor: function(models, options) {
+ // Apply parent constructor (this will also call initialize).
+ Backbone.Collection.apply(this, arguments);
+
+ if (options && options.firebase) {
+ this.firebase = options.firebase;
+ }
+ switch (typeof this.firebase) {
+ case "object":
+ break;
+ case "string":
+ this.firebase = new Firebase(this.firebase);
+ break;
+ case "function":
+ this.firebase = this.firebase();
+ break;
+ default:
+ throw new Error("Invalid firebase reference created");
+ }
+
+ // Add handlers for remote events.
+ this.firebase.on("child_added", _.bind(this._childAdded, this));
+ this.firebase.on("child_moved", _.bind(this._childMoved, this));
+ this.firebase.on("child_changed", _.bind(this._childChanged, this));
+ this.firebase.on("child_removed", _.bind(this._childRemoved, this));
+
+ // Once handler to emit "sync" event.
+ this.firebase.once("value", _.bind(function() {
+ this.trigger("sync", this, null, null);
+ }, this));
+
+ // Handle changes in any local models.
+ this.listenTo(this, "change", this._updateModel, this);
+ // Listen for destroy event to remove models.
+ this.listenTo(this, "destroy", this._removeModel, this);
+
+ // Don't suppress local events by default.
+ this._suppressEvent = false;
+ },
+
+ comparator: function(model) {
+ return model.id;
+ },
+
+ add: function(models, options) {
+ var parsed = this._parseModels(models);
+ options = options ? _.clone(options) : {};
+ options.success =
+ _.isFunction(options.success) ? options.success : function() {};
+
+ for (var i = 0; i < parsed.length; i++) {
+ var model = parsed[i];
+ var childRef = this.firebase.ref().child(model.id);
+ if (options.silent === true) {
+ this._suppressEvent = true;
+ }
+ childRef.set(model, _.bind(options.success, model));
+ }
+
+ return parsed;
+ },
+
+ remove: function(models, options) {
+ var parsed = this._parseModels(models);
+ options = options ? _.clone(options) : {};
+ options.success =
+ _.isFunction(options.success) ? options.success : function() {};
+
+ for (var i = 0; i < parsed.length; i++) {
+ var model = parsed[i];
+ var childRef = this.firebase.ref().child(model.id);
+ if (options.silent === true) {
+ this._suppressEvent = true;
+ }
+ childRef.set(null, _.bind(options.success, model));
+ }
+
+ return parsed;
+ },
+
+ create: function(model, options) {
+ options = options ? _.clone(options) : {};
+ if (options.wait) {
+ this._log("Wait option provided to create, ignoring.");
+ }
+ model = Backbone.Collection.prototype._prepareModel.apply(
+ this, [model, options]
+ );
+ if (!model) {
+ return false;
+ }
+ var set = this.add([model], options);
+ return set[0];
+ },
+
+ reset: function(models, options) {
+ options = options ? _.clone(options) : {};
+ // Remove all models remotely.
+ this.remove(this.models, {silent: true});
+ // Add new models.
+ var ret = this.add(models, {silent: true});
+ // Trigger "reset" event.
+ if (!options.silent) {
+ this.trigger("reset", this, options);
+ }
+ return ret;
+ },
+
+ _log: function(msg) {
+ if (console && console.log) {
+ console.log(msg);
+ }
+ },
+
+ // TODO: Options will be ignored for add & remove, document this!
+ _parseModels: function(models) {
+ var ret = [];
+ models = _.isArray(models) ? models.slice() : [models];
+ for (var i = 0; i < models.length; i++) {
+ var model = models[i];
+ if (model.toJSON && typeof model.toJSON == "function") {
+ model = model.toJSON();
+ }
+ if (!model.id) {
+ model.id = Backbone.Firebase.prototype._getKey(this.firebase.ref().push());
+ }
+ ret.push(model);
+ }
+ return ret;
+ },
+
+ _childAdded: function(snap) {
+ var model = snap.val();
+ if (!model.id) {
+ if (!_.isObject(model)) {
+ model = {};
+ }
+ model.id = Backbone.Firebase.prototype._getKey(snap);
+ }
+ if (this._suppressEvent === true) {
+ this._suppressEvent = false;
+ Backbone.Collection.prototype.add.apply(this, [model], {silent: true});
+ } else {
+ Backbone.Collection.prototype.add.apply(this, [model]);
+ }
+ this.get(model.id)._remoteAttributes = model;
+ },
+
+ _childMoved: function(snap) {
+ // TODO: Investigate: can this occur without the ID changing?
+ this._log("_childMoved called with " + snap.val());
+ },
+
+ _childChanged: function(snap) {
+ var model = snap.val();
+ if (!model.id) {
+ model.id = Backbone.Firebase.prototype._getKey(snap);
+ }
+
+ var item = _.find(this.models, function(child) {
+ return child.id == model.id;
+ });
+
+ if (!item) {
+ // TODO: Investigate: what is the right way to handle this case?
+ throw new Error("Could not find model with ID " + model.id);
+ }
+
+ this._preventSync(item, true);
+ item._remoteAttributes = model;
+
+ var diff = _.difference(_.keys(item.attributes), _.keys(model));
+ _.each(diff, function(key) {
+ item.unset(key);
+ });
+
+ item.set(model);
+ this._preventSync(item, false);
+ },
+
+ _childRemoved: function(snap) {
+ var model = snap.val();
+ if (!model.id) {
+ model.id = Backbone.Firebase.prototype._getKey(snap);
+ }
+ if (this._suppressEvent === true) {
+ this._suppressEvent = false;
+ Backbone.Collection.prototype.remove.apply(
+ this, [model], {silent: true}
+ );
+ } else {
+ Backbone.Collection.prototype.remove.apply(this, [model]);
+ }
+ },
+
+ // Add handlers for all models in this collection, and any future ones
+ // that may be added.
+ _updateModel: function(model) {
+ if (model._remoteChanging) {
+ return;
+ }
+
+ var remoteAttributes = model._remoteAttributes || {};
+ var localAttributes = model.toJSON();
+ var updateAttributes = {};
+
+ var union = _.union(_.keys(remoteAttributes), _.keys(localAttributes));
+ _.each(union, function(key) {
+ if (!_.has(localAttributes, key)) {
+ updateAttributes[key] = null;
+ } else if (localAttributes[key] != remoteAttributes[key]) {
+ updateAttributes[key] = localAttributes[key];
+ }
+ });
+
+ if (_.size(updateAttributes)) {
+ // Special case if ".priority" was updated - a merge is not
+ // allowed so we'll have to do a full setWithPriority.
+ if (_.has(updateAttributes, ".priority")) {
+ var ref = this.firebase.ref().child(model.id);
+ var priority = localAttributes[".priority"];
+ delete localAttributes[".priority"];
+ ref.setWithPriority(localAttributes, priority);
+ } else {
+ this.firebase.ref().child(model.id).update(updateAttributes);
+ }
+ }
+ },
+
+ // Triggered when model.destroy() is called on one of the children.
+ _removeModel: function(model, collection, options) {
+ options = options ? _.clone(options) : {};
+ options.success =
+ _.isFunction(options.success) ? options.success : function() {};
+ var childRef = this.firebase.ref().child(model.id);
+ childRef.set(null, _.bind(options.success, model));
+ },
+
+ _preventSync: function(model, state) {
+ model._remoteChanging = state;
+ }
+ });
+
+ // Custom Firebase Model.
+ Backbone.Firebase.Model = Backbone.Model.extend({
+ save: function() {
+ this._log("Save called on a Firebase model, ignoring.");
+ },
+
+ destroy: function(options) {
+ // TODO: Fix naive success callback. Add error callback.
+ this.firebase.ref().set(null, this._log);
+ this.trigger("destroy", this, this.collection, options);
+ if (options.success) {
+ options.success(this, null, options);
+ }
+ },
+
+ constructor: function(model, options) {
+ // Store defaults so they don't get applied immediately.
+ var defaults = _.result(this, "defaults");
+
+ // Apply defaults only after first sync.
+ this.once("sync", function() {
+ this.set(_.defaults(this.toJSON(), defaults));
+ });
+
+ // Apply parent constructor (this will also call initialize).
+ Backbone.Model.apply(this, arguments);
+
+ if (options && options.firebase) {
+ this.firebase = options.firebase;
+ }
+ switch (typeof this.firebase) {
+ case "object":
+ break;
+ case "string":
+ this.firebase = new Firebase(this.firebase);
+ break;
+ case "function":
+ this.firebase = this.firebase();
+ break;
+ default:
+ throw new Error("Invalid firebase reference created");
+ }
+
+ // Add handlers for remote events.
+ this.firebase.on("value", _.bind(this._modelChanged, this));
+
+ this._listenLocalChange(true);
+ },
+
+ _listenLocalChange: function(state) {
+ if (state) {
+ this.on("change", this._updateModel, this);
+ } else {
+ this.off("change", this._updateModel, this);
+ }
+ },
+
+ _updateModel: function(model) {
+ // Find the deleted keys and set their values to null
+ // so Firebase properly deletes them.
+ var modelObj = model.changedAttributes();
+ _.each(model.changed, function(value, key) {
+ if (typeof value === "undefined" || value === null) {
+ if (key == "id") {
+ delete modelObj[key];
+ } else {
+ modelObj[key] = null;
+ }
+ }
+ });
+ if (_.size(modelObj)) {
+ this.firebase.ref().update(modelObj, this._log);
+ }
+ },
+
+ _modelChanged: function(snap) {
+ // Unset attributes that have been deleted from the server
+ // by comparing the keys that have been removed.
+ var newModel = snap.val();
+ if (typeof newModel === "object" && newModel !== null) {
+ var diff = _.difference(_.keys(this.attributes), _.keys(newModel));
+ var self = this;
+ _.each(diff, function(key) {
+ self.unset(key);
+ });
+ }
+ this._listenLocalChange(false);
+ this.set(newModel);
+ this._listenLocalChange(true);
+ this.trigger("sync", this, null, null);
+ },
+
+ _log: function(msg) {
+ if (typeof msg === "undefined" || msg === null) {
+ return;
+ }
+ if (console && console.log) {
+ console.log(msg);
+ }
+ }
+
+ });
+
+})();
diff --git a/dist/backfire.min.js b/dist/backfire.min.js
new file mode 100644
index 0000000..0676dc0
--- /dev/null
+++ b/dist/backfire.min.js
@@ -0,0 +1,11 @@
+/*!
+ * BackFire is the officially supported Backbone binding for Firebase. The
+ * bindings let you use special model and collection types that will
+ * automatically synchronize with Firebase, and also allow you to use regular
+ * Backbone.Sync based synchronization methods.
+ *
+ * BackFire 0.4.0
+ * https://github.com/firebase/backfire/
+ * License: MIT
+ */
+"use strict";!function(){var a=window._,b=window.Backbone;b.Firebase=function(a){this._fbref=a,this._children=[],"string"==typeof a&&(this._fbref=new Firebase(a)),this._fbref.on("child_added",this._childAdded,this),this._fbref.on("child_moved",this._childMoved,this),this._fbref.on("child_changed",this._childChanged,this),this._fbref.on("child_removed",this._childRemoved,this)},a.extend(b.Firebase.prototype,{_getKey:function(a){return"function"==typeof a.key?a.key():a.name()},_childAdded:function(b,c){var d=b.val();if(d.id=this._getKey(b),c){var e=a.find(this._children,function(a){return a.id==c});this._children.splice(this._children.indexOf(e)+1,0,d)}else this._children.unshift(d)},_childMoved:function(b,c){var d=b.val();this._children=a.reject(this._children,function(a){return a.id==d.id}),this._childAdded(b,c)},_childChanged:function(b){var c=b.val();c.id=this._getKey(b);var d=a.find(this._children,function(a){return a.id==c.id});this._children[this._children.indexOf(d)]=c},_childRemoved:function(b){var c=b.val();this._children=a.reject(this._children,function(a){return a.id==c.id})},create:function(b,c){b.id||(b.id=this._getKey(this._fbref.ref().push()));var d=b.toJSON();this._fbref.ref().child(b.id).set(d,a.bind(function(a){a?c("Could not create model "+b.id):c(null,d)},this))},read:function(b,c){if(!b.id)return void a.defer(c,"Invalid model ID provided to read");var d=a.find(this._children,function(a){return a.id==b.id});a.defer(c,null,this._children[d])},readAll:function(b,c){a.defer(c,null,this._children)},update:function(a,b){var c=a.toJSON();this._fbref.ref().child(a.id).update(c,function(d){d?b("Could not update model "+a.id,null):b(null,c)})},"delete":function(a,b){this._fbref.ref().child(a.id).remove(function(c){c?b("Could not delete model "+a.id):b(null,a)})},ref:function(){return this._fbref}}),b.Firebase.sync=function(a,c,d,e){var f=c.firebase||c.collection.firebase;"function"==typeof d&&(d={success:d,error:e}),"read"==a&&void 0===c.id&&(a="readAll"),f[a].apply(f,[c,function(a,e){a?(c.trigger("error",c,a,d),"0.9.10"===b.VERSION?d.error(c,a,d):d.error(a)):(c.trigger("sync",c,e,d),"0.9.10"===b.VERSION?d.success(c,e,d):d.success(e))}])},b.oldSync=b.sync,b.sync=function(a,c,d,e){var f=b.oldSync;return(c.firebase||c.collection&&c.collection.firebase)&&(f=b.Firebase.sync),f.apply(this,[a,c,d,e])},b.Firebase.Collection=b.Collection.extend({sync:function(){this._log("Sync called on a Firebase collection, ignoring.")},fetch:function(){this._log("Fetch called on a Firebase collection, ignoring.")},constructor:function(c,d){switch(b.Collection.apply(this,arguments),d&&d.firebase&&(this.firebase=d.firebase),typeof this.firebase){case"object":break;case"string":this.firebase=new Firebase(this.firebase);break;case"function":this.firebase=this.firebase();break;default:throw new Error("Invalid firebase reference created")}this.firebase.on("child_added",a.bind(this._childAdded,this)),this.firebase.on("child_moved",a.bind(this._childMoved,this)),this.firebase.on("child_changed",a.bind(this._childChanged,this)),this.firebase.on("child_removed",a.bind(this._childRemoved,this)),this.firebase.once("value",a.bind(function(){this.trigger("sync",this,null,null)},this)),this.listenTo(this,"change",this._updateModel,this),this.listenTo(this,"destroy",this._removeModel,this),this._suppressEvent=!1},comparator:function(a){return a.id},add:function(b,c){var d=this._parseModels(b);c=c?a.clone(c):{},c.success=a.isFunction(c.success)?c.success:function(){};for(var e=0;e (https://www.firebase.com/)",
"homepage": "https://github.com/firebase/backfire/",
"repository": {