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

Commit d4d1031

Browse files
jbedardmgol
authored andcommitted
fix(ngRepeat): fix trackBy function being invoked with incorrect scope
Also fixes a leak of that scope across all further instances of the repeated element. Fixes #16776 Closes #16777
1 parent 7981140 commit d4d1031

File tree

2 files changed

+55
-20
lines changed

2 files changed

+55
-20
lines changed

src/ng/directive/ngRepeat.js

+18-20
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,13 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
454454
return block.clone[block.clone.length - 1];
455455
};
456456

457+
var trackByIdArrayFn = function($scope, key, value) {
458+
return hashKey(value);
459+
};
460+
461+
var trackByIdObjFn = function($scope, key) {
462+
return key;
463+
};
457464

458465
return {
459466
restrict: 'A',
@@ -493,32 +500,23 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
493500
aliasAs);
494501
}
495502

496-
var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn;
497-
var hashFnLocals = {$id: hashKey};
503+
var trackByIdExpFn;
498504

499505
if (trackByExp) {
500-
trackByExpGetter = $parse(trackByExp);
501-
} else {
502-
trackByIdArrayFn = function(key, value) {
503-
return hashKey(value);
504-
};
505-
trackByIdObjFn = function(key) {
506-
return key;
506+
var hashFnLocals = {$id: hashKey};
507+
var trackByExpGetter = $parse(trackByExp);
508+
509+
trackByIdExpFn = function($scope, key, value, index) {
510+
// assign key, value, and $index to the locals so that they can be used in hash functions
511+
if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
512+
hashFnLocals[valueIdentifier] = value;
513+
hashFnLocals.$index = index;
514+
return trackByExpGetter($scope, hashFnLocals);
507515
};
508516
}
509517

510518
return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
511519

512-
if (trackByExpGetter) {
513-
trackByIdExpFn = function(key, value, index) {
514-
// assign key, value, and $index to the locals so that they can be used in hash functions
515-
if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
516-
hashFnLocals[valueIdentifier] = value;
517-
hashFnLocals.$index = index;
518-
return trackByExpGetter($scope, hashFnLocals);
519-
};
520-
}
521-
522520
// Store a list of elements from previous run. This is a hash where key is the item from the
523521
// iterator, and the value is objects with following properties.
524522
// - scope: bound scope
@@ -572,7 +570,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
572570
for (index = 0; index < collectionLength; index++) {
573571
key = (collection === collectionKeys) ? index : collectionKeys[index];
574572
value = collection[key];
575-
trackById = trackByIdFn(key, value, index);
573+
trackById = trackByIdFn($scope, key, value, index);
576574
if (lastBlockMap[trackById]) {
577575
// found previously seen block
578576
block = lastBlockMap[trackById];

test/ng/directive/ngRepeatSpec.js

+37
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,43 @@ describe('ngRepeat', function() {
400400
expect(element.find('input')[1].checked).toBe(true);
401401
expect(element.find('input')[2].checked).toBe(true);
402402
}));
403+
404+
it('should invoke track by with correct locals', function() {
405+
scope.trackBy = jasmine.createSpy().and.callFake(function(k, v) {
406+
return [k, v].join('');
407+
});
408+
409+
element = $compile(
410+
'<ul>' +
411+
'<li ng-repeat="(k, v) in [1, 2] track by trackBy(k, v)"></li>' +
412+
'</ul>')(scope);
413+
scope.$digest();
414+
415+
expect(scope.trackBy).toHaveBeenCalledTimes(2);
416+
expect(scope.trackBy.calls.argsFor(0)).toEqual([0, 1]);
417+
expect(scope.trackBy.calls.argsFor(1)).toEqual([1, 2]);
418+
});
419+
420+
// https://github.com/angular/angular.js/issues/16776
421+
it('should invoke nested track by with correct locals', function() {
422+
scope.trackBy = jasmine.createSpy().and.callFake(function(k1, v1, k2, v2) {
423+
return [k1, v1, k2, v2].join('');
424+
});
425+
426+
element = $compile(
427+
'<ul>' +
428+
'<li ng-repeat="(k1, v1) in [1, 2]">' +
429+
'<div ng-repeat="(k2, v2) in [3, 4] track by trackBy(k1, v1, k2, v2)"></div>' +
430+
'</li>' +
431+
'</ul>')(scope);
432+
scope.$digest();
433+
434+
expect(scope.trackBy).toHaveBeenCalledTimes(4);
435+
expect(scope.trackBy.calls.argsFor(0)).toEqual([0, 1, 0, 3]);
436+
expect(scope.trackBy.calls.argsFor(1)).toEqual([0, 1, 1, 4]);
437+
expect(scope.trackBy.calls.argsFor(2)).toEqual([1, 2, 0, 3]);
438+
expect(scope.trackBy.calls.argsFor(3)).toEqual([1, 2, 1, 4]);
439+
});
403440
});
404441

405442
describe('alias as', function() {

0 commit comments

Comments
 (0)