mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-16 23:30:23 +00:00
207 lines
7.8 KiB
JavaScript
207 lines
7.8 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name ng.directive:ngRepeat
|
||
*
|
||
* @description
|
||
* The `ngRepeat` directive instantiates a template once per item from a collection. Each template
|
||
* 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)
|
||
* * `$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.
|
||
*
|
||
*
|
||
* @element ANY
|
||
* @scope
|
||
* @priority 1000
|
||
* @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. Two
|
||
* 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
|
||
* then uses `ngRepeat` to display every person:
|
||
<doc:example>
|
||
<doc:source>
|
||
<div ng-init="friends = [{name:'John', age:25}, {name:'Mary', age:28}]">
|
||
I have {{friends.length}} friends. They are:
|
||
<ul>
|
||
<li ng-repeat="friend in friends">
|
||
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</doc:source>
|
||
<doc:scenario>
|
||
it('should check ng-repeat', function() {
|
||
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) {
|
||
throw Error("Expected ngRepeat 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 '" +
|
||
lhs + "'.");
|
||
}
|
||
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();
|
||
var indexValues = [];
|
||
scope.$watch(function ngRepeatWatch(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 || [];
|
||
}
|
||
|
||
// 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];
|
||
|
||
// 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;
|
||
}
|
||
|
||
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 {
|
||
if (indexValues.hasOwnProperty(index) && collection !== array) {
|
||
var preValue = indexValues[index];
|
||
var v = lastOrder.shift(preValue);
|
||
v.element.remove();
|
||
v.scope.$destroy();
|
||
}
|
||
// new item which we don't know about
|
||
childScope = scope.$new();
|
||
}
|
||
|
||
childScope[valueIdent] = value;
|
||
if (keyIdent) childScope[keyIdent] = key;
|
||
childScope.$index = index;
|
||
|
||
childScope.$first = (index === 0);
|
||
childScope.$last = (index === (collectionLength - 1));
|
||
childScope.$middle = !(childScope.$first || childScope.$last);
|
||
|
||
if (!last) {
|
||
linker(childScope, function(clone){
|
||
cursor.after(clone);
|
||
last = {
|
||
scope: childScope,
|
||
element: (cursor = clone),
|
||
index: index
|
||
};
|
||
nextOrder.push(value, last);
|
||
indexValues[index] = value;
|
||
});
|
||
}
|
||
}
|
||
|
||
var i, l;
|
||
for (i = 0, l = indexValues.length - length; i < l; i++) {
|
||
indexValues.pop();
|
||
}
|
||
|
||
//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;
|
||
});
|
||
};
|
||
}
|
||
});
|