|
7 | 7 | * |
8 | 8 | * Internally, the $firebase object depends on this class to provide 5 $$ methods, which it invokes |
9 | 9 | * to notify the array whenever a change has been made at the server: |
10 | | - * $$added - called whenever a child_added event occurs |
11 | | - * $$updated - called whenever a child_changed event occurs |
12 | | - * $$moved - called whenever a child_moved event occurs |
13 | | - * $$removed - called whenever a child_removed event occurs |
| 10 | + * $$added - called whenever a child_added event occurs, returns the new record, or null to cancel |
| 11 | + * $$updated - called whenever a child_changed event occurs, returns true if updates were applied |
| 12 | + * $$moved - called whenever a child_moved event occurs, returns true if move should be applied |
| 13 | + * $$removed - called whenever a child_removed event occurs, returns true if remove should be applied |
14 | 14 | * $$error - called when listeners are canceled due to a security error |
15 | 15 | * $$process - called immediately after $$added/$$updated/$$moved/$$removed |
16 | | - * to splice/manipulate the array and invokes $$notify |
| 16 | + * (assuming that these methods do not abort by returning false or null) |
| 17 | + * to splice/manipulate the array and invoke $$notify |
17 | 18 | * |
18 | 19 | * Additionally, these methods may be of interest to devs extending this class: |
19 | 20 | * $$notify - triggers notifications to any $watch listeners, called by $$process |
20 | 21 | * $$getKey - determines how to look up a record's key (returns $id by default) |
21 | 22 | * |
22 | | - * Instead of directly modifying this class, one should generally use the $extendFactory |
23 | | - * method to add or change how methods behave. $extendFactory modifies the prototype of |
| 23 | + * Instead of directly modifying this class, one should generally use the $extend |
| 24 | + * method to add or change how methods behave. $extend modifies the prototype of |
24 | 25 | * the array class by returning a clone of $FirebaseArray. |
25 | 26 | * |
26 | 27 | * <pre><code> |
27 | | - * var NewFactory = $FirebaseArray.$extendFactory({ |
| 28 | + * var ExtendedArray = $FirebaseArray.$extend({ |
28 | 29 | * // add a new method to the prototype |
29 | 30 | * foo: function() { return 'bar'; }, |
30 | 31 | * |
|
38 | 39 | * return this.$getRecord(snap.key()).update(snap); |
39 | 40 | * } |
40 | 41 | * }); |
41 | | - * </code></pre> |
42 | 42 | * |
43 | | - * And then the new factory can be passed as an argument: |
44 | | - * <code>$firebase( firebaseRef, {arrayFactory: NewFactory}).$asArray();</code> |
| 43 | + * var list = new ExtendedArray(ref); |
| 44 | + * </code></pre> |
45 | 45 | */ |
46 | 46 | angular.module('firebase').factory('$FirebaseArray', ["$log", "$firebaseUtils", |
47 | 47 | function($log, $firebaseUtils) { |
48 | 48 | /** |
49 | 49 | * This constructor should probably never be called manually. It is used internally by |
50 | 50 | * <code>$firebase.$asArray()</code>. |
51 | 51 | * |
52 | | - * @param $firebase |
53 | | - * @param {Function} destroyFn invoking this will cancel all event listeners and stop |
54 | | - * notifications from being delivered to $$added, $$updated, $$moved, and $$removed |
55 | | - * @param readyPromise resolved when the initial data downloaded from Firebase |
| 52 | + * @param {Firebase} ref |
56 | 53 | * @returns {Array} |
57 | 54 | * @constructor |
58 | 55 | */ |
59 | | - function FirebaseArray($firebase, destroyFn, readyPromise) { |
| 56 | + function FirebaseArray(ref) { |
60 | 57 | var self = this; |
61 | 58 | this._observers = []; |
62 | 59 | this.$list = []; |
63 | | - this._inst = $firebase; |
64 | | - this._promise = readyPromise; |
65 | | - this._destroyFn = destroyFn; |
| 60 | + this._ref = ref; |
| 61 | + this._sync = new ArraySyncManager(this); |
| 62 | + |
| 63 | + $firebaseUtils.assertValidRef(ref, 'Must pass a valid Firebase reference ' + |
| 64 | + 'to $FirebaseArray (not a string or URL)'); |
66 | 65 |
|
67 | 66 | // indexCache is a weak hashmap (a lazy list) of keys to array indices, |
68 | 67 | // items are not guaranteed to stay up to date in this list (since the data |
|
80 | 79 | self.$list[key] = fn.bind(self); |
81 | 80 | }); |
82 | 81 |
|
| 82 | + this._sync.init(this.$list); |
| 83 | + |
83 | 84 | return this.$list; |
84 | 85 | } |
85 | 86 |
|
|
101 | 102 | */ |
102 | 103 | $add: function(data) { |
103 | 104 | this._assertNotDestroyed('$add'); |
104 | | - return this.$inst().$push($firebaseUtils.toJSON(data)); |
| 105 | + var def = $firebaseUtils.defer(); |
| 106 | + var ref = this.$ref().ref().push(); |
| 107 | + ref.set($firebaseUtils.toJSON(data), $firebaseUtils.makeNodeResolver(def)); |
| 108 | + return def.promise.then(function() { |
| 109 | + return ref; |
| 110 | + }); |
105 | 111 | }, |
106 | 112 |
|
107 | 113 | /** |
|
124 | 130 | var item = self._resolveItem(indexOrItem); |
125 | 131 | var key = self.$keyAt(item); |
126 | 132 | if( key !== null ) { |
127 | | - return self.$inst().$set(key, $firebaseUtils.toJSON(item)) |
128 | | - .then(function(ref) { |
129 | | - self.$$notify('child_changed', key); |
130 | | - return ref; |
131 | | - }); |
| 133 | + var ref = self.$ref().ref().child(key); |
| 134 | + var data = $firebaseUtils.toJSON(item); |
| 135 | + return $firebaseUtils.doSet(ref, data).then(function() { |
| 136 | + self.$$notify('child_changed', key); |
| 137 | + return ref; |
| 138 | + }); |
132 | 139 | } |
133 | 140 | else { |
134 | | - return $firebaseUtils.reject('Invalid record; could determine its key: '+indexOrItem); |
| 141 | + return $firebaseUtils.reject('Invalid record; could determine key for '+indexOrItem); |
135 | 142 | } |
136 | 143 | }, |
137 | 144 |
|
|
153 | 160 | this._assertNotDestroyed('$remove'); |
154 | 161 | var key = this.$keyAt(indexOrItem); |
155 | 162 | if( key !== null ) { |
156 | | - return this.$inst().$remove(key); |
| 163 | + var ref = this.$ref().ref().child(key); |
| 164 | + return $firebaseUtils.doRemove(ref).then(function() { |
| 165 | + return ref; |
| 166 | + }); |
157 | 167 | } |
158 | 168 | else { |
159 | | - return $firebaseUtils.reject('Invalid record; could not find key: '+indexOrItem); |
| 169 | + return $firebaseUtils.reject('Invalid record; could not determine key for '+indexOrItem); |
160 | 170 | } |
161 | 171 | }, |
162 | 172 |
|
|
208 | 218 | * @returns a promise |
209 | 219 | */ |
210 | 220 | $loaded: function(resolve, reject) { |
211 | | - var promise = this._promise; |
| 221 | + var promise = this._sync.ready(); |
212 | 222 | if( arguments.length ) { |
213 | 223 | // allow this method to be called just like .then |
214 | 224 | // by passing any arguments on to .then |
|
218 | 228 | }, |
219 | 229 |
|
220 | 230 | /** |
221 | | - * @returns the original $firebase object used to create this object. |
| 231 | + * @returns {Firebase} the original Firebase ref used to create this object. |
222 | 232 | */ |
223 | | - $inst: function() { return this._inst; }, |
| 233 | + $ref: function() { return this._ref; }, |
224 | 234 |
|
225 | 235 | /** |
226 | 236 | * Listeners passed into this method are notified whenever a new change (add, updated, |
|
257 | 267 | $destroy: function(err) { |
258 | 268 | if( !this._isDestroyed ) { |
259 | 269 | this._isDestroyed = true; |
| 270 | + this._sync.destroy(err); |
260 | 271 | this.$list.length = 0; |
261 | | - $log.debug('destroy called for FirebaseArray: '+this.$inst().$ref().toString()); |
262 | | - this._destroyFn(err); |
| 272 | + $log.debug('destroy called for FirebaseArray: '+this.$ref().ref().toString()); |
263 | 273 | } |
264 | 274 | }, |
265 | 275 |
|
|
282 | 292 | * @param {object} snap a Firebase snapshot |
283 | 293 | * @param {string} prevChild |
284 | 294 | * @return {object} the record to be inserted into the array |
| 295 | + * @protected |
285 | 296 | */ |
286 | 297 | $$added: function(snap/*, prevChild*/) { |
287 | 298 | // check to make sure record does not exist |
|
540 | 551 | * @param {Object} [methods] a list of functions to add onto the prototype |
541 | 552 | * @returns {Function} a new factory suitable for use with $firebase |
542 | 553 | */ |
543 | | - FirebaseArray.$extendFactory = function(ChildClass, methods) { |
| 554 | + FirebaseArray.$extend = function(ChildClass, methods) { |
544 | 555 | if( arguments.length === 1 && angular.isObject(ChildClass) ) { |
545 | 556 | methods = ChildClass; |
546 | 557 | ChildClass = function() { return FirebaseArray.apply(this, arguments); }; |
547 | 558 | } |
548 | 559 | return $firebaseUtils.inherit(ChildClass, FirebaseArray, methods); |
549 | 560 | }; |
550 | 561 |
|
| 562 | + function ArraySyncManager(firebaseArray) { |
| 563 | + function destroy(err) { |
| 564 | + if( !sync.isDestroyed ) { |
| 565 | + sync.isDestroyed = true; |
| 566 | + var ref = firebaseArray.$ref(); |
| 567 | + ref.off('child_added', created); |
| 568 | + ref.off('child_moved', moved); |
| 569 | + ref.off('child_changed', updated); |
| 570 | + ref.off('child_removed', removed); |
| 571 | + firebaseArray = null; |
| 572 | + resolve(err||'destroyed'); |
| 573 | + } |
| 574 | + } |
| 575 | + |
| 576 | + function init($list) { |
| 577 | + var ref = firebaseArray.$ref(); |
| 578 | + |
| 579 | + // listen for changes at the Firebase instance |
| 580 | + ref.on('child_added', created, error); |
| 581 | + ref.on('child_moved', moved, error); |
| 582 | + ref.on('child_changed', updated, error); |
| 583 | + ref.on('child_removed', removed, error); |
| 584 | + |
| 585 | + // determine when initial load is completed |
| 586 | + ref.once('value', function(snap) { |
| 587 | + if (angular.isArray(snap.val())) { |
| 588 | + $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.'); |
| 589 | + } |
| 590 | + |
| 591 | + resolve(null, $list); |
| 592 | + }, resolve); |
| 593 | + } |
| 594 | + |
| 595 | + // call resolve(), do not call this directly |
| 596 | + function _resolveFn(err, result) { |
| 597 | + if( !isResolved ) { |
| 598 | + isResolved = true; |
| 599 | + if( err ) { def.reject(err); } |
| 600 | + else { def.resolve(result); } |
| 601 | + } |
| 602 | + } |
| 603 | + |
| 604 | + var def = $firebaseUtils.defer(); |
| 605 | + var batch = $firebaseUtils.batch(); |
| 606 | + var created = batch(function(snap, prevChild) { |
| 607 | + var rec = firebaseArray.$$added(snap, prevChild); |
| 608 | + if( rec ) { |
| 609 | + firebaseArray.$$process('child_added', rec, prevChild); |
| 610 | + } |
| 611 | + }); |
| 612 | + var updated = batch(function(snap) { |
| 613 | + var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap)); |
| 614 | + if( rec ) { |
| 615 | + var changed = firebaseArray.$$updated(snap); |
| 616 | + if( changed ) { |
| 617 | + firebaseArray.$$process('child_changed', rec); |
| 618 | + } |
| 619 | + } |
| 620 | + }); |
| 621 | + var moved = batch(function(snap, prevChild) { |
| 622 | + var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap)); |
| 623 | + if( rec ) { |
| 624 | + var confirmed = firebaseArray.$$moved(snap, prevChild); |
| 625 | + if( confirmed ) { |
| 626 | + firebaseArray.$$process('child_moved', rec, prevChild); |
| 627 | + } |
| 628 | + } |
| 629 | + }); |
| 630 | + var removed = batch(function(snap) { |
| 631 | + var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap)); |
| 632 | + if( rec ) { |
| 633 | + var confirmed = firebaseArray.$$removed(snap); |
| 634 | + if( confirmed ) { |
| 635 | + firebaseArray.$$process('child_removed', rec); |
| 636 | + } |
| 637 | + } |
| 638 | + }); |
| 639 | + |
| 640 | + var isResolved = false; |
| 641 | + var error = batch(function(err) { |
| 642 | + _resolveFn(err); |
| 643 | + firebaseArray.$$error(err); |
| 644 | + }); |
| 645 | + var resolve = batch(_resolveFn); |
| 646 | + |
| 647 | + var sync = { |
| 648 | + destroy: destroy, |
| 649 | + isDestroyed: false, |
| 650 | + init: init, |
| 651 | + ready: function() { return def.promise; } |
| 652 | + }; |
| 653 | + |
| 654 | + return sync; |
| 655 | + } |
| 656 | + |
551 | 657 | return FirebaseArray; |
552 | 658 | } |
553 | 659 | ]); |
|
0 commit comments