OLD | NEW |
1 <!-- | 1 <!-- |
2 @license | 2 @license |
3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved. | 3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved. |
4 This code may only be used under the BSD style license found at http://polymer.g
ithub.io/LICENSE.txt | 4 This code may only be used under the BSD style license found at http://polymer.g
ithub.io/LICENSE.txt |
5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | 5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
6 The complete set of contributors may be found at http://polymer.github.io/CONTRI
BUTORS.txt | 6 The complete set of contributors may be found at http://polymer.github.io/CONTRI
BUTORS.txt |
7 Code distributed by Google as part of the polymer project is also | 7 Code distributed by Google as part of the polymer project is also |
8 subject to an additional IP rights grant found at http://polymer.github.io/PATEN
TS.txt | 8 subject to an additional IP rights grant found at http://polymer.github.io/PATEN
TS.txt |
9 --> | 9 --> |
10 | 10 |
(...skipping 215 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
226 } | 226 } |
227 }, | 227 }, |
228 | 228 |
229 listeners: { | 229 listeners: { |
230 'firebase-child-added': '_onFirebaseChildAdded', | 230 'firebase-child-added': '_onFirebaseChildAdded', |
231 'firebase-child-removed': '_onFirebaseChildRemoved', | 231 'firebase-child-removed': '_onFirebaseChildRemoved', |
232 'firebase-child-changed': '_onFirebaseChildChanged', | 232 'firebase-child-changed': '_onFirebaseChildChanged', |
233 'firebase-child-moved': '_onFirebaseChildMoved', | 233 'firebase-child-moved': '_onFirebaseChildMoved', |
234 }, | 234 }, |
235 | 235 |
| 236 created: function() { |
| 237 this._pendingSplices = []; |
| 238 this._lastLocallyAddedIndex = null; |
| 239 }, |
| 240 |
236 /** | 241 /** |
237 * Add an item to the document referenced at `location`. A key associated | 242 * Add an item to the document referenced at `location`. A key associated |
238 * with the item will be created by Firebase, and can be accessed via the | 243 * with the item will be created by Firebase, and can be accessed via the |
239 * Firebase Query instance returned by this method. | 244 * Firebase Query instance returned by this method. |
240 * | 245 * |
241 * @param {Object} data A value to add to the document. | 246 * @param {Object} data A value to add to the document. |
242 * @return {Object} A Firebase Query instance referring to the added item. | 247 * @return {Object} A Firebase Query instance referring to the added item. |
243 */ | 248 */ |
244 add: function(data) { | 249 add: function(data) { |
245 var query; | 250 var query; |
246 | 251 |
247 this._log('Adding new item to collection with value:', data); | 252 this._log('Adding new item to collection with value:', data); |
248 | 253 |
249 query = this.query.ref().push(); | 254 query = this.query.ref().push(); |
250 query.set(data); | 255 query.set(data); |
251 | 256 |
252 return query; | 257 return query; |
253 }, | 258 }, |
254 | 259 |
255 /** | 260 /** |
256 * Remove an item from the document referenced at `location`. The item | 261 * Remove an item from the document referenced at `location`. The item |
257 * is assumed to be an identical reference to an item already in the | 262 * is assumed to be an identical reference to an item already in the |
258 * `data` Array. | 263 * `data` Array. |
259 * | 264 * |
260 * @param {Object} data An identical reference to an item in `this.data`. | 265 * @param {Object} data An identical reference to an item in `this.data`. |
261 */ | 266 */ |
262 remove: function(data) { | 267 remove: function(data) { |
263 if (data == null || data.__firebaseKey__ == null) { | 268 if (data == null || data.__firebaseKey__ == null) { |
264 this._error('Failed to remove unknown value:', data); | 269 // NOTE(cdata): This might be better as an error message in the |
| 270 // console, but `Polymer.Base._error` throws, which we don't want |
| 271 // to happen in this case. |
| 272 this._warn('Refusing to remove unknown value:', data); |
265 return; | 273 return; |
266 } | 274 } |
267 | 275 |
268 this._log('Removing collection item "' + data.__firebaseKey__ + '"', dat
a.value); | 276 this._log('Removing collection item "' + data.__firebaseKey__ + '"', dat
a.value); |
269 | 277 |
270 this.removeByKey(data.__firebaseKey__); | 278 this.removeByKey(data.__firebaseKey__); |
271 }, | 279 }, |
272 | 280 |
273 /** | 281 /** |
274 * Look up an item in the local `data` Array by key. | 282 * Look up an item in the local `data` Array by key. |
275 * | 283 * |
276 * @param {String} key The key associated with the item in the parent | 284 * @param {String} key The key associated with the item in the parent |
277 * document. | 285 * document. |
278 */ | 286 */ |
279 getByKey: function(key) { | 287 getByKey: function(key) { |
280 return this._valueMap[key]; | 288 return this._valueMap[key]; |
281 }, | 289 }, |
282 | 290 |
283 /** | 291 /** |
284 * Remove an item from the document associated with `location` by key. | 292 * Remove an item from the document associated with `location` by key. |
285 * | 293 * |
286 * @param {String} key The key associated with the item in the document | 294 * @param {String} key The key associated with the item in the document |
287 * located at `location`. | 295 * located at `location`. |
288 */ | 296 */ |
289 removeByKey: function(key) { | 297 removeByKey: function(key) { |
| 298 if (!this.query) { |
| 299 this._error('Cannot remove items before query has been initialized.'); |
| 300 return; |
| 301 } |
| 302 |
290 this.query.ref().child(key).remove(); | 303 this.query.ref().child(key).remove(); |
291 }, | 304 }, |
292 | 305 |
293 _applyLocalDataChanges: function(change) { | 306 _localDataChanged: function(change) { |
294 var pathParts = change.path.split('.'); | 307 var pathParts = change.path.split('.'); |
295 var firebaseKey; | |
296 var key; | |
297 var value; | |
298 | 308 |
| 309 // We don't care about self-changes, and we don't respond directly to |
| 310 // length changes: |
299 if (pathParts.length < 2 || pathParts[1] === 'length') { | 311 if (pathParts.length < 2 || pathParts[1] === 'length') { |
300 return; | 312 return; |
301 } | 313 } |
302 | 314 |
303 if (pathParts[1] === 'splices') { | 315 // Handle splices via the adoption process. `indexSplices` is known to |
304 this._applySplicesToRemoteData(change.value.indexSplices); | 316 // sometimes be null, so guard against that. |
| 317 if (pathParts[1] === 'splices' && change.value.indexSplices != null) { |
| 318 this._adoptSplices(change.value.indexSplices); |
305 return; | 319 return; |
306 } | 320 } |
307 | 321 |
308 key = pathParts[1]; | 322 // Otherwise, the change is happening to a sub-path of the array. |
309 value = Polymer.Collection.get(change.base).getItem(key); | 323 this._applySubPathChange(change); |
| 324 }, |
310 | 325 |
311 // Temporarily remove the client-only `__firebaseKey__` property: | 326 _applySubPathChange: function(change) { |
312 firebaseKey = value.__firebaseKey__; | 327 var key = change.path.split('.')[1]; |
| 328 var value = Polymer.Collection.get(change.base).getItem(key); |
| 329 var firebaseKey = value.__firebaseKey__; |
| 330 |
| 331 // We don't want to accidentally reflect `__firebaseKey__` in the |
| 332 // remote data, so we remove it temporarily. `null` values will be |
| 333 // discarded by Firebase, so `delete` is not necessary: |
313 value.__firebaseKey__ = null; | 334 value.__firebaseKey__ = null; |
314 | |
315 this.query.ref().child(firebaseKey).set(value); | 335 this.query.ref().child(firebaseKey).set(value); |
316 | |
317 value.__firebaseKey__ = firebaseKey; | 336 value.__firebaseKey__ = firebaseKey; |
318 }, | 337 }, |
319 | 338 |
320 _applySplicesToRemoteData: function(splices) { | 339 _adoptSplices: function(splices) { |
321 this._log('splices', splices); | 340 this._pendingSplices = this._pendingSplices.concat(splices); |
322 splices.forEach(function(splice) { | |
323 var added = splice.object.slice(splice.index, splice.index + splice.ad
dedCount); | |
324 | 341 |
325 splice.removed.forEach(function(removed) { | 342 // We can afford apply removals synchronously, so we do that first |
326 this.remove(removed); | 343 // and save the `added` operations for the `debounce` below. |
| 344 this._applyLocalDataChange(function() { |
| 345 splices.forEach(function(splice) { |
| 346 splice.removed.forEach(function(removed) { |
| 347 this.remove(removed); |
| 348 }, this); |
327 }, this); | 349 }, this); |
| 350 }); |
328 | 351 |
329 added.forEach(function(added) { | 352 // We async until the next turn. The reason we want to do this is |
330 this.add(added); | 353 // that splicing within a splice handler will break the data binding |
331 }, this); | 354 // system in some places. This is referred to as the "re-entrancy" |
332 }, this); | 355 // problem. See polymer/polymer#2491. |
| 356 this.debounce('_adoptSplices', function() { |
| 357 this._applyLocalDataChange(function() { |
| 358 var splices = this._pendingSplices; |
| 359 |
| 360 this._pendingSplices = []; |
| 361 |
| 362 splices.forEach(function(splice) { |
| 363 var added = splice.object.slice(splice.index, splice.index + splic
e.addedCount); |
| 364 |
| 365 added.forEach(function(added, index) { |
| 366 this._lastLocallyAddedIndex = splice.index + index; |
| 367 this.add(added); |
| 368 }, this); |
| 369 }, this); |
| 370 |
| 371 this._lastLocallyAddedIndex = null; |
| 372 }); |
| 373 }); |
333 }, | 374 }, |
334 | 375 |
335 _computeQuery: function(location, limitToFirst, limitToLast, orderByMethod
Name, startAt, endAt, equalTo) { | 376 _computeQuery: function(location, limitToFirst, limitToLast, orderByMethod
Name, startAt, endAt, equalTo) { |
336 var query; | 377 var query; |
337 | 378 |
338 if (!location) { | 379 if (!location) { |
339 return; | 380 return; |
340 } | 381 } |
341 | 382 |
| 383 this._log('Recomputing query.', arguments); |
| 384 |
342 query = new Firebase(location); | 385 query = new Firebase(location); |
343 | 386 |
344 if (orderByMethodName) { | 387 if (orderByMethodName) { |
345 if (orderByMethodName === 'orderByChild') { | 388 if (orderByMethodName === 'orderByChild') { |
346 query = query[orderByMethodName](this.orderByChild); | 389 query = query[orderByMethodName](this.orderByChild); |
347 } else { | 390 } else { |
348 query = query[orderByMethodName](); | 391 query = query[orderByMethodName](); |
349 } | 392 } |
350 } | 393 } |
351 | 394 |
(...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
453 | 496 |
454 persistable[property] = value[property]; | 497 persistable[property] = value[property]; |
455 } | 498 } |
456 | 499 |
457 return persistable; | 500 return persistable; |
458 }, | 501 }, |
459 | 502 |
460 _onFirebaseChildAdded: function(event) { | 503 _onFirebaseChildAdded: function(event) { |
461 this._applyRemoteDataChange(function() { | 504 this._applyRemoteDataChange(function() { |
462 var value = this._valueFromSnapshot(event.detail.childSnapshot); | 505 var value = this._valueFromSnapshot(event.detail.childSnapshot); |
| 506 var key = value.__firebaseKey__; |
463 var previousValueKey = event.detail.previousChildName; | 507 var previousValueKey = event.detail.previousChildName; |
464 var index = previousValueKey != null ? | 508 var index = previousValueKey != null ? |
465 this.data.indexOf(this._valueMap[previousValueKey]) + 1 : 0; | 509 this.data.indexOf(this._valueMap[previousValueKey]) + 1 : 0; |
| 510 var lastLocallyAddedValue; |
466 | 511 |
467 this._valueMap[value.__firebaseKey__] = value; | 512 this._valueMap[value.__firebaseKey__] = value; |
468 | 513 |
469 this.splice('data', index, 0, value); | 514 // NOTE(cdata): The rationale for this conditional dance around the |
| 515 // last locally added index (since you will inevitably be wondering |
| 516 // why we do it): |
| 517 // There may be a "locally" added value which was spliced in. If |
| 518 // there is, it may be in the "wrong" place in the array. This is |
| 519 // due to the fact that Firebase may be applying a sort to the |
| 520 // data, so we won't know the correct index for the locally added |
| 521 // value until the `child_added` event is fired. |
| 522 // Once we get the `child_added` event, we can check to see if the |
| 523 // locally added value is in the right place. If it is, we just |
| 524 // `set` it to the Firebase-provided value. If it is not, then |
| 525 // we grab the original value, splice in the Firebase-provided |
| 526 // value in the right place, and then (importantly: at the end) |
| 527 // find the locally-added value again (since its index may have |
| 528 // changed by splicing-in Firebase's value) and splice it out of the |
| 529 // array. |
| 530 if (this._lastLocallyAddedIndex === index) { |
| 531 this.set(['data', index], value); |
| 532 } else { |
| 533 if (this._lastLocallyAddedIndex != null) { |
| 534 lastLocallyAddedValue = this.data[this._lastLocallyAddedIndex]; |
| 535 } |
| 536 |
| 537 this.splice('data', index, 0, value); |
| 538 |
| 539 if (this._lastLocallyAddedIndex != null) { |
| 540 this.splice('data', this.data.indexOf(lastLocallyAddedValue), 1); |
| 541 } |
| 542 } |
470 }); | 543 }); |
471 }, | 544 }, |
472 | 545 |
473 _onFirebaseChildRemoved: function(event) { | 546 _onFirebaseChildRemoved: function(event) { |
| 547 if (this._receivingLocalChanges) { |
| 548 this._valueMap[event.detail.oldChildSnapshot.key()] = null; |
| 549 // NOTE(cdata): If we are receiving local changes, that means that |
| 550 // the splices have already been performed and items are already |
| 551 // removed from the local data representation. No need to remove |
| 552 // them again. |
| 553 return; |
| 554 } |
| 555 |
474 this._applyRemoteDataChange(function() { | 556 this._applyRemoteDataChange(function() { |
475 var key = event.detail.oldChildSnapshot.key(); | 557 var key = event.detail.oldChildSnapshot.key(); |
476 var value = this._valueMap[key]; | 558 var value = this._valueMap[key]; |
477 var index; | 559 var index; |
478 | 560 |
479 if (!value) { | 561 if (!value) { |
480 this._error('Received firebase-child-removed event for unknown child
"' + key + '"'); | 562 this._error('Received firebase-child-removed event for unknown child
"' + key + '"'); |
481 return; | 563 return; |
482 } | 564 } |
483 | 565 |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
533 } | 615 } |
534 }); | 616 }); |
535 | 617 |
536 FirebaseCollection.ORDER_VALUE_TYPES = { | 618 FirebaseCollection.ORDER_VALUE_TYPES = { |
537 string: String, | 619 string: String, |
538 number: Number, | 620 number: Number, |
539 boolean: Boolean | 621 boolean: Boolean |
540 }; | 622 }; |
541 })(); | 623 })(); |
542 </script> | 624 </script> |
OLD | NEW |