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

Commit 3945f88

Browse files
committed
fix(ng:repeat) with array ignore properties not representing array elements
Along the way I also changed the repeater impl to use for loop instead of for in loop. Iteration over objects is handled by creating an array of keys, which is sorted and this array then determines the order of iteration over an element. This makes repeating over objects deterministic and cross-browser compatible.
1 parent d5ccabc commit 3945f88

File tree

2 files changed

+77
-45
lines changed

2 files changed

+77
-45
lines changed

src/widgets.js

+55-43
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ angularWidget('@ng:repeat', function(expression, element){
361361
// We expect this to be a rare case.
362362
var lastOrder = new HashQueueMap();
363363
this.$watch(function(scope){
364-
var index = 0,
364+
var index, length,
365365
collection = scope.$eval(rhs),
366366
collectionLength = size(collection, true),
367367
childScope,
@@ -372,52 +372,64 @@ angularWidget('@ng:repeat', function(expression, element){
372372
array, last, // last object information {scope, element, index}
373373
cursor = iterStartElement; // current position of the node
374374

375-
for (key in collection) {
376-
if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
377-
last = lastOrder.shift(value = collection[key]);
378-
if (last) {
379-
// if we have already seen this object, then we need to reuse the
380-
// associated scope/element
381-
childScope = last.scope;
382-
nextOrder.push(value, last);
383-
384-
if (index === last.index) {
385-
// do nothing
386-
cursor = last.element;
387-
} else {
388-
// existing item which got moved
389-
last.index = index;
390-
// This may be a noop, if the element is next, but I don't know of a good way to
391-
// figure this out, since it would require extra DOM access, so let's just hope that
392-
// the browsers realizes that it is noop, and treats it as such.
393-
cursor.after(last.element);
394-
cursor = last.element;
395-
}
396-
} else {
397-
// new item which we don't know about
398-
childScope = parentScope.$new();
375+
if (!isArray(collection)) {
376+
// if object, extract keys, sort them and use to determine order of iteration over obj props
377+
array = [];
378+
for(key in collection) {
379+
if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
380+
array.push(key);
399381
}
382+
}
383+
array.sort();
384+
} else {
385+
array = collection || [];
386+
}
400387

401-
childScope[valueIdent] = collection[key];
402-
if (keyIdent) childScope[keyIdent] = key;
403-
childScope.$index = index;
404-
childScope.$position = index == 0
405-
? 'first'
406-
: (index == collectionLength - 1 ? 'last' : 'middle');
407-
408-
if (!last) {
409-
linker(childScope, function(clone){
410-
cursor.after(clone);
411-
last = {
412-
scope: childScope,
413-
element: (cursor = clone),
414-
index: index
415-
};
416-
nextOrder.push(value, last);
417-
});
388+
// we are not using forEach for perf reasons (trying to avoid #call)
389+
for (index = 0, length = array.length; index < length; index++) {
390+
key = (collection === array) ? index : array[index];
391+
value = collection[key];
392+
last = lastOrder.shift(value);
393+
if (last) {
394+
// if we have already seen this object, then we need to reuse the
395+
// associated scope/element
396+
childScope = last.scope;
397+
nextOrder.push(value, last);
398+
399+
if (index === last.index) {
400+
// do nothing
401+
cursor = last.element;
402+
} else {
403+
// existing item which got moved
404+
last.index = index;
405+
// This may be a noop, if the element is next, but I don't know of a good way to
406+
// figure this out, since it would require extra DOM access, so let's just hope that
407+
// the browsers realizes that it is noop, and treats it as such.
408+
cursor.after(last.element);
409+
cursor = last.element;
418410
}
411+
} else {
412+
// new item which we don't know about
413+
childScope = parentScope.$new();
414+
}
419415

420-
index ++;
416+
childScope[valueIdent] = value;
417+
if (keyIdent) childScope[keyIdent] = key;
418+
childScope.$index = index;
419+
childScope.$position = index == 0
420+
? 'first'
421+
: (index == collectionLength - 1 ? 'last' : 'middle');
422+
423+
if (!last) {
424+
linker(childScope, function(clone){
425+
cursor.after(clone);
426+
last = {
427+
scope: childScope,
428+
element: (cursor = clone),
429+
index: index
430+
};
431+
nextOrder.push(value, last);
432+
});
421433
}
422434
}
423435

test/widgetsSpec.js

+22-2
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ describe("widget", function() {
254254
'ng:bind="key + \':\' + val + $index + \'|\'"></li></ul>');
255255
scope.items = {'misko':'m', 'shyam':'s', 'frodo':'f'};
256256
scope.$digest();
257-
expect(element.text()).toEqual('misko:m0|shyam:s1|frodo:f2|');
257+
expect(element.text()).toEqual('frodo:f0|misko:m1|shyam:s2|');
258258
});
259259

260260
it('should expose iterator position as $position when iterating over arrays', function() {
@@ -282,7 +282,7 @@ describe("widget", function() {
282282
'</ul>');
283283
scope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f'};
284284
scope.$digest();
285-
expect(element.text()).toEqual('misko:m:first|shyam:s:middle|doug:d:middle|frodo:f:last|');
285+
expect(element.text()).toEqual('doug:d:first|frodo:f:middle|misko:m:middle|shyam:s:last|');
286286

287287
delete scope.items.doug;
288288
delete scope.items.frodo;
@@ -312,6 +312,26 @@ describe("widget", function() {
312312
expect(element.text()).toEqual('a|b|Xc|d|X');
313313
});
314314

315+
it('should ignore non-array element properties when iterating over an array', function() {
316+
var scope = compile('<ul><li ng:repeat="item in array">{{item}}|</li></ul>');
317+
scope.array = ['a', 'b', 'c'];
318+
scope.array.foo = '23';
319+
scope.array.bar = function() {};
320+
scope.$digest();
321+
322+
expect(element.text()).toBe('a|b|c|');
323+
});
324+
325+
it('should iterate over non-existent elements of a sparse array', function() {
326+
var scope = compile('<ul><li ng:repeat="item in array">{{item}}|</li></ul>');
327+
scope.array = ['a', 'b'];
328+
scope.array[4] = 'c';
329+
scope.array[6] = 'd';
330+
scope.$digest();
331+
332+
expect(element.text()).toBe('a|b|||c||d|');
333+
});
334+
315335

316336
describe('stability', function() {
317337
var a, b, c, d, scope, lis;

0 commit comments

Comments
 (0)