|
1 | 1 | (function() { |
2 | 2 | 'use strict'; |
3 | 3 | /** |
4 | | - * Creates and maintains a synchronized object. This constructor should not be |
5 | | - * manually invoked. Instead, one should create a $firebase object and call $asObject |
6 | | - * on it: <code>$firebase( firebaseRef ).$asObject()</code>; |
| 4 | + * Creates and maintains a synchronized object, with 2-way bindings between Angular and Firebase. |
7 | 5 | * |
8 | | - * Internally, the $firebase object depends on this class to provide 2 methods, which it invokes |
9 | | - * to notify the object whenever a change has been made at the server: |
| 6 | + * Implementations of this class are contracted to provide the following internal methods, |
| 7 | + * which are used by the synchronization process and 3-way bindings: |
10 | 8 | * $$updated - called whenever a change occurs (a value event from Firebase) |
11 | 9 | * $$error - called when listeners are canceled due to a security error |
| 10 | + * $$notify - called to update $watch listeners and trigger updates to 3-way bindings |
| 11 | + * $ref - called to obtain the underlying Firebase reference |
12 | 12 | * |
13 | | - * Instead of directly modifying this class, one should generally use the $extendFactory |
| 13 | + * Instead of directly modifying this class, one should generally use the $extend |
14 | 14 | * method to add or change how methods behave: |
15 | 15 | * |
16 | 16 | * <pre><code> |
17 | | - * var NewFactory = $FirebaseObject.$extendFactory({ |
| 17 | + * var ExtendedObject = $FirebaseObject.$extend({ |
18 | 18 | * // add a new method to the prototype |
19 | 19 | * foo: function() { return 'bar'; }, |
20 | 20 | * }); |
21 | | - * </code></pre> |
22 | 21 | * |
23 | | - * And then the new factory can be used by passing it as an argument: |
24 | | - * <code>$firebase( firebaseRef, {objectFactory: NewFactory}).$asObject();</code> |
| 22 | + * var obj = new ExtendedObject(ref); |
| 23 | + * </code></pre> |
25 | 24 | */ |
26 | 25 | angular.module('firebase').factory('$FirebaseObject', [ |
27 | | - '$parse', '$firebaseUtils', '$log', '$interval', |
| 26 | + '$parse', '$firebaseUtils', '$log', |
28 | 27 | function($parse, $firebaseUtils, $log) { |
29 | 28 | /** |
30 | | - * This constructor should probably never be called manually. It is used internally by |
31 | | - * <code>$firebase.$asObject()</code>. |
| 29 | + * Creates a synchronized object with 2-way bindings between Angular and Firebase. |
32 | 30 | * |
33 | | - * @param $firebase |
34 | | - * @param {Function} destroyFn invoking this will cancel all event listeners and stop |
35 | | - * notifications from being delivered to $$updated and $$error |
36 | | - * @param readyPromise resolved when the initial data downloaded from Firebase |
| 31 | + * @param {Firebase} ref |
37 | 32 | * @returns {FirebaseObject} |
38 | 33 | * @constructor |
39 | 34 | */ |
40 | | - function FirebaseObject($firebase, destroyFn, readyPromise) { |
| 35 | + function FirebaseObject(ref) { |
41 | 36 | // These are private config props and functions used internally |
42 | 37 | // they are collected here to reduce clutter in console.log and forEach |
43 | 38 | this.$$conf = { |
44 | | - promise: readyPromise, |
45 | | - inst: $firebase, |
| 39 | + // synchronizes data to Firebase |
| 40 | + sync: new ObjectSyncManager(this, ref), |
| 41 | + // stores the Firebase ref |
| 42 | + ref: ref, |
| 43 | + // synchronizes $scope variables with this object |
46 | 44 | binding: new ThreeWayBinding(this), |
47 | | - destroyFn: destroyFn, |
| 45 | + // stores observers registered with $watch |
48 | 46 | listeners: [] |
49 | 47 | }; |
50 | 48 |
|
51 | 49 | // this bit of magic makes $$conf non-enumerable and non-configurable |
52 | 50 | // and non-writable (its properties are still writable but the ref cannot be replaced) |
53 | | - // we declare it above so the IDE can relax |
| 51 | + // we redundantly assign it above so the IDE can relax |
54 | 52 | Object.defineProperty(this, '$$conf', { |
55 | 53 | value: this.$$conf |
56 | 54 | }); |
57 | 55 |
|
58 | | - this.$id = $firebaseUtils.getKey($firebase.$ref().ref()); |
| 56 | + this.$id = $firebaseUtils.getKey(ref.ref()); |
59 | 57 | this.$priority = null; |
60 | 58 |
|
61 | 59 | $firebaseUtils.applyDefaults(this, this.$$defaults); |
| 60 | + |
| 61 | + // start synchronizing data with Firebase |
| 62 | + this.$$conf.sync.init(); |
62 | 63 | } |
63 | 64 |
|
64 | 65 | FirebaseObject.prototype = { |
|
68 | 69 | */ |
69 | 70 | $save: function () { |
70 | 71 | var self = this; |
71 | | - return self.$inst().$set($firebaseUtils.toJSON(self)) |
72 | | - .then(function(ref) { |
73 | | - self.$$notify(); |
74 | | - return ref; |
75 | | - }); |
| 72 | + var ref = self.$ref(); |
| 73 | + var data = $firebaseUtils.toJSON(self); |
| 74 | + return $firebaseUtils.doSet(ref, data).then(function() { |
| 75 | + self.$$notify(); |
| 76 | + return self.$ref(); |
| 77 | + }); |
76 | 78 | }, |
77 | 79 |
|
78 | 80 | /** |
|
83 | 85 | */ |
84 | 86 | $remove: function() { |
85 | 87 | var self = this; |
86 | | - $firebaseUtils.trimKeys(this, {}); |
87 | | - this.$value = null; |
88 | | - return self.$inst().$remove().then(function(ref) { |
| 88 | + $firebaseUtils.trimKeys(self, {}); |
| 89 | + self.$value = null; |
| 90 | + return $firebaseUtils.doRemove(self.$ref()).then(function() { |
89 | 91 | self.$$notify(); |
90 | | - return ref; |
| 92 | + return self.$ref(); |
91 | 93 | }); |
92 | 94 | }, |
93 | 95 |
|
|
104 | 106 | * @returns a promise which resolves after initial data is downloaded from Firebase |
105 | 107 | */ |
106 | 108 | $loaded: function(resolve, reject) { |
107 | | - var promise = this.$$conf.promise; |
| 109 | + var promise = this.$$conf.sync.ready(); |
108 | 110 | if (arguments.length) { |
109 | 111 | // allow this method to be called just like .then |
110 | 112 | // by passing any arguments on to .then |
|
114 | 116 | }, |
115 | 117 |
|
116 | 118 | /** |
117 | | - * @returns the original $firebase object used to create this object. |
| 119 | + * @returns {Firebase} the original Firebase instance used to create this object. |
118 | 120 | */ |
119 | | - $inst: function () { |
120 | | - return this.$$conf.inst; |
| 121 | + $ref: function () { |
| 122 | + return this.$$conf.ref; |
121 | 123 | }, |
122 | 124 |
|
123 | 125 | /** |
|
172 | 174 | * Informs $firebase to stop sending events and clears memory being used |
173 | 175 | * by this object (delete's its local content). |
174 | 176 | */ |
175 | | - $destroy: function (err) { |
| 177 | + $destroy: function(err) { |
176 | 178 | var self = this; |
177 | 179 | if (!self.$isDestroyed) { |
178 | 180 | self.$isDestroyed = true; |
| 181 | + self.$$conf.sync.destroy(err); |
179 | 182 | self.$$conf.binding.destroy(); |
180 | 183 | $firebaseUtils.each(self, function (v, k) { |
181 | 184 | delete self[k]; |
182 | 185 | }); |
183 | | - self.$$conf.destroyFn(err); |
184 | 186 | } |
185 | 187 | }, |
186 | 188 |
|
|
223 | 225 | $$scopeUpdated: function(newData) { |
224 | 226 | // we use a one-directional loop to avoid feedback with 3-way bindings |
225 | 227 | // since set() is applied locally anyway, this is still performant |
226 | | - return this.$inst().$set($firebaseUtils.toJSON(newData)); |
| 228 | + var def = $firebaseUtils.defer(); |
| 229 | + this.$ref().set($firebaseUtils.toJSON(newData), $firebaseUtils.makeNodeResolver(def)); |
| 230 | + return def.promise; |
227 | 231 | }, |
228 | 232 |
|
229 | 233 | /** |
|
262 | 266 | * `objectFactory` parameter: |
263 | 267 | * |
264 | 268 | * <pre><code> |
265 | | - * var MyFactory = $FirebaseObject.$extendFactory({ |
| 269 | + * var MyFactory = $FirebaseObject.$extend({ |
266 | 270 | * // add a method onto the prototype that prints a greeting |
267 | 271 | * getGreeting: function() { |
268 | 272 | * return 'Hello ' + this.first_name + ' ' + this.last_name + '!'; |
|
277 | 281 | * @param {Object} [methods] a list of functions to add onto the prototype |
278 | 282 | * @returns {Function} a new factory suitable for use with $firebase |
279 | 283 | */ |
280 | | - FirebaseObject.$extendFactory = function(ChildClass, methods) { |
| 284 | + FirebaseObject.$extend = function(ChildClass, methods) { |
281 | 285 | if( arguments.length === 1 && angular.isObject(ChildClass) ) { |
282 | 286 | methods = ChildClass; |
283 | 287 | ChildClass = function() { FirebaseObject.apply(this, arguments); }; |
|
304 | 308 | if( this.scope ) { |
305 | 309 | var msg = 'Cannot bind to ' + varName + ' because this instance is already bound to ' + |
306 | 310 | this.key + '; one binding per instance ' + |
307 | | - '(call unbind method or create another $firebase instance)'; |
| 311 | + '(call unbind method or create another FirebaseObject instance)'; |
308 | 312 | $log.error(msg); |
309 | 313 | return $firebaseUtils.reject(msg); |
310 | 314 | } |
|
394 | 398 | } |
395 | 399 | }; |
396 | 400 |
|
| 401 | + function ObjectSyncManager(firebaseObject, ref) { |
| 402 | + function destroy(err) { |
| 403 | + if( !sync.isDestroyed ) { |
| 404 | + sync.isDestroyed = true; |
| 405 | + ref.off('value', applyUpdate); |
| 406 | + firebaseObject = null; |
| 407 | + initComplete(err||'destroyed'); |
| 408 | + } |
| 409 | + } |
| 410 | + |
| 411 | + function init() { |
| 412 | + ref.on('value', applyUpdate, error); |
| 413 | + ref.once('value', function(snap) { |
| 414 | + if (angular.isArray(snap.val())) { |
| 415 | + $log.warn('Storing data using array indices in Firebase can result in unexpected behavior. See https://www.firebase.com/docs/web/guide/understanding-data.html#section-arrays-in-firebase for more information. Also note that you probably wanted $FirebaseArray and not $FirebaseObject.'); |
| 416 | + } |
| 417 | + |
| 418 | + initComplete(null); |
| 419 | + }, initComplete); |
| 420 | + } |
| 421 | + |
| 422 | + // call initComplete(); do not call this directly |
| 423 | + function _initComplete(err) { |
| 424 | + if( !isResolved ) { |
| 425 | + isResolved = true; |
| 426 | + if( err ) { def.reject(err); } |
| 427 | + else { def.resolve(firebaseObject); } |
| 428 | + } |
| 429 | + } |
| 430 | + |
| 431 | + var isResolved = false; |
| 432 | + var def = $firebaseUtils.defer(); |
| 433 | + var batch = $firebaseUtils.batch(); |
| 434 | + var applyUpdate = batch(function(snap) { |
| 435 | + var changed = firebaseObject.$$updated(snap); |
| 436 | + if( changed ) { |
| 437 | + // notifies $watch listeners and |
| 438 | + // updates $scope if bound to a variable |
| 439 | + firebaseObject.$$notify(); |
| 440 | + } |
| 441 | + }); |
| 442 | + var error = batch(firebaseObject.$$error, firebaseObject); |
| 443 | + var initComplete = batch(_initComplete); |
| 444 | + |
| 445 | + var sync = { |
| 446 | + isDestroyed: false, |
| 447 | + destroy: destroy, |
| 448 | + init: init, |
| 449 | + ready: function() { return def.promise; } |
| 450 | + }; |
| 451 | + return sync; |
| 452 | + } |
| 453 | + |
397 | 454 | return FirebaseObject; |
398 | 455 | } |
399 | 456 | ]); |
|
0 commit comments