/backbone-firebase.js

https://bitbucket.org/skudatech/backfire · JavaScript · 284 lines · 231 code · 37 blank · 16 comment · 46 complexity · 75d76bfa2cb2d94eeb1ee3469d48d858 MD5 · raw file

  1. /**
  2. * Backbone Firebase Adapter.
  3. */
  4. (function() {
  5. var _ = this._;
  6. var Backbone = this.Backbone;
  7. Backbone.Firebase = function(ref) {
  8. this._fbref = ref;
  9. this._children = [];
  10. if (typeof ref == "string") {
  11. this._fbref = new Firebase(ref);
  12. }
  13. _.bindAll(this);
  14. this._fbref.on("child_added", this._childAdded);
  15. this._fbref.on("child_moved", this._childMoved);
  16. this._fbref.on("child_changed", this._childChanged);
  17. this._fbref.on("child_removed", this._childRemoved);
  18. };
  19. _.extend(Backbone.Firebase.prototype, {
  20. _childAdded: function(childSnap, prevChild) {
  21. var model = childSnap.val();
  22. model.id = childSnap.name();
  23. if (prevChild) {
  24. var item = _.find(this._children, function(child) {
  25. return child.id == prevChild
  26. });
  27. this._children.splice(this._children.indexOf(item) + 1, 0, model);
  28. } else {
  29. this._children.unshift(model);
  30. }
  31. },
  32. _childMoved: function(childSnap, prevChild) {
  33. var model = childSnap.val();
  34. this._children = _.reject(this._children, function(child) {
  35. return child.id == model.id;
  36. });
  37. this._childAdded(childSnap, prevChild);
  38. },
  39. _childChanged: function(childSnap, prevChild) {
  40. var model = childSnap.val();
  41. model.id = childSnap.name();
  42. var item = _.find(this._children, function(child) {
  43. return child.id == model.id
  44. });
  45. this._children[this._children.indexOf(item)] = model;
  46. },
  47. _childRemoved: function(oldChildSnap) {
  48. var model = oldChildSnap.val();
  49. this._children = _.reject(this._children, function(child) {
  50. return child.id == model.id
  51. });
  52. },
  53. create: function(model, cb) {
  54. if (!model.id) {
  55. model.id = this._fbref.push().name();
  56. }
  57. var val = model.toJSON();
  58. this._fbref.child(model.id).set(val, _.bind(function(err) {
  59. if (!err) {
  60. cb(null, val);
  61. } else {
  62. cb("Could not create model " + model.id);
  63. }
  64. }, this));
  65. },
  66. read: function(model, cb) {
  67. if (!model.id) {
  68. _.defer(cb, "Invalid model ID provided to read");
  69. return;
  70. }
  71. var index = _.find(this._children, function(child) {
  72. return child.id == model.id
  73. });
  74. _.defer(cb, null, this._children[index]);
  75. },
  76. readAll: function(model, cb) {
  77. _.defer(cb, null, this._children);
  78. },
  79. update: function(model, cb) {
  80. var val = model.toJSON();
  81. this._fbref.child(model.id).update(val, function(err) {
  82. if (!err) {
  83. cb(null, val);
  84. } else {
  85. cb("Could not update model " + model.id, null);
  86. }
  87. });
  88. },
  89. delete: function(model, cb) {
  90. this._fbref.child(model.id).remove(function(err) {
  91. if (!err) {
  92. cb(null, model);
  93. } else {
  94. cb("Could not delete model " + model.id);
  95. }
  96. });
  97. }
  98. });
  99. Backbone.Firebase.sync = function(method, model, options, error) {
  100. var store = model.firebase || model.collection.firebase;
  101. // Backwards compatibility with Backbone <= 0.3.3
  102. if (typeof options == 'function') {
  103. options = {
  104. success: options,
  105. error: error
  106. };
  107. }
  108. if (method == "read" && model.id == undefined) {
  109. method = "readAll";
  110. }
  111. store[method].apply(this, [model, function(err, val) {
  112. if (err) {
  113. options.error(model, err, options);
  114. } else {
  115. options.success(model, val, options);
  116. }
  117. }]);
  118. };
  119. Backbone.oldSync = Backbone.sync;
  120. // Override 'Backbone.sync' to default to Firebase sync.
  121. // the original 'Backbone.sync' is still available in 'Backbone.oldSync'
  122. Backbone.sync = function(method, model, options, error) {
  123. var syncMethod = Backbone.oldSync;
  124. if (model.firebase || (model.collection && model.collection.firebase)) {
  125. syncMethod = Backbone.Firebase.sync;
  126. }
  127. return syncMethod.apply(this, [method, model, options, error]);
  128. };
  129. // Custom Firebase Collection.
  130. Backbone.Firebase.Collection = Backbone.Collection.extend({
  131. sync: function() {
  132. this._log("Sync called on a Firebase collection, ignoring.");
  133. },
  134. fetch: function() {
  135. this._log("Fetch called on a Firebase collection, ignoring.");
  136. },
  137. constructor: function(models, options) {
  138. if (options && options.firebase) {
  139. this.firebase = options.firebase;
  140. }
  141. switch (typeof this.firebase) {
  142. case "object": break;
  143. case "string": this.firebase = new Firebase(this.firebase); break;
  144. default: throw new Error("Invalid firebase reference created");
  145. }
  146. // Add handlers for remote events.
  147. this.firebase.on("child_added", this._childAdded.bind(this));
  148. this.firebase.on("child_moved", this._childMoved.bind(this));
  149. this.firebase.on("child_changed", this._childChanged.bind(this));
  150. this.firebase.on("child_removed", this._childRemoved.bind(this));
  151. // Apply parent constructor (this will also call initialize).
  152. Backbone.Collection.apply(this, arguments);
  153. // Add handlers for all models in this collection, and any future ones
  154. // that may be added.
  155. function _updateModel(model, options) {
  156. this.firebase.child(model.id).update(model.toJSON());
  157. }
  158. function _unUpdateModel(model) {
  159. model.off("change", _updateModel, this);
  160. }
  161. for (var i = 0; i < this.models.length; i++) {
  162. this.models[i].on("change", _updateModel, this);
  163. this.models[i].once("remove", _unUpdateModel, this);
  164. }
  165. this.on("add", function(model) {
  166. model.on("change", _updateModel, this);
  167. model.once("remove", _unUpdateModel, this);
  168. }, this);
  169. },
  170. comparator: function(model) {
  171. return model.id;
  172. },
  173. add: function(models, options) {
  174. var parsed = this._parseModels(models);
  175. for (var i = 0; i < parsed.length; i++) {
  176. var model = parsed[i];
  177. this.firebase.child(model.id).set(model);
  178. }
  179. // TODO: Implement options.success
  180. },
  181. remove: function(models, options) {
  182. var parsed = this._parseModels(models);
  183. for (var i = 0; i < parsed.length; i++) {
  184. var model = parsed[i];
  185. this.firebase.child(model.id).set(null);
  186. }
  187. // TODO: Implement options.success
  188. },
  189. create: function(model, options) {
  190. this._log("Create called, aliasing to add. Consider using Collection.add!");
  191. options = options ? _.clone(options) : {};
  192. if (options.wait) {
  193. this._log("Wait option provided to create, ignoring.");
  194. }
  195. model = Backbone.Collection.prototype._prepareModel.apply(
  196. this, [model, options]
  197. );
  198. if (!model) {
  199. return false;
  200. }
  201. this.add([model], options);
  202. return model;
  203. },
  204. _log: function(msg) {
  205. if (console && console.log) {
  206. console.log(msg);
  207. }
  208. },
  209. // TODO: Options will be ignored for add & remove, document this!
  210. _parseModels: function(models) {
  211. var ret = [];
  212. models = _.isArray(models) ? models.slice() : [models];
  213. for (var i = 0; i < models.length; i++) {
  214. var model = models[i];
  215. if (!model.id) {
  216. model.id = this.firebase.push().name();
  217. }
  218. if (model.toJSON && typeof model.toJSON == "function") {
  219. model = model.toJSON();
  220. }
  221. ret.push(model);
  222. }
  223. return ret;
  224. },
  225. _childAdded: function(snap) {
  226. Backbone.Collection.prototype.add.apply(this, [snap.val()]);
  227. },
  228. _childMoved: function(snap) {
  229. // TODO: Investigate: can this occur without the ID changing?
  230. this._log("_childMoved called with " + snap.val());
  231. },
  232. _childChanged: function(snap) {
  233. var model = snap.val();
  234. var item = _.find(this.models, function(child) {
  235. return child.id == model.id
  236. });
  237. if (!item) {
  238. // TODO: Investigate: what is the right way to handle this case?
  239. throw new Error("Could not find model with ID " + model.id);
  240. }
  241. item.set(model);
  242. },
  243. _childRemoved: function(snap) {
  244. Backbone.Collection.prototype.remove.apply(this, [snap.val()]);
  245. }
  246. });
  247. })();