aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/ng/rootScope.js141
-rw-r--r--test/ng/rootScopeSpec.js159
2 files changed, 300 insertions, 0 deletions
diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js
index 48ba038f..1fad7b0e 100644
--- a/src/ng/rootScope.js
+++ b/src/ng/rootScope.js
@@ -320,6 +320,147 @@ function $RootScopeProvider(){
};
},
+
+ /**
+ * @ngdoc function
+ * @name ng.$rootScope.Scope#$watchCollection
+ * @methodOf ng.$rootScope.Scope
+ * @function
+ *
+ * @description
+ * Shallow watches the properties of an object and fires whenever any of the properties change
+ * (for arrays this implies watching the array items, for object maps this implies watching the properties).
+ * If a change is detected the `listener` callback is fired.
+ *
+ * - The `obj` collection is observed via standard $watch operation and is examined on every call to $digest() to
+ * see if any items have been added, removed, or moved.
+ * - The `listener` is called whenever anything within the `obj` has changed. Examples include adding new items
+ * into the object or array, removing and moving items around.
+ *
+ *
+ * # Example
+ * <pre>
+ $scope.names = ['igor', 'matias', 'misko', 'james'];
+ $scope.dataCount = 4;
+
+ $scope.$watchCollection('names', function(newNames, oldNames) {
+ $scope.dataCount = newNames.length;
+ });
+
+ expect($scope.dataCount).toEqual(4);
+ $scope.$digest();
+
+ //still at 4 ... no changes
+ expect($scope.dataCount).toEqual(4);
+
+ $scope.names.pop();
+ $scope.$digest();
+
+ //now there's been a change
+ expect($scope.dataCount).toEqual(3);
+ * </pre>
+ *
+ *
+ * @param {string|Function(scope)} obj Evaluated as {@link guide/expression expression}. The expression value
+ * should evaluate to an object or an array which is observed on each
+ * {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the collection will trigger
+ * a call to the `listener`.
+ *
+ * @param {function(newCollection, oldCollection, scope)} listener a callback function that is fired with both
+ * the `newCollection` and `oldCollection` as parameters.
+ * The `newCollection` object is the newly modified data obtained from the `obj` expression and the
+ * `oldCollection` object is a copy of the former collection data.
+ * The `scope` refers to the current scope.
+ *
+ * @returns {function()} Returns a de-registration function for this listener. When the de-registration function is executed
+ * then the internal watch operation is terminated.
+ */
+ $watchCollection: function(obj, listener) {
+ var self = this;
+ var oldValue;
+ var newValue;
+ var changeDetected = 0;
+ var objGetter = $parse(obj);
+ var internalArray = [];
+ var internalObject = {};
+ var oldLength = 0;
+
+ function $watchCollectionWatch() {
+ newValue = objGetter(self);
+ var newLength, key;
+
+ if (!isObject(newValue)) {
+ if (oldValue !== newValue) {
+ oldValue = newValue;
+ changeDetected++;
+ }
+ } else if (isArray(newValue)) {
+ if (oldValue !== internalArray) {
+ // we are transitioning from something which was not an array into array.
+ oldValue = internalArray;
+ oldLength = oldValue.length = 0;
+ changeDetected++;
+ }
+
+ newLength = newValue.length;
+
+ if (oldLength !== newLength) {
+ // if lengths do not match we need to trigger change notification
+ changeDetected++;
+ oldValue.length = oldLength = newLength;
+ }
+ // copy the items to oldValue and look for changes.
+ for (var i = 0; i < newLength; i++) {
+ if (oldValue[i] !== newValue[i]) {
+ changeDetected++;
+ oldValue[i] = newValue[i];
+ }
+ }
+ } else {
+ if (oldValue !== internalObject) {
+ // we are transitioning from something which was not an object into object.
+ oldValue = internalObject = {};
+ oldLength = 0;
+ changeDetected++;
+ }
+ // copy the items to oldValue and look for changes.
+ newLength = 0;
+ for (key in newValue) {
+ if (newValue.hasOwnProperty(key)) {
+ newLength++;
+ if (oldValue.hasOwnProperty(key)) {
+ if (oldValue[key] !== newValue[key]) {
+ changeDetected++;
+ oldValue[key] = newValue[key];
+ }
+ } else {
+ oldLength++;
+ oldValue[key] = newValue[key];
+ changeDetected++;
+ }
+ }
+ }
+ if (oldLength > newLength) {
+ // we used to have more keys, need to find them and destroy them.
+ changeDetected++;
+ for(key in oldValue) {
+ if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) {
+ oldLength--;
+ delete oldValue[key];
+ }
+ }
+ }
+ }
+ return changeDetected;
+ }
+
+ function $watchCollectionAction() {
+ listener(newValue, oldValue, self);
+ }
+
+ return this.$watch($watchCollectionWatch, $watchCollectionAction);
+ },
+
/**
* @ngdoc function
* @name ng.$rootScope.Scope#$digest
diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js
index 33db814c..f4b6e8a1 100644
--- a/test/ng/rootScopeSpec.js
+++ b/test/ng/rootScopeSpec.js
@@ -362,6 +362,165 @@ describe('Scope', function() {
$rootScope.$digest();
expect(log).toEqual([]);
}));
+
+ describe('$watchCollection', function() {
+ var log, $rootScope, deregister;
+
+ beforeEach(inject(function(_$rootScope_) {
+ log = [];
+ $rootScope = _$rootScope_;
+ deregister = $rootScope.$watchCollection('obj', function logger(obj) {
+ log.push(toJson(obj));
+ });
+ }));
+
+
+ it('should not trigger if nothing change', inject(function($rootScope) {
+ $rootScope.$digest();
+ expect(log).toEqual([undefined]);
+
+ $rootScope.$digest();
+ expect(log).toEqual([undefined]);
+ }));
+
+
+ it('should allow deregistration', inject(function($rootScope) {
+ $rootScope.obj = [];
+ $rootScope.$digest();
+
+ expect(log).toEqual(['[]']);
+
+ $rootScope.obj.push('a');
+ deregister();
+
+ $rootScope.$digest();
+ expect(log).toEqual(['[]']);
+ }));
+
+
+ describe('array', function() {
+ it('should trigger when property changes into array', function() {
+ $rootScope.obj = 'test';
+ $rootScope.$digest();
+ expect(log).toEqual(['"test"']);
+
+ $rootScope.obj = [];
+ $rootScope.$digest();
+ expect(log).toEqual(['"test"', '[]']);
+
+ $rootScope.obj = {};
+ $rootScope.$digest();
+ expect(log).toEqual(['"test"', '[]', '{}']);
+
+ $rootScope.obj = [];
+ $rootScope.$digest();
+ expect(log).toEqual(['"test"', '[]', '{}', '[]']);
+
+ $rootScope.obj = undefined;
+ $rootScope.$digest();
+ expect(log).toEqual(['"test"', '[]', '{}', '[]', undefined]);
+ });
+
+
+ it('should not trigger change when object in collection changes', function() {
+ $rootScope.obj = [{}];
+ $rootScope.$digest();
+ expect(log).toEqual(['[{}]']);
+
+ $rootScope.obj[0].name = 'foo';
+ $rootScope.$digest();
+ expect(log).toEqual(['[{}]']);
+ });
+
+
+ it('should watch array properties', function() {
+ $rootScope.obj = [];
+ $rootScope.$digest();
+ expect(log).toEqual(['[]']);
+
+ $rootScope.obj.push('a');
+ $rootScope.$digest();
+ expect(log).toEqual(['[]', '["a"]']);
+
+ $rootScope.obj[0] = 'b';
+ $rootScope.$digest();
+ expect(log).toEqual(['[]', '["a"]', '["b"]']);
+
+ $rootScope.obj.push([]);
+ $rootScope.obj.push({});
+ log = [];
+ $rootScope.$digest();
+ expect(log).toEqual(['["b",[],{}]']);
+
+ var temp = $rootScope.obj[1];
+ $rootScope.obj[1] = $rootScope.obj[2];
+ $rootScope.obj[2] = temp;
+ $rootScope.$digest();
+ expect(log).toEqual([ '["b",[],{}]', '["b",{},[]]' ]);
+
+ $rootScope.obj.shift()
+ log = [];
+ $rootScope.$digest();
+ expect(log).toEqual([ '[{},[]]' ]);
+ });
+ });
+
+
+ describe('object', function() {
+ it('should trigger when property changes into object', function() {
+ $rootScope.obj = 'test';
+ $rootScope.$digest();
+ expect(log).toEqual(['"test"']);
+
+ $rootScope.obj = {};
+ $rootScope.$digest();
+ expect(log).toEqual(['"test"', '{}']);
+ });
+
+
+ it('should not trigger change when object in collection changes', function() {
+ $rootScope.obj = {name: {}};
+ $rootScope.$digest();
+ expect(log).toEqual(['{"name":{}}']);
+
+ $rootScope.obj.name.bar = 'foo';
+ $rootScope.$digest();
+ expect(log).toEqual(['{"name":{}}']);
+ });
+
+
+ it('should watch object properties', function() {
+ $rootScope.obj = {};
+ $rootScope.$digest();
+ expect(log).toEqual(['{}']);
+
+ $rootScope.obj.a= 'A';
+ $rootScope.$digest();
+ expect(log).toEqual(['{}', '{"a":"A"}']);
+
+ $rootScope.obj.a = 'B';
+ $rootScope.$digest();
+ expect(log).toEqual(['{}', '{"a":"A"}', '{"a":"B"}']);
+
+ $rootScope.obj.b = [];
+ $rootScope.obj.c = {};
+ log = [];
+ $rootScope.$digest();
+ expect(log).toEqual(['{"a":"B","b":[],"c":{}}']);
+
+ var temp = $rootScope.obj.a;
+ $rootScope.obj.a = $rootScope.obj.b;
+ $rootScope.obj.c = temp;
+ $rootScope.$digest();
+ expect(log).toEqual([ '{"a":"B","b":[],"c":{}}', '{"a":[],"b":[],"c":"B"}' ]);
+
+ delete $rootScope.obj.a;
+ log = [];
+ $rootScope.$digest();
+ expect(log).toEqual([ '{"b":[],"c":"B"}' ]);
+ })
+ });
+ });
});