mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-16 23:30:23 +00:00
feat(Scope): add $watchCollection method for observing collections
The new method allows to shallow watch collections (Arrays/Maps).
This commit is contained in:
parent
04cc1d2890
commit
5eb968553a
2 changed files with 300 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}' ]);
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue