2012-03-08 23:00:38 +00:00
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @ngdoc directive
|
2012-06-12 06:49:24 +00:00
|
|
|
|
* @name ng.directive:ngRepeat
|
2012-03-08 23:00:38 +00:00
|
|
|
|
*
|
|
|
|
|
|
* @description
|
2012-04-06 23:35:17 +00:00
|
|
|
|
* The `ngRepeat` directive instantiates a template once per item from a collection. Each template
|
2012-03-08 23:00:38 +00:00
|
|
|
|
* instance gets its own scope, where the given loop variable is set to the current collection item,
|
|
|
|
|
|
* and `$index` is set to the item index or key.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Special properties are exposed on the local scope of each template instance, including:
|
|
|
|
|
|
*
|
|
|
|
|
|
* * `$index` – `{number}` – iterator offset of the repeated element (0..length-1)
|
2012-05-02 16:51:31 +00:00
|
|
|
|
* * `$first` – `{boolean}` – true if the repeated element is first in the iterator.
|
|
|
|
|
|
* * `$middle` – `{boolean}` – true if the repeated element is between the first and last in the iterator.
|
|
|
|
|
|
* * `$last` – `{boolean}` – true if the repeated element is last in the iterator.
|
2012-03-08 23:00:38 +00:00
|
|
|
|
*
|
|
|
|
|
|
*
|
|
|
|
|
|
* @element ANY
|
|
|
|
|
|
* @scope
|
|
|
|
|
|
* @priority 1000
|
2012-04-06 23:35:17 +00:00
|
|
|
|
* @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. Two
|
2012-03-08 23:00:38 +00:00
|
|
|
|
* formats are currently supported:
|
|
|
|
|
|
*
|
|
|
|
|
|
* * `variable in expression` – where variable is the user defined loop variable and `expression`
|
|
|
|
|
|
* is a scope expression giving the collection to enumerate.
|
|
|
|
|
|
*
|
|
|
|
|
|
* For example: `track in cd.tracks`.
|
|
|
|
|
|
*
|
|
|
|
|
|
* * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers,
|
|
|
|
|
|
* and `expression` is the scope expression giving the collection to enumerate.
|
|
|
|
|
|
*
|
|
|
|
|
|
* For example: `(name, age) in {'adam':10, 'amalie':12}`.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @example
|
|
|
|
|
|
* This example initializes the scope to a list of names and
|
2012-04-06 23:35:17 +00:00
|
|
|
|
* then uses `ngRepeat` to display every person:
|
2012-03-08 23:00:38 +00:00
|
|
|
|
<doc:example>
|
|
|
|
|
|
<doc:source>
|
2012-03-09 08:00:05 +00:00
|
|
|
|
<div ng-init="friends = [{name:'John', age:25}, {name:'Mary', age:28}]">
|
2012-03-08 23:00:38 +00:00
|
|
|
|
I have {{friends.length}} friends. They are:
|
|
|
|
|
|
<ul>
|
2012-03-09 08:00:05 +00:00
|
|
|
|
<li ng-repeat="friend in friends">
|
2012-03-08 23:00:38 +00:00
|
|
|
|
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</doc:source>
|
|
|
|
|
|
<doc:scenario>
|
2012-03-09 08:00:05 +00:00
|
|
|
|
it('should check ng-repeat', function() {
|
2012-03-08 23:00:38 +00:00
|
|
|
|
var r = using('.doc-example-live').repeater('ul li');
|
|
|
|
|
|
expect(r.count()).toBe(2);
|
|
|
|
|
|
expect(r.row(0)).toEqual(["1","John","25"]);
|
|
|
|
|
|
expect(r.row(1)).toEqual(["2","Mary","28"]);
|
|
|
|
|
|
});
|
|
|
|
|
|
</doc:scenario>
|
|
|
|
|
|
</doc:example>
|
|
|
|
|
|
*/
|
|
|
|
|
|
var ngRepeatDirective = ngDirective({
|
|
|
|
|
|
transclude: 'element',
|
|
|
|
|
|
priority: 1000,
|
|
|
|
|
|
terminal: true,
|
|
|
|
|
|
compile: function(element, attr, linker) {
|
|
|
|
|
|
return function(scope, iterStartElement, attr){
|
|
|
|
|
|
var expression = attr.ngRepeat;
|
|
|
|
|
|
var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
|
|
|
|
|
|
lhs, rhs, valueIdent, keyIdent;
|
|
|
|
|
|
if (! match) {
|
2012-04-06 23:35:17 +00:00
|
|
|
|
throw Error("Expected ngRepeat in form of '_item_ in _collection_' but got '" +
|
2012-03-08 23:00:38 +00:00
|
|
|
|
expression + "'.");
|
|
|
|
|
|
}
|
|
|
|
|
|
lhs = match[1];
|
|
|
|
|
|
rhs = match[2];
|
2012-03-17 22:57:55 +00:00
|
|
|
|
match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
|
2012-03-08 23:00:38 +00:00
|
|
|
|
if (!match) {
|
|
|
|
|
|
throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
|
2012-03-17 22:57:55 +00:00
|
|
|
|
lhs + "'.");
|
2012-03-08 23:00:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
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();
|
2012-05-08 21:00:38 +00:00
|
|
|
|
var indexValues = [];
|
2012-07-06 09:53:40 +00:00
|
|
|
|
scope.$watch(function ngRepeatWatch(scope){
|
2012-03-08 23:00:38 +00:00
|
|
|
|
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 || [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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];
|
2012-05-08 21:00:38 +00:00
|
|
|
|
|
|
|
|
|
|
// if collection is array and value is object, it can be shifted to allow for position change
|
|
|
|
|
|
// if collection is array and value is not object, need to first check whether index is same to
|
|
|
|
|
|
// avoid shifting wrong value
|
|
|
|
|
|
// if collection is not array, need to always check index to avoid shifting wrong value
|
|
|
|
|
|
if (lastOrder.peek(value)) {
|
|
|
|
|
|
last = collection === array ?
|
|
|
|
|
|
((isObject(value)) ? lastOrder.shift(value) :
|
|
|
|
|
|
(index === lastOrder.peek(value).index ? lastOrder.shift(value) : undefined)) :
|
|
|
|
|
|
(index === lastOrder.peek(value).index ? lastOrder.shift(value) : undefined);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
last = undefined;
|
|
|
|
|
|
}
|
2012-07-06 09:53:40 +00:00
|
|
|
|
|
2012-03-08 23:00:38 +00:00
|
|
|
|
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 {
|
2012-05-08 21:00:38 +00:00
|
|
|
|
if (indexValues.hasOwnProperty(index) && collection !== array) {
|
|
|
|
|
|
var preValue = indexValues[index];
|
|
|
|
|
|
var v = lastOrder.shift(preValue);
|
|
|
|
|
|
v.element.remove();
|
|
|
|
|
|
v.scope.$destroy();
|
|
|
|
|
|
}
|
2012-03-08 23:00:38 +00:00
|
|
|
|
// new item which we don't know about
|
|
|
|
|
|
childScope = scope.$new();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
childScope[valueIdent] = value;
|
|
|
|
|
|
if (keyIdent) childScope[keyIdent] = key;
|
|
|
|
|
|
childScope.$index = index;
|
2012-05-02 16:51:31 +00:00
|
|
|
|
|
|
|
|
|
|
childScope.$first = (index === 0);
|
|
|
|
|
|
childScope.$last = (index === (collectionLength - 1));
|
|
|
|
|
|
childScope.$middle = !(childScope.$first || childScope.$last);
|
2012-03-08 23:00:38 +00:00
|
|
|
|
|
|
|
|
|
|
if (!last) {
|
|
|
|
|
|
linker(childScope, function(clone){
|
|
|
|
|
|
cursor.after(clone);
|
|
|
|
|
|
last = {
|
|
|
|
|
|
scope: childScope,
|
|
|
|
|
|
element: (cursor = clone),
|
|
|
|
|
|
index: index
|
|
|
|
|
|
};
|
|
|
|
|
|
nextOrder.push(value, last);
|
2012-05-08 21:00:38 +00:00
|
|
|
|
indexValues[index] = value;
|
2012-03-08 23:00:38 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2012-05-08 21:00:38 +00:00
|
|
|
|
var i, l;
|
|
|
|
|
|
for (i = 0, l = indexValues.length - length; i < l; i++) {
|
|
|
|
|
|
indexValues.pop();
|
|
|
|
|
|
}
|
2012-07-06 09:53:40 +00:00
|
|
|
|
|
2012-03-08 23:00:38 +00:00
|
|
|
|
//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;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|