|
106 | 106 | // The `AngularFire` object that implements synchronization.
|
107 | 107 | AngularFire = function($q, $parse, $timeout, ref) {
|
108 | 108 | this._q = $q;
|
109 |
| - this._bound = false; |
110 |
| - this._loaded = false; |
111 | 109 | this._parse = $parse;
|
112 | 110 | this._timeout = $timeout;
|
113 | 111 |
|
| 112 | + // set to true when $bind is called, this tells us whether we need |
| 113 | + // to synchronize a $scope variable during data change events |
| 114 | + // and also whether we will need to $watch the variable for changes |
| 115 | + // we can only $bind to a single instance at a time |
| 116 | + this._bound = false; |
| 117 | + |
| 118 | + // true after the initial loading event completes, see _getInitialValue() |
| 119 | + this._loaded = false; |
| 120 | + |
| 121 | + // stores the list of keys if our data is an object, see $getIndex() |
114 | 122 | this._index = [];
|
115 | 123 |
|
116 | 124 | // An object storing handlers used for different events.
|
|
423 | 431 | },
|
424 | 432 |
|
425 | 433 | // This function is responsible for fetching the initial data for the
|
426 |
| - // given reference. If the data returned from the server is an object or |
427 |
| - // array, we'll attach appropriate child event handlers. If the value is |
428 |
| - // a primitive, we'll continue to watch for value changes. |
| 434 | + // given reference and attaching appropriate child event handlers. |
429 | 435 | _getInitialValue: function() {
|
430 | 436 | var self = this;
|
431 |
| - var gotInitialValue = function(snapshot) { |
432 |
| - var value = snapshot.val(); |
433 |
| - if (value === null) { |
434 |
| - // NULLs are handled specially. If there's a 3-way data binding |
435 |
| - // on a local primitive, then update that, otherwise switch to object |
436 |
| - // binding using child events. |
437 |
| - if (self._bound) { |
438 |
| - var local = self._parseObject(self._parse(self._name)(self._scope)); |
439 |
| - switch (typeof local) { |
440 |
| - // Primitive defaults. |
441 |
| - case "string": |
442 |
| - case "undefined": |
443 |
| - value = ""; |
444 |
| - break; |
445 |
| - case "number": |
446 |
| - value = 0; |
447 |
| - break; |
448 |
| - case "boolean": |
449 |
| - value = false; |
450 |
| - break; |
451 |
| - } |
452 |
| - } |
453 |
| - } |
454 | 437 |
|
455 |
| - // Call handlers for the "loaded" event. |
456 |
| - if (self._loaded !== true) { |
457 |
| - self._loaded = true; |
458 |
| - self._broadcastEvent("loaded", value); |
459 |
| - if( self._on.hasOwnProperty('child_added')) { |
460 |
| - self._iterateChildren(function(key, val, prevChild) { |
461 |
| - self._broadcastEvent('child_added', self._makeEventSnapshot(key, val, prevChild)); |
462 |
| - }); |
463 |
| - } |
464 |
| - } |
465 |
| - |
466 |
| - self._broadcastEvent('value', self._makeEventSnapshot(snapshot.name(), value, null)); |
467 |
| - |
468 |
| - switch (typeof value) { |
469 |
| - // For primitive values, simply update the object returned. |
470 |
| - case "string": |
471 |
| - case "number": |
472 |
| - case "boolean": |
473 |
| - self._updatePrimitive(value); |
474 |
| - break; |
475 |
| - // For arrays and objects, switch to child methods. |
476 |
| - case "object": |
477 |
| - self._fRef.off("value", gotInitialValue); |
478 |
| - // Before switching to child methods, save priority for top node. |
479 |
| - if (snapshot.getPriority() !== null) { |
480 |
| - self._updateModel("$priority", snapshot.getPriority()); |
481 |
| - } |
482 |
| - self._getChildValues(); |
483 |
| - break; |
484 |
| - default: |
485 |
| - throw new Error("Unexpected type from remote data " + typeof value); |
486 |
| - } |
487 |
| - }; |
488 |
| - |
489 |
| - self._fRef.on("value", gotInitialValue); |
490 |
| - }, |
491 |
| - |
492 |
| - // This function attaches child events for object and array types. |
493 |
| - _getChildValues: function() { |
494 |
| - var self = this; |
495 |
| - // Store the priority of the current property as "$priority". Changing |
496 |
| - // the value of this property will also update the priority of the |
497 |
| - // object (see _parseObject). |
| 438 | + // store changes to children and update the index of keys appropriately |
498 | 439 | function _processSnapshot(snapshot, prevChild) {
|
499 | 440 | var key = snapshot.name();
|
500 | 441 | var val = snapshot.val();
|
|
513 | 454 | self._index.unshift(key);
|
514 | 455 | }
|
515 | 456 |
|
516 |
| - // Update local model with priority field, if needed. |
517 |
| - if (snapshot.getPriority() !== null) { |
| 457 | + // Store the priority of the current property as "$priority". Changing |
| 458 | + // the value of this property will also update the priority of the |
| 459 | + // object (see _parseObject). |
| 460 | + if (!_isPrimitive(val) && snapshot.getPriority() !== null) { |
518 | 461 | val.$priority = snapshot.getPriority();
|
519 | 462 | }
|
520 | 463 | self._updateModel(key, val);
|
|
543 | 486 | // Remove from local model.
|
544 | 487 | self._updateModel(key, null);
|
545 | 488 | });
|
| 489 | + |
| 490 | + function _isPrimitive(v) { |
| 491 | + return v === null || typeof(v) !== 'object'; |
| 492 | + } |
| 493 | + |
| 494 | + function _initialLoad(value) { |
| 495 | + // Call handlers for the "loaded" event. |
| 496 | + self._loaded = true; |
| 497 | + self._broadcastEvent("loaded", value); |
| 498 | + } |
| 499 | + |
| 500 | + function handleNullValues(value) { |
| 501 | + // NULLs are handled specially. If there's a 3-way data binding |
| 502 | + // on a local primitive, then update that, otherwise switch to object |
| 503 | + // binding using child events. |
| 504 | + if (self._bound && value === null) { |
| 505 | + var local = self._parseObject(self._parse(self._name)(self._scope)); |
| 506 | + switch (typeof local) { |
| 507 | + // Primitive defaults. |
| 508 | + case "string": |
| 509 | + case "undefined": |
| 510 | + value = ""; |
| 511 | + break; |
| 512 | + case "number": |
| 513 | + value = 0; |
| 514 | + break; |
| 515 | + case "boolean": |
| 516 | + value = false; |
| 517 | + break; |
| 518 | + } |
| 519 | + } |
| 520 | + |
| 521 | + return value; |
| 522 | + } |
| 523 | + |
| 524 | + // We handle primitives and objects here together. There is no harm in having |
| 525 | + // child_* listeners attached; if the data suddenly changes between an object |
| 526 | + // and a primitive, the child_added/removed events will fire, and our data here |
| 527 | + // will get updated accordingly so we should be able to transition without issue |
546 | 528 | self._fRef.on('value', function(snap) {
|
547 |
| - self._broadcastEvent('value', self._makeEventSnapshot(snap.name(), snap.val())); |
| 529 | + // primitive handling |
| 530 | + var value = snap.val(); |
| 531 | + if( _isPrimitive(value) ) { |
| 532 | + value = handleNullValues(value); |
| 533 | + self._updatePrimitive(value); |
| 534 | + } |
| 535 | + else { |
| 536 | + delete self._object.$value; |
| 537 | + } |
| 538 | + |
| 539 | + // broadcast the value event |
| 540 | + self._broadcastEvent('value', self._makeEventSnapshot(snap.name(), value)); |
| 541 | + |
| 542 | + // broadcast initial loaded event once data and indices are set up appropriately |
| 543 | + if( !self._loaded ) { |
| 544 | + _initialLoad(value); |
| 545 | + } |
548 | 546 | });
|
549 | 547 | },
|
550 | 548 |
|
551 | 549 | // Called whenever there is a remote change. Applies them to the local
|
552 | 550 | // model for both explicit and implicit sync modes.
|
553 | 551 | _updateModel: function(key, value) {
|
554 |
| - var self = this; |
555 |
| - self._timeout(function() { |
556 |
| - if (value == null) { |
557 |
| - delete self._object[key]; |
558 |
| - } else { |
559 |
| - self._object[key] = value; |
560 |
| - } |
| 552 | + if (value == null) { |
| 553 | + delete this._object[key]; |
| 554 | + } else { |
| 555 | + this._object[key] = value; |
| 556 | + } |
561 | 557 |
|
562 |
| - // Call change handlers. |
563 |
| - self._broadcastEvent("change", key); |
| 558 | + // Call change handlers. |
| 559 | + this._broadcastEvent("change", key); |
564 | 560 |
|
565 |
| - // If there is an implicit binding, also update the local model. |
566 |
| - if (!self._bound) { |
567 |
| - return; |
568 |
| - } |
| 561 | + // update Angular by forcing a compile event |
| 562 | + this._triggerModelUpdate(); |
| 563 | + }, |
569 | 564 |
|
570 |
| - var current = self._object; |
571 |
| - var local = self._parse(self._name)(self._scope); |
572 |
| - // If remote value matches local value, don't do anything, otherwise |
573 |
| - // apply the change. |
574 |
| - if (!angular.equals(current, local)) { |
575 |
| - self._parse(self._name).assign(self._scope, angular.copy(current)); |
576 |
| - } |
577 |
| - }); |
| 565 | + // this method triggers a self._timeout event, which forces Angular to run $apply() |
| 566 | + // and compile the DOM content |
| 567 | + _triggerModelUpdate: function() { |
| 568 | + // since the timeout runs asynchronously, multiple updates could invoke this method |
| 569 | + // before it is actually executed (this occurs when Firebase sends it's initial deluge of data |
| 570 | + // back to our _getInitialValue() method, or when there are locally cached changes) |
| 571 | + // We don't want to trigger it multiple times if we can help, creating multiple dirty checks |
| 572 | + // and $apply operations, which are costly, so if one is already queued, we just wait for |
| 573 | + // it to do its work. |
| 574 | + if( !this._runningTimer ) { |
| 575 | + var self = this; |
| 576 | + this._runningTimer = self._timeout(function() { |
| 577 | + self._runningTimer = null; |
| 578 | + |
| 579 | + // If there is an implicit binding, also update the local model. |
| 580 | + if (!self._bound) { |
| 581 | + return; |
| 582 | + } |
| 583 | + |
| 584 | + var current = self._object; |
| 585 | + var local = self._parse(self._name)(self._scope); |
| 586 | + // If remote value matches local value, don't do anything, otherwise |
| 587 | + // apply the change. |
| 588 | + if (!angular.equals(current, local)) { |
| 589 | + self._parse(self._name).assign(self._scope, angular.copy(current)); |
| 590 | + } |
| 591 | + }); |
| 592 | + } |
578 | 593 | },
|
579 | 594 |
|
580 | 595 | // Called whenever there is a remote change for a primitive value.
|
|
0 commit comments