Skip to content

Commit

Permalink
fix(ng:repeat): use transclusion
Browse files Browse the repository at this point in the history
  • Loading branch information
mhevery committed Feb 22, 2012
1 parent 7bd69d0 commit 3df7b8e
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 132 deletions.
222 changes: 109 additions & 113 deletions src/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,131 +310,127 @@ var htmlAnchorDirective = valueFn({
</doc:scenario>
</doc:example>
*/
var ngRepeatDirective = ['$compile', function($compile) {
return {
priority: 1000,
terminal: true,
compile: function(element, attr) {
var ngRepeatDirective = valueFn({
transclude: 'element',
priority: 1000,
terminal: true,
compile: function(element, attr, linker) {
return function(scope, iterStartElement, attr){
var expression = attr.ngRepeat;
attr.$set(attr.$attr.ngRepeat);
element.replaceWith(jqLite('<!-- ng:repeat: ' + expression + ' -->'));
var linker = $compile(element);
return function(scope, iterStartElement, attr){
var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
lhs, rhs, valueIdent, keyIdent;
if (! match) {
throw Error("Expected ng:repeat in form of '_item_ in _collection_' but got '" +
expression + "'.");
}
lhs = match[1];
rhs = match[2];
match = lhs.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/);
if (!match) {
throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
keyValue + "'.");
}
valueIdent = match[3] || match[1];
keyIdent = match[2];

// Store a list of elements from previous run. This is a hash where key is the item from the
// iterator, and the value is an array of objects with following properties.
// - scope: bound scope
// - element: previous element.
// - index: position
// We need an array of these objects since the same object can be returned from the iterator.
// We expect this to be a rare case.
var lastOrder = new HashQueueMap();
scope.$watch(function(scope){
var index, length,
collection = scope.$eval(rhs),
collectionLength = size(collection, true),
childScope,
// Same as lastOrder but it has the current state. It will become the
// lastOrder on the next iteration.
nextOrder = new HashQueueMap(),
key, value, // key/value of iteration
array, last, // last object information {scope, element, index}
cursor = iterStartElement; // current position of the node

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);
}
var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
lhs, rhs, valueIdent, keyIdent;
if (! match) {
throw Error("Expected ng:repeat in form of '_item_ in _collection_' but got '" +
expression + "'.");
}
lhs = match[1];
rhs = match[2];
match = lhs.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/);
if (!match) {
throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
keyValue + "'.");
}
valueIdent = match[3] || match[1];
keyIdent = match[2];

// Store a list of elements from previous run. This is a hash where key is the item from the
// iterator, and the value is an array of objects with following properties.
// - scope: bound scope
// - element: previous element.
// - index: position
// We need an array of these objects since the same object can be returned from the iterator.
// We expect this to be a rare case.
var lastOrder = new HashQueueMap();
scope.$watch(function(scope){
var index, length,
collection = scope.$eval(rhs),
collectionLength = size(collection, true),
childScope,
// Same as lastOrder but it has the current state. It will become the
// lastOrder on the next iteration.
nextOrder = new HashQueueMap(),
key, value, // key/value of iteration
array, last, // last object information {scope, element, index}
cursor = iterStartElement; // current position of the node

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 || [];
}
array.sort();
} else {
array = collection || [];
}

// 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;
}
// 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 {
// new item which we don't know about
childScope = scope.$new();
// 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 = scope.$new();
}

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);
});
}
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);
});
}
}

//shrink children
for (key in lastOrder) {
if (lastOrder.hasOwnProperty(key)) {
array = lastOrder[key];
while(array.length) {
value = array.pop();
value.element.remove();
value.scope.$destroy();
}
//shrink children
for (key in lastOrder) {
if (lastOrder.hasOwnProperty(key)) {
array = lastOrder[key];
while(array.length) {
value = array.pop();
value.element.remove();
value.scope.$destroy();
}
}
}

lastOrder = nextOrder;
});
};
}
};
}];
lastOrder = nextOrder;
});
};
}
});


