Skip to content

Commit ed461ab

Browse files
committed
Fixes FirebaseExtended#276 and fixes FirebaseExtended#262, making $getIndex() work in conjunction with 'loaded' event, also improves several edge cases for primitives and child event notifications.
1 parent 91b63f6 commit ed461ab

File tree

1 file changed

+110
-95
lines changed

1 file changed

+110
-95
lines changed

angularfire.js

+110-95
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,19 @@
106106
// The `AngularFire` object that implements synchronization.
107107
AngularFire = function($q, $parse, $timeout, ref) {
108108
this._q = $q;
109-
this._bound = false;
110-
this._loaded = false;
111109
this._parse = $parse;
112110
this._timeout = $timeout;
113111

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()
114122
this._index = [];
115123

116124
// An object storing handlers used for different events.
@@ -423,78 +431,11 @@
423431
},
424432

425433
// 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.
429435
_getInitialValue: function() {
430436
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-
}
454437

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
498439
function _processSnapshot(snapshot, prevChild) {
499440
var key = snapshot.name();
500441
var val = snapshot.val();
@@ -513,8 +454,10 @@
513454
self._index.unshift(key);
514455
}
515456

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) {
518461
val.$priority = snapshot.getPriority();
519462
}
520463
self._updateModel(key, val);
@@ -543,38 +486,110 @@
543486
// Remove from local model.
544487
self._updateModel(key, null);
545488
});
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
546528
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+
}
548546
});
549547
},
550548

551549
// Called whenever there is a remote change. Applies them to the local
552550
// model for both explicit and implicit sync modes.
553551
_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+
}
561557

562-
// Call change handlers.
563-
self._broadcastEvent("change", key);
558+
// Call change handlers.
559+
this._broadcastEvent("change", key);
564560

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+
},
569564

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+
}
578593
},
579594

580595
// Called whenever there is a remote change for a primitive value.

0 commit comments

Comments
 (0)