| Index: lib/src/firebase-element/firebase-collection.html
|
| diff --git a/lib/src/firebase-element/firebase-collection.html b/lib/src/firebase-element/firebase-collection.html
|
| index 64de6f60c413bcff1bf86291a829b080a05765fe..612fc837d2e3b2bd03f5d47b098982176d37792a 100644
|
| --- a/lib/src/firebase-element/firebase-collection.html
|
| +++ b/lib/src/firebase-element/firebase-collection.html
|
| @@ -233,6 +233,11 @@ manipulation of the collection without direct knowledge of Firebase key values.
|
| 'firebase-child-moved': '_onFirebaseChildMoved',
|
| },
|
|
|
| + created: function() {
|
| + this._pendingSplices = [];
|
| + this._lastLocallyAddedIndex = null;
|
| + },
|
| +
|
| /**
|
| * Add an item to the document referenced at `location`. A key associated
|
| * with the item will be created by Firebase, and can be accessed via the
|
| @@ -261,7 +266,10 @@ manipulation of the collection without direct knowledge of Firebase key values.
|
| */
|
| remove: function(data) {
|
| if (data == null || data.__firebaseKey__ == null) {
|
| - this._error('Failed to remove unknown value:', data);
|
| + // NOTE(cdata): This might be better as an error message in the
|
| + // console, but `Polymer.Base._error` throws, which we don't want
|
| + // to happen in this case.
|
| + this._warn('Refusing to remove unknown value:', data);
|
| return;
|
| }
|
|
|
| @@ -287,49 +295,82 @@ manipulation of the collection without direct knowledge of Firebase key values.
|
| * located at `location`.
|
| */
|
| removeByKey: function(key) {
|
| + if (!this.query) {
|
| + this._error('Cannot remove items before query has been initialized.');
|
| + return;
|
| + }
|
| +
|
| this.query.ref().child(key).remove();
|
| },
|
|
|
| - _applyLocalDataChanges: function(change) {
|
| + _localDataChanged: function(change) {
|
| var pathParts = change.path.split('.');
|
| - var firebaseKey;
|
| - var key;
|
| - var value;
|
|
|
| + // We don't care about self-changes, and we don't respond directly to
|
| + // length changes:
|
| if (pathParts.length < 2 || pathParts[1] === 'length') {
|
| return;
|
| }
|
|
|
| - if (pathParts[1] === 'splices') {
|
| - this._applySplicesToRemoteData(change.value.indexSplices);
|
| + // Handle splices via the adoption process. `indexSplices` is known to
|
| + // sometimes be null, so guard against that.
|
| + if (pathParts[1] === 'splices' && change.value.indexSplices != null) {
|
| + this._adoptSplices(change.value.indexSplices);
|
| return;
|
| }
|
|
|
| - key = pathParts[1];
|
| - value = Polymer.Collection.get(change.base).getItem(key);
|
| + // Otherwise, the change is happening to a sub-path of the array.
|
| + this._applySubPathChange(change);
|
| + },
|
| +
|
| + _applySubPathChange: function(change) {
|
| + var key = change.path.split('.')[1];
|
| + var value = Polymer.Collection.get(change.base).getItem(key);
|
| + var firebaseKey = value.__firebaseKey__;
|
|
|
| - // Temporarily remove the client-only `__firebaseKey__` property:
|
| - firebaseKey = value.__firebaseKey__;
|
| + // We don't want to accidentally reflect `__firebaseKey__` in the
|
| + // remote data, so we remove it temporarily. `null` values will be
|
| + // discarded by Firebase, so `delete` is not necessary:
|
| value.__firebaseKey__ = null;
|
| -
|
| this.query.ref().child(firebaseKey).set(value);
|
| -
|
| value.__firebaseKey__ = firebaseKey;
|
| },
|
|
|
| - _applySplicesToRemoteData: function(splices) {
|
| - this._log('splices', splices);
|
| - splices.forEach(function(splice) {
|
| - var added = splice.object.slice(splice.index, splice.index + splice.addedCount);
|
| + _adoptSplices: function(splices) {
|
| + this._pendingSplices = this._pendingSplices.concat(splices);
|
|
|
| - splice.removed.forEach(function(removed) {
|
| - this.remove(removed);
|
| + // We can afford apply removals synchronously, so we do that first
|
| + // and save the `added` operations for the `debounce` below.
|
| + this._applyLocalDataChange(function() {
|
| + splices.forEach(function(splice) {
|
| + splice.removed.forEach(function(removed) {
|
| + this.remove(removed);
|
| + }, this);
|
| }, this);
|
| + });
|
|
|
| - added.forEach(function(added) {
|
| - this.add(added);
|
| - }, this);
|
| - }, this);
|
| + // We async until the next turn. The reason we want to do this is
|
| + // that splicing within a splice handler will break the data binding
|
| + // system in some places. This is referred to as the "re-entrancy"
|
| + // problem. See polymer/polymer#2491.
|
| + this.debounce('_adoptSplices', function() {
|
| + this._applyLocalDataChange(function() {
|
| + var splices = this._pendingSplices;
|
| +
|
| + this._pendingSplices = [];
|
| +
|
| + splices.forEach(function(splice) {
|
| + var added = splice.object.slice(splice.index, splice.index + splice.addedCount);
|
| +
|
| + added.forEach(function(added, index) {
|
| + this._lastLocallyAddedIndex = splice.index + index;
|
| + this.add(added);
|
| + }, this);
|
| + }, this);
|
| +
|
| + this._lastLocallyAddedIndex = null;
|
| + });
|
| + });
|
| },
|
|
|
| _computeQuery: function(location, limitToFirst, limitToLast, orderByMethodName, startAt, endAt, equalTo) {
|
| @@ -339,6 +380,8 @@ manipulation of the collection without direct knowledge of Firebase key values.
|
| return;
|
| }
|
|
|
| + this._log('Recomputing query.', arguments);
|
| +
|
| query = new Firebase(location);
|
|
|
| if (orderByMethodName) {
|
| @@ -460,17 +503,56 @@ manipulation of the collection without direct knowledge of Firebase key values.
|
| _onFirebaseChildAdded: function(event) {
|
| this._applyRemoteDataChange(function() {
|
| var value = this._valueFromSnapshot(event.detail.childSnapshot);
|
| + var key = value.__firebaseKey__;
|
| var previousValueKey = event.detail.previousChildName;
|
| var index = previousValueKey != null ?
|
| this.data.indexOf(this._valueMap[previousValueKey]) + 1 : 0;
|
| + var lastLocallyAddedValue;
|
|
|
| this._valueMap[value.__firebaseKey__] = value;
|
|
|
| - this.splice('data', index, 0, value);
|
| + // NOTE(cdata): The rationale for this conditional dance around the
|
| + // last locally added index (since you will inevitably be wondering
|
| + // why we do it):
|
| + // There may be a "locally" added value which was spliced in. If
|
| + // there is, it may be in the "wrong" place in the array. This is
|
| + // due to the fact that Firebase may be applying a sort to the
|
| + // data, so we won't know the correct index for the locally added
|
| + // value until the `child_added` event is fired.
|
| + // Once we get the `child_added` event, we can check to see if the
|
| + // locally added value is in the right place. If it is, we just
|
| + // `set` it to the Firebase-provided value. If it is not, then
|
| + // we grab the original value, splice in the Firebase-provided
|
| + // value in the right place, and then (importantly: at the end)
|
| + // find the locally-added value again (since its index may have
|
| + // changed by splicing-in Firebase's value) and splice it out of the
|
| + // array.
|
| + if (this._lastLocallyAddedIndex === index) {
|
| + this.set(['data', index], value);
|
| + } else {
|
| + if (this._lastLocallyAddedIndex != null) {
|
| + lastLocallyAddedValue = this.data[this._lastLocallyAddedIndex];
|
| + }
|
| +
|
| + this.splice('data', index, 0, value);
|
| +
|
| + if (this._lastLocallyAddedIndex != null) {
|
| + this.splice('data', this.data.indexOf(lastLocallyAddedValue), 1);
|
| + }
|
| + }
|
| });
|
| },
|
|
|
| _onFirebaseChildRemoved: function(event) {
|
| + if (this._receivingLocalChanges) {
|
| + this._valueMap[event.detail.oldChildSnapshot.key()] = null;
|
| + // NOTE(cdata): If we are receiving local changes, that means that
|
| + // the splices have already been performed and items are already
|
| + // removed from the local data representation. No need to remove
|
| + // them again.
|
| + return;
|
| + }
|
| +
|
| this._applyRemoteDataChange(function() {
|
| var key = event.detail.oldChildSnapshot.key();
|
| var value = this._valueMap[key];
|
|
|