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

Commit a0bc71e

Browse files
committed
fix(ngRepeat): prevent initial duplicates
1 parent a491ea3 commit a0bc71e

File tree

3 files changed

+33
-10
lines changed

3 files changed

+33
-10
lines changed

docs/content/cookbook/mvc.ngdoc

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ view.
8585
Next Player: {{nextMove}}
8686
<div class="winner" ng-show="winner">Player {{winner}} has won!</div>
8787
<table class="board">
88-
<tr ng-repeat="row in board" style="height:15px;">
89-
<td ng-repeat="cell in row" ng-style="cellStyle"
88+
<tr ng-repeat="row in board track by $index" style="height:15px;">
89+
<td ng-repeat="cell in row track by $index" ng-style="cellStyle"
9090
ng-click="dropPiece($parent.$index, $index)">{{cell}}</td>
9191
</tr>
9292
</table>

src/ng/directive/ngRepeat.js

+8-5
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
153153
var animate = $animator($scope, $attr);
154154
var expression = $attr.ngRepeat;
155155
var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
156-
trackByExp, hashExpFn, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier,
156+
trackByExp, trackByExpGetter, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier,
157157
hashFnLocals = {$id: hashKey};
158158

159159
if (!match) {
@@ -166,13 +166,13 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
166166
trackByExp = match[4];
167167

168168
if (trackByExp) {
169-
hashExpFn = $parse(trackByExp);
169+
trackByExpGetter = $parse(trackByExp);
170170
trackByIdFn = function(key, value, index) {
171171
// assign key, value, and $index to the locals so that they can be used in hash functions
172172
if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
173173
hashFnLocals[valueIdentifier] = value;
174174
hashFnLocals.$index = index;
175-
return hashExpFn($scope, hashFnLocals);
175+
return trackByExpGetter($scope, hashFnLocals);
176176
};
177177
} else {
178178
trackByIdFn = function(key, value) {
@@ -233,7 +233,8 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
233233
key = (collection === collectionKeys) ? index : collectionKeys[index];
234234
value = collection[key];
235235
trackById = trackByIdFn(key, value, index);
236-
if((block = lastBlockMap[trackById])) {
236+
if(lastBlockMap.hasOwnProperty(trackById)) {
237+
block = lastBlockMap[trackById]
237238
delete lastBlockMap[trackById];
238239
nextBlockMap[trackById] = block;
239240
nextBlockOrder[index] = block;
@@ -243,10 +244,12 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
243244
if (block && block.element) lastBlockMap[block.id] = block;
244245
});
245246
// This is a duplicate and we need to throw an error
246-
throw new Error('Duplicates in a repeater are not allowed. Repeater: ' + expression);
247+
throw new Error('Duplicates in a repeater are not allowed. Repeater: ' + expression +
248+
' key: ' + trackById);
247249
} else {
248250
// new never before seen block
249251
nextBlockOrder[index] = { id: trackById };
252+
nextBlockMap[trackById] = false;
250253
}
251254
}
252255

test/ng/directive/ngRepeatSpec.js

+23-3
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ describe('ngRepeat', function() {
391391

392392

393393
it('should iterate over non-existent elements of a sparse array', function() {
394-
element = $compile('<ul><li ng-repeat="item in array">{{item}}|</li></ul>')(scope);
394+
element = $compile('<ul><li ng-repeat="item in array track by $index">{{item}}|</li></ul>')(scope);
395395
scope.array = ['a', 'b'];
396396
scope.array[4] = 'c';
397397
scope.array[6] = 'd';
@@ -457,11 +457,31 @@ describe('ngRepeat', function() {
457457
});
458458

459459

460-
it('should throw error on duplicates and recover', function() {
460+
it('should throw error on adding existing duplicates and recover', function() {
461461
scope.items = [a, a, a];
462462
scope.$digest();
463463
expect($exceptionHandler.errors.shift().message).
464-
toEqual('Duplicates in a repeater are not allowed. Repeater: item in items');
464+
toEqual('Duplicates in a repeater are not allowed. Repeater: item in items key: object:003');
465+
466+
// recover
467+
scope.items = [a];
468+
scope.$digest();
469+
var newElements = element.find('li');
470+
expect(newElements.length).toEqual(1);
471+
expect(newElements[0]).toEqual(lis[0]);
472+
473+
scope.items = [];
474+
scope.$digest();
475+
var newElements = element.find('li');
476+
expect(newElements.length).toEqual(0);
477+
});
478+
479+
480+
it('should throw error on new duplicates and recover', function() {
481+
scope.items = [d, d, d];
482+
scope.$digest();
483+
expect($exceptionHandler.errors.shift().message).
484+
toEqual('Duplicates in a repeater are not allowed. Repeater: item in items key: object:009');
465485

466486
// recover
467487
scope.items = [a];

0 commit comments

Comments
 (0)