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

Commit 5eb9685

Browse files
matskomhevery
authored andcommitted
feat(Scope): add $watchCollection method for observing collections
The new method allows to shallow watch collections (Arrays/Maps).
1 parent 04cc1d2 commit 5eb9685

File tree

2 files changed

+300
-0
lines changed

2 files changed

+300
-0
lines changed

src/ng/rootScope.js

+141
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,147 @@ function $RootScopeProvider(){
320320
};
321321
},
322322

323+
324+
/**
325+
* @ngdoc function
326+
* @name ng.$rootScope.Scope#$watchCollection
327+
* @methodOf ng.$rootScope.Scope
328+
* @function
329+
*
330+
* @description
331+
* Shallow watches the properties of an object and fires whenever any of the properties change
332+
* (for arrays this implies watching the array items, for object maps this implies watching the properties).
333+
* If a change is detected the `listener` callback is fired.
334+
*
335+
* - The `obj` collection is observed via standard $watch operation and is examined on every call to $digest() to
336+
* see if any items have been added, removed, or moved.
337+
* - The `listener` is called whenever anything within the `obj` has changed. Examples include adding new items
338+
* into the object or array, removing and moving items around.
339+
*
340+
*
341+
* # Example
342+
* <pre>
343+
$scope.names = ['igor', 'matias', 'misko', 'james'];
344+
$scope.dataCount = 4;
345+
346+
$scope.$watchCollection('names', function(newNames, oldNames) {
347+
$scope.dataCount = newNames.length;
348+
});
349+
350+
expect($scope.dataCount).toEqual(4);
351+
$scope.$digest();
352+
353+
//still at 4 ... no changes
354+
expect($scope.dataCount).toEqual(4);
355+
356+
$scope.names.pop();
357+
$scope.$digest();
358+
359+
//now there's been a change
360+
expect($scope.dataCount).toEqual(3);
361+
* </pre>
362+
*
363+
*
364+
* @param {string|Function(scope)} obj Evaluated as {@link guide/expression expression}. The expression value
365+
* should evaluate to an object or an array which is observed on each
366+
* {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the collection will trigger
367+
* a call to the `listener`.
368+
*
369+
* @param {function(newCollection, oldCollection, scope)} listener a callback function that is fired with both
370+
* the `newCollection` and `oldCollection` as parameters.
371+
* The `newCollection` object is the newly modified data obtained from the `obj` expression and the
372+
* `oldCollection` object is a copy of the former collection data.
373+
* The `scope` refers to the current scope.
374+
*
375+
* @returns {function()} Returns a de-registration function for this listener. When the de-registration function is executed
376+
* then the internal watch operation is terminated.
377+
*/
378+
$watchCollection: function(obj, listener) {
379+
var self = this;
380+
var oldValue;
381+
var newValue;
382+
var changeDetected = 0;
383+
var objGetter = $parse(obj);
384+
var internalArray = [];
385+
var internalObject = {};
386+
var oldLength = 0;
387+
388+
function $watchCollectionWatch() {
389+
newValue = objGetter(self);
390+
var newLength, key;
391+
392+
if (!isObject(newValue)) {
393+
if (oldValue !== newValue) {
394+
oldValue = newValue;
395+
changeDetected++;
396+
}
397+
} else if (isArray(newValue)) {
398+
if (oldValue !== internalArray) {
399+
// we are transitioning from something which was not an array into array.
400+
oldValue = internalArray;
401+
oldLength = oldValue.length = 0;
402+
changeDetected++;
403+
}
404+
405+
newLength = newValue.length;
406+
407+
if (oldLength !== newLength) {
408+
// if lengths do not match we need to trigger change notification
409+
changeDetected++;
410+
oldValue.length = oldLength = newLength;
411+
}
412+
// copy the items to oldValue and look for changes.
413+
for (var i = 0; i < newLength; i++) {
414+
if (oldValue[i] !== newValue[i]) {
415+
changeDetected++;
416+
oldValue[i] = newValue[i];
417+
}
418+
}
419+
} else {
420+
if (oldValue !== internalObject) {
421+
// we are transitioning from something which was not an object into object.
422+
oldValue = internalObject = {};
423+
oldLength = 0;
424+
changeDetected++;
425+
}
426+
// copy the items to oldValue and look for changes.
427+
newLength = 0;
428+
for (key in newValue) {
429+
if (newValue.hasOwnProperty(key)) {
430+
newLength++;
431+
if (oldValue.hasOwnProperty(key)) {
432+
if (oldValue[key] !== newValue[key]) {
433+
changeDetected++;
434+
oldValue[key] = newValue[key];
435+
}
436+
} else {
437+
oldLength++;
438+
oldValue[key] = newValue[key];
439+
changeDetected++;
440+
}
441+
}
442+
}
443+
if (oldLength > newLength) {
444+
// we used to have more keys, need to find them and destroy them.
445+
changeDetected++;
446+
for(key in oldValue) {
447+
if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) {
448+
oldLength--;
449+
delete oldValue[key];
450+
}
451+
}
452+
}
453+
}
454+
return changeDetected;
455+
}
456+
457+
function $watchCollectionAction() {
458+
listener(newValue, oldValue, self);
459+
}
460+
461+
return this.$watch($watchCollectionWatch, $watchCollectionAction);
462+
},
463+
323464
/**
324465
* @ngdoc function
325466
* @name ng.$rootScope.Scope#$digest

test/ng/rootScopeSpec.js

+159
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,165 @@ describe('Scope', function() {
362362
$rootScope.$digest();
363363
expect(log).toEqual([]);
364364
}));
365+
366+
describe('$watchCollection', function() {
367+
var log, $rootScope, deregister;
368+
369+
beforeEach(inject(function(_$rootScope_) {
370+
log = [];
371+
$rootScope = _$rootScope_;
372+
deregister = $rootScope.$watchCollection('obj', function logger(obj) {
373+
log.push(toJson(obj));
374+
});
375+
}));
376+
377+
378+
it('should not trigger if nothing change', inject(function($rootScope) {
379+
$rootScope.$digest();
380+
expect(log).toEqual([undefined]);
381+
382+
$rootScope.$digest();
383+
expect(log).toEqual([undefined]);
384+
}));
385+
386+
387+
it('should allow deregistration', inject(function($rootScope) {
388+
$rootScope.obj = [];
389+
$rootScope.$digest();
390+
391+
expect(log).toEqual(['[]']);
392+
393+
$rootScope.obj.push('a');
394+
deregister();
395+
396+
$rootScope.$digest();
397+
expect(log).toEqual(['[]']);
398+
}));
399+
400+
401+
describe('array', function() {
402+
it('should trigger when property changes into array', function() {
403+
$rootScope.obj = 'test';
404+
$rootScope.$digest();
405+
expect(log).toEqual(['"test"']);
406+
407+
$rootScope.obj = [];
408+
$rootScope.$digest();
409+
expect(log).toEqual(['"test"', '[]']);
410+
411+
$rootScope.obj = {};
412+
$rootScope.$digest();
413+
expect(log).toEqual(['"test"', '[]', '{}']);
414+
415+
$rootScope.obj = [];
416+
$rootScope.$digest();
417+
expect(log).toEqual(['"test"', '[]', '{}', '[]']);
418+
419+
$rootScope.obj = undefined;
420+
$rootScope.$digest();
421+
expect(log).toEqual(['"test"', '[]', '{}', '[]', undefined]);
422+
});
423+
424+
425+
it('should not trigger change when object in collection changes', function() {
426+
$rootScope.obj = [{}];
427+
$rootScope.$digest();
428+
expect(log).toEqual(['[{}]']);
429+
430+
$rootScope.obj[0].name = 'foo';
431+
$rootScope.$digest();
432+
expect(log).toEqual(['[{}]']);
433+
});
434+
435+
436+
it('should watch array properties', function() {
437+
$rootScope.obj = [];
438+
$rootScope.$digest();
439+
expect(log).toEqual(['[]']);
440+
441+
$rootScope.obj.push('a');
442+
$rootScope.$digest();
443+
expect(log).toEqual(['[]', '["a"]']);
444+
445+
$rootScope.obj[0] = 'b';
446+
$rootScope.$digest();
447+
expect(log).toEqual(['[]', '["a"]', '["b"]']);
448+
449+
$rootScope.obj.push([]);
450+
$rootScope.obj.push({});
451+
log = [];
452+
$rootScope.$digest();
453+
expect(log).toEqual(['["b",[],{}]']);
454+
455+
var temp = $rootScope.obj[1];
456+
$rootScope.obj[1] = $rootScope.obj[2];
457+
$rootScope.obj[2] = temp;
458+
$rootScope.$digest();
459+
expect(log).toEqual([ '["b",[],{}]', '["b",{},[]]' ]);
460+
461+
$rootScope.obj.shift()
462+
log = [];
463+
$rootScope.$digest();
464+
expect(log).toEqual([ '[{},[]]' ]);
465+
});
466+
});
467+
468+
469+
describe('object', function() {
470+
it('should trigger when property changes into object', function() {
471+
$rootScope.obj = 'test';
472+
$rootScope.$digest();
473+
expect(log).toEqual(['"test"']);
474+
475+
$rootScope.obj = {};
476+
$rootScope.$digest();
477+
expect(log).toEqual(['"test"', '{}']);
478+
});
479+
480+
481+
it('should not trigger change when object in collection changes', function() {
482+
$rootScope.obj = {name: {}};
483+
$rootScope.$digest();
484+
expect(log).toEqual(['{"name":{}}']);
485+
486+
$rootScope.obj.name.bar = 'foo';
487+
$rootScope.$digest();
488+
expect(log).toEqual(['{"name":{}}']);
489+
});
490+
491+
492+
it('should watch object properties', function() {
493+
$rootScope.obj = {};
494+
$rootScope.$digest();
495+
expect(log).toEqual(['{}']);
496+
497+
$rootScope.obj.a= 'A';
498+
$rootScope.$digest();
499+
expect(log).toEqual(['{}', '{"a":"A"}']);
500+
501+
$rootScope.obj.a = 'B';
502+
$rootScope.$digest();
503+
expect(log).toEqual(['{}', '{"a":"A"}', '{"a":"B"}']);
504+
505+
$rootScope.obj.b = [];
506+
$rootScope.obj.c = {};
507+
log = [];
508+
$rootScope.$digest();
509+
expect(log).toEqual(['{"a":"B","b":[],"c":{}}']);
510+
511+
var temp = $rootScope.obj.a;
512+
$rootScope.obj.a = $rootScope.obj.b;
513+
$rootScope.obj.c = temp;
514+
$rootScope.$digest();
515+
expect(log).toEqual([ '{"a":"B","b":[],"c":{}}', '{"a":[],"b":[],"c":"B"}' ]);
516+
517+
delete $rootScope.obj.a;
518+
log = [];
519+
$rootScope.$digest();
520+
expect(log).toEqual([ '{"b":[],"c":"B"}' ]);
521+
})
522+
});
523+
});
365524
});
366525

367526

0 commit comments

Comments
 (0)