/**
Expand Down
36 changes: 18 additions & 18 deletions test/BinderSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,27 +109,27 @@ describe('Binder', function() {
expect(sortedHtml(form)).toBe(
'<ul>' +
'<#comment></#comment>' +
'<li ng:bind="item.a">A</li>' +
'<li ng:bind="item.a">B</li>' +
'<li ng:bind="item.a" ng:repeat="item in model.items">A</li>' +
'<li ng:bind="item.a" ng:repeat="item in model.items">B</li>' +
'</ul>');

items.unshift({a: 'C'});
$rootScope.$apply();
expect(sortedHtml(form)).toBe(
'<ul>' +
'<#comment></#comment>' +
'<li ng:bind="item.a">C</li>' +
'<li ng:bind="item.a">A</li>' +
'<li ng:bind="item.a">B</li>' +
'<li ng:bind="item.a" ng:repeat="item in model.items">C</li>' +
'<li ng:bind="item.a" ng:repeat="item in model.items">A</li>' +
'<li ng:bind="item.a" ng:repeat="item in model.items">B</li>' +
'</ul>');

items.shift();
$rootScope.$apply();
expect(sortedHtml(form)).toBe(
'<ul>' +
'<#comment></#comment>' +
'<li ng:bind="item.a">A</li>' +
'<li ng:bind="item.a">B</li>' +
'<li ng:bind="item.a" ng:repeat="item in model.items">A</li>' +
'<li ng:bind="item.a" ng:repeat="item in model.items">B</li>' +
'</ul>');

items.shift();
Expand All @@ -147,7 +147,7 @@ describe('Binder', function() {
expect(sortedHtml(element)).toBe(
'<ul>' +
'<#comment></#comment>' +
'<li><span ng:bind="item.a">A</span></li>' +
'<li ng:repeat="item in model.items"><span ng:bind="item.a">A</span></li>' +
'</ul>');
}));

Expand Down Expand Up @@ -249,15 +249,15 @@ describe('Binder', function() {
expect(sortedHtml(element)).toBe(
'<div>'+
'<#comment></#comment>'+
'<div name="a">'+
'<div name="a" ng:repeat="m in model">'+
'<#comment></#comment>'+
'<ul name="a1"></ul>'+
'<ul name="a2"></ul>'+
'<ul name="a1" ng:repeat="i in m.item"></ul>'+
'<ul name="a2" ng:repeat="i in m.item"></ul>'+
'</div>'+
'<div name="b">'+
'<div name="b" ng:repeat="m in model">'+
'<#comment></#comment>'+
'<ul name="b1"></ul>'+
'<ul name="b2"></ul>'+
'<ul name="b1" ng:repeat="i in m.item"></ul>'+
'<ul name="b2" ng:repeat="i in m.item"></ul>'+
'</div>' +
'</div>');
}));
Expand Down Expand Up @@ -341,8 +341,8 @@ describe('Binder', function() {
expect(d2.hasClass('e')).toBeTruthy();
expect(sortedHtml(element)).toBe(
'<div><#comment></#comment>' +
'<div class="o" ng:class-even="\'e\'" ng:class-odd="\'o\'"></div>' +
'<div class="e" ng:class-even="\'e\'" ng:class-odd="\'o\'"></div></div>');
'<div class="o" ng:class-even="\'e\'" ng:class-odd="\'o\'" ng:repeat="i in [0,1]"></div>' +
'<div class="e" ng:class-even="\'e\'" ng:class-odd="\'o\'" ng:repeat="i in [0,1]"></div></div>');
}));

it('BindStyle', inject(function($rootScope, $compile) {
Expand Down Expand Up @@ -469,8 +469,8 @@ describe('Binder', function() {
expect(sortedHtml(element)).toBe(
'<ul>' +
'<#comment></#comment>' +
'<li ng:bind=\"k + v\">a0</li>' +
'<li ng:bind=\"k + v\">b1</li>' +
'<li ng:bind=\"k + v\" ng:repeat="(k,v) in {a:0,b:1}">a0</li>' +
'<li ng:bind=\"k + v\" ng:repeat="(k,v) in {a:0,b:1}">b1</li>' +
'</ul>');
}));

Expand Down
2 changes: 1 addition & 1 deletion test/widgetsSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ describe('widget', function() {
});


describe('@ng:repeat', function() {
describe('ng:repeat', function() {
it('should ng:repeat over array', inject(function($rootScope, $compile) {
element = $compile(
'<ul>' +
Expand Down

0 comments on commit 3df7b8e

Please sign in to comment.