mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-16 23:30:23 +00:00
255 lines
10 KiB
JavaScript
255 lines
10 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.
|
||
*
|
||
* Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**,
|
||
* **leave** and **move** effects.
|
||
*
|
||
* @animations
|
||
* enter - when a new item is added to the list or when an item is revealed after a filter
|
||
* leave - when an item is removed from the list or when an item is filtered out
|
||
* move - when an adjacent item is filtered out causing a reorder or when the item contents are reordered
|
||
*
|
||
* @element ANY
|
||
* @scope
|
||
* @priority 1000
|
||
* @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These
|
||
* 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}`.
|
||
*
|
||
* * `variable in expression track by tracking_expression` – You can also provide an optional tracking function
|
||
* which can be used to associate the objects in the collection with the DOM elements. If no tractking function
|
||
* is specified the ng-repeat associates elements by identity in the collection. It is an error to have
|
||
* more then one tractking function to resolve to the same key. (This would mean that two distinct objects are
|
||
* mapped to the same DOM element, which is not possible.)
|
||
*
|
||
* For example: `item in items` is equivalent to `item in items track by $id(item)'. This implies that the DOM elements
|
||
* will be associated by item identity in the array.
|
||
*
|
||
* For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique
|
||
* `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
|
||
* with the corresponding item in the array by identity. Moving the same object in array would move the DOM
|
||
* element in the same way ian the DOM.
|
||
*
|
||
* For example: `item in items track by item.id` Is a typical pattern when the items come from the database. In this
|
||
* case the object identity does not matter. Two objects are considered equivalent as long as their `id`
|
||
* property is same.
|
||
*
|
||
* @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 = ['$parse', '$animator', function($parse, $animator) {
|
||
var NG_REMOVED = '$$NG_REMOVED';
|
||
return {
|
||
transclude: 'element',
|
||
priority: 1000,
|
||
terminal: true,
|
||
compile: function(element, attr, linker) {
|
||
return function($scope, $element, $attr){
|
||
var animate = $animator($scope, $attr);
|
||
var expression = $attr.ngRepeat;
|
||
var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
|
||
trackByExp, hashExpFn, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier,
|
||
hashFnLocals = {$id: hashKey};
|
||
|
||
if (!match) {
|
||
throw Error("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got '" +
|
||
expression + "'.");
|
||
}
|
||
|
||
lhs = match[1];
|
||
rhs = match[2];
|
||
trackByExp = match[4];
|
||
|
||
if (trackByExp) {
|
||
hashExpFn = $parse(trackByExp);
|
||
trackByIdFn = function(key, value, index) {
|
||
// assign key, value, and $index to the locals so that they can be used in hash functions
|
||
if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
|
||
hashFnLocals[valueIdentifier] = value;
|
||
hashFnLocals.$index = index;
|
||
return hashExpFn($scope, hashFnLocals);
|
||
};
|
||
} else {
|
||
trackByIdFn = function(key, value) {
|
||
return hashKey(value);
|
||
}
|
||
}
|
||
|
||
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 + "'.");
|
||
}
|
||
valueIdentifier = match[3] || match[1];
|
||
keyIdentifier = 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 objects with following properties.
|
||
// - scope: bound scope
|
||
// - element: previous element.
|
||
// - index: position
|
||
var lastBlockMap = {};
|
||
|
||
//watch props
|
||
$scope.$watchCollection(rhs, function ngRepeatAction(collection){
|
||
var index, length,
|
||
cursor = $element, // current position of the node
|
||
nextCursor,
|
||
// Same as lastBlockMap but it has the current state. It will become the
|
||
// lastBlockMap on the next iteration.
|
||
nextBlockMap = {},
|
||
arrayLength,
|
||
childScope,
|
||
key, value, // key/value of iteration
|
||
trackById,
|
||
collectionKeys,
|
||
block, // last object information {scope, element, id}
|
||
nextBlockOrder = [];
|
||
|
||
|
||
if (isArray(collection)) {
|
||
collectionKeys = collection;
|
||
} else {
|
||
// if object, extract keys, sort them and use to determine order of iteration over obj props
|
||
collectionKeys = [];
|
||
for (key in collection) {
|
||
if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
|
||
collectionKeys.push(key);
|
||
}
|
||
}
|
||
collectionKeys.sort();
|
||
}
|
||
|
||
arrayLength = collectionKeys.length;
|
||
|
||
// locate existing items
|
||
length = nextBlockOrder.length = collectionKeys.length;
|
||
for(index = 0; index < length; index++) {
|
||
key = (collection === collectionKeys) ? index : collectionKeys[index];
|
||
value = collection[key];
|
||
trackById = trackByIdFn(key, value, index);
|
||
if((block = lastBlockMap[trackById])) {
|
||
delete lastBlockMap[trackById];
|
||
nextBlockMap[trackById] = block;
|
||
nextBlockOrder[index] = block;
|
||
} else if (nextBlockMap.hasOwnProperty(trackById)) {
|
||
// restore lastBlockMap
|
||
forEach(nextBlockOrder, function(block) {
|
||
if (block && block.element) lastBlockMap[block.id] = block;
|
||
});
|
||
// This is a duplicate and we need to throw an error
|
||
throw new Error('Duplicates in a repeater are not allowed. Repeater: ' + expression);
|
||
} else {
|
||
// new never before seen block
|
||
nextBlockOrder[index] = { id: trackById };
|
||
}
|
||
}
|
||
|
||
// remove existing items
|
||
for (key in lastBlockMap) {
|
||
if (lastBlockMap.hasOwnProperty(key)) {
|
||
block = lastBlockMap[key];
|
||
animate.leave(block.element);
|
||
block.element[0][NG_REMOVED] = true;
|
||
block.scope.$destroy();
|
||
}
|
||
}
|
||
|
||
// we are not using forEach for perf reasons (trying to avoid #call)
|
||
for (index = 0, length = collectionKeys.length; index < length; index++) {
|
||
key = (collection === collectionKeys) ? index : collectionKeys[index];
|
||
value = collection[key];
|
||
block = nextBlockOrder[index];
|
||
|
||
if (block.element) {
|
||
// if we have already seen this object, then we need to reuse the
|
||
// associated scope/element
|
||
childScope = block.scope;
|
||
|
||
nextCursor = cursor[0];
|
||
do {
|
||
nextCursor = nextCursor.nextSibling;
|
||
} while(nextCursor && nextCursor[NG_REMOVED]);
|
||
|
||
if (block.element[0] == nextCursor) {
|
||
// do nothing
|
||
cursor = block.element;
|
||
} else {
|
||
// existing item which got moved
|
||
animate.move(block.element, null, cursor);
|
||
cursor = block.element;
|
||
}
|
||
} else {
|
||
// new item which we don't know about
|
||
childScope = $scope.$new();
|
||
}
|
||
|
||
childScope[valueIdentifier] = value;
|
||
if (keyIdentifier) childScope[keyIdentifier] = key;
|
||
childScope.$index = index;
|
||
childScope.$first = (index === 0);
|
||
childScope.$last = (index === (arrayLength - 1));
|
||
childScope.$middle = !(childScope.$first || childScope.$last);
|
||
|
||
if (!block.element) {
|
||
linker(childScope, function(clone) {
|
||
animate.enter(clone, null, cursor);
|
||
cursor = clone;
|
||
block.scope = childScope;
|
||
block.element = clone;
|
||
nextBlockMap[block.id] = block;
|
||
});
|
||
}
|
||
}
|
||
lastBlockMap = nextBlockMap;
|
||
});
|
||
};
|
||
}
|
||
};
|
||
}];
|
||
|