Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
feat(Scope): add $watchCollection method for observing collections
Browse files Browse the repository at this point in the history
The new method allows to shallow watch collections (Arrays/Maps).
  • Loading branch information
matsko authored and mhevery committed Mar 30, 2013
1 parent 04cc1d2 commit 5eb9685
Show file tree
Hide file tree
Showing 2 changed files with 300 additions and 0 deletions.
141 changes: 141 additions & 0 deletions src/ng/rootScope.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
159 changes: 159 additions & 0 deletions test/ng/rootScopeSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"}' ]);
})
});
});
});


Expand Down

3 comments on commit 5eb9685

@lrlopez
Copy link
Contributor

@lrlopez lrlopez commented on 5eb9685 Jul 4, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matsko, @mhevery ... This PR makes IE8 fail some tests on my VM. Is there a problem with my setup?

IE 8.0 (Windows) Scope $watch/$digest $watchCollection should not trigger if nothing change FAILED
        Expected [ 'undefined' ] to equal [ undefined ].
        Expected [ 'undefined' ] to equal [ undefined ].
IE 8.0 (Windows) Scope $watch/$digest $watchCollection array should trigger when property changes into array FAILED
        Expected [ '"test"', '[]', '{}', '[]', 'undefined' ] to equal [ '"test"', '[]', '{}', '[]', undefined ].
IE 8.0 (Windows): Executed 1872 of 1872 (2 FAILED) (2 mins 18.751 secs / 2 mins 8.954 secs)

@matsko
Copy link
Contributor Author

@matsko matsko commented on 5eb9685 Jul 4, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be running IE8 in a strict standards mode. Try running it in quirks mode.

@lrlopez
Copy link
Contributor

@lrlopez lrlopez commented on 5eb9685 Jul 4, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about the newbie question. I've enabled "compatibility view" (that is quirks mode, right?) for all sites, but now Karma refuses to start any tests. Can you point me how to run the tests in quirks mode?

Please sign in to comment.