angular.js/src/ng/directive/ngRepeat.js
2013-04-02 14:05:06 -07:00

255 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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