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

Commit

Permalink
fix(ng:repeat) with array ignore properties not representing array el…
Browse files Browse the repository at this point in the history
…ements

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.
  • Loading branch information
IgorMinar committed Oct 26, 2011
1 parent d5ccabc commit 3945f88
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 45 deletions.
98 changes: 55 additions & 43 deletions src/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ angularWidget('@ng:repeat', function(expression, element){
// We expect this to be a rare case.
var lastOrder = new HashQueueMap();
this.$watch(function(scope){
var index = 0,
var index, length,
collection = scope.$eval(rhs),
collectionLength = size(collection, true),
childScope,
Expand All @@ -372,52 +372,64 @@ angularWidget('@ng:repeat', function(expression, element){
array, last, // last object information {scope, element, index}
cursor = iterStartElement; // current position of the node

for (key in collection) {
if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
last = lastOrder.shift(value = collection[key]);
if (last) {
// if we have already seen this object, then we need to reuse the
// associated scope/element
childScope = last.scope;
nextOrder.push(value, last);

if (index === last.index) {
// do nothing
cursor = last.element;
} else {
// existing item which got moved
last.index = index;
// This may be a noop, if the element is next, but I don't know of a good way to
// figure this out, since it would require extra DOM access, so let's just hope that
// the browsers realizes that it is noop, and treats it as such.
cursor.after(last.element);
cursor = last.element;
}
} else {
// new item which we don't know about
childScope = parentScope.$new();
if (!isArray(collection)) {
// if object, extract keys, sort them and use to determine order of iteration over obj props
array = [];
for(key in collection) {
if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
array.push(key);
}
}
array.sort();
} else {
array = collection || [];
}

childScope[valueIdent] = collection[key];
if (keyIdent) childScope[keyIdent] = key;
childScope.$index = index;
childScope.$position = index == 0
? 'first'
: (index == collectionLength - 1 ? 'last' : 'middle');

if (!last) {
linker(childScope, function(clone){
cursor.after(clone);
last = {
scope: childScope,
element: (cursor = clone),
index: index
};
nextOrder.push(value, last);
});
// we are not using forEach for perf reasons (trying to avoid #call)
for (index = 0, length = array.length; index < length; index++) {
key = (collection === array) ? index : array[index];
value = collection[key];
last = lastOrder.shift(value);
if (last) {
// if we have already seen this object, then we need to reuse the
// associated scope/element
childScope = last.scope;
nextOrder.push(value, last);

if (index === last.index) {
// do nothing
cursor = last.element;
} else {
// existing item which got moved
last.index = index;
// This may be a noop, if the element is next, but I don't know of a good way to
// figure this out, since it would require extra DOM access, so let's just hope that
// the browsers realizes that it is noop, and treats it as such.
cursor.after(last.element);
cursor = last.element;
}
} else {
// new item which we don't know about
childScope = parentScope.$new();
}

index ++;
childScope[valueIdent] = value;
if (keyIdent) childScope[keyIdent] = key;
childScope.$index = index;
childScope.$position = index == 0
? 'first'
: (index == collectionLength - 1 ? 'last' : 'middle');

if (!last) {
linker(childScope, function(clone){
cursor.after(clone);
last = {
scope: childScope,
element: (cursor = clone),
index: index
};
nextOrder.push(value, last);
});
}
}

Expand Down
24 changes: 22 additions & 2 deletions test/widgetsSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ describe("widget", function() {
'ng:bind="key + \':\' + val + $index + \'|\'"></li></ul>');
scope.items = {'misko':'m', 'shyam':'s', 'frodo':'f'};
scope.$digest();
expect(element.text()).toEqual('misko:m0|shyam:s1|frodo:f2|');
expect(element.text()).toEqual('frodo:f0|misko:m1|shyam:s2|');
});

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

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

it('should ignore non-array element properties when iterating over an array', function() {
var scope = compile('<ul><li ng:repeat="item in array">{{item}}|</li></ul>');
scope.array = ['a', 'b', 'c'];
scope.array.foo = '23';
scope.array.bar = function() {};
scope.$digest();

expect(element.text()).toBe('a|b|c|');
});

it('should iterate over non-existent elements of a sparse array', function() {
var scope = compile('<ul><li ng:repeat="item in array">{{item}}|</li></ul>');
scope.array = ['a', 'b'];
scope.array[4] = 'c';
scope.array[6] = 'd';
scope.$digest();

expect(element.text()).toBe('a|b|||c||d|');
});


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

0 comments on commit 3945f88

Please sign in to comment.