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
|
|
|
|
*
|
2013-03-20 23:24:23 +00:00
|
|
|
|
* 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
|
2012-03-08 23:00:38 +00:00
|
|
|
|
*
|
|
|
|
|
|
* @element ANY
|
|
|
|
|
|
* @scope
|
|
|
|
|
|
* @priority 1000
|
2013-03-20 05:27:27 +00:00
|
|
|
|
* @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These
|
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}`.
|
|
|
|
|
|
*
|
2013-03-20 05:27:27 +00:00
|
|
|
|
* * `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.
|
|
|
|
|
|
*
|
2012-03-08 23:00:38 +00:00
|
|
|
|
* @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:
|
2013-04-02 23:41:16 +00:00
|
|
|
|
<example animations="true">
|
|
|
|
|
|
<file name="index.html">
|
|
|
|
|
|
<div ng-init="friends = [
|
|
|
|
|
|
{name:'John', age:25, gender:'boy'},
|
|
|
|
|
|
{name:'Jessie', age:30, gender:'girl'},
|
|
|
|
|
|
{name:'Johanna', age:28, gender:'girl'},
|
|
|
|
|
|
{name:'Joy', age:15, gender:'girl'},
|
|
|
|
|
|
{name:'Mary', age:28, gender:'girl'},
|
|
|
|
|
|
{name:'Peter', age:95, gender:'boy'},
|
|
|
|
|
|
{name:'Sebastian', age:50, gender:'boy'},
|
|
|
|
|
|
{name:'Erika', age:27, gender:'girl'},
|
|
|
|
|
|
{name:'Patrick', age:40, gender:'boy'},
|
|
|
|
|
|
{name:'Samantha', age:60, gender:'girl'}
|
|
|
|
|
|
]">
|
|
|
|
|
|
I have {{friends.length}} friends. They are:
|
|
|
|
|
|
<input type="search" ng-model="q" placeholder="filter friends..." />
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li ng-repeat="friend in friends | filter:q"
|
|
|
|
|
|
ng-animate="{enter: 'example-repeat-enter',
|
|
|
|
|
|
leave: 'example-repeat-leave',
|
|
|
|
|
|
move: 'example-repeat-move'}">
|
|
|
|
|
|
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</file>
|
|
|
|
|
|
<file name="animations.css">
|
|
|
|
|
|
.example-repeat-enter-setup,
|
|
|
|
|
|
.example-repeat-leave-setup,
|
|
|
|
|
|
.example-repeat-move-setup {
|
|
|
|
|
|
-webkit-transition:all linear 0.5s;
|
|
|
|
|
|
-moz-transition:all linear 0.5s;
|
|
|
|
|
|
-ms-transition:all linear 0.5s;
|
|
|
|
|
|
-o-transition:all linear 0.5s;
|
|
|
|
|
|
transition:all linear 0.5s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.example-repeat-enter-setup {
|
|
|
|
|
|
line-height:0;
|
|
|
|
|
|
opacity:0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.example-repeat-enter-setup.example-repeat-enter-start {
|
|
|
|
|
|
line-height:20px;
|
|
|
|
|
|
opacity:1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.example-repeat-leave-setup {
|
|
|
|
|
|
opacity:1;
|
|
|
|
|
|
line-height:20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.example-repeat-leave-setup.example-repeat-leave-start {
|
|
|
|
|
|
opacity:0;
|
|
|
|
|
|
line-height:0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.example-repeat-move-setup { }
|
|
|
|
|
|
.example-repeat-move-setup.example-repeat-move-start { }
|
|
|
|
|
|
</file>
|
|
|
|
|
|
<file name="scenario.js">
|
|
|
|
|
|
it('should render initial data set', function() {
|
|
|
|
|
|
var r = using('.doc-example-live').repeater('ul li');
|
|
|
|
|
|
expect(r.count()).toBe(10);
|
|
|
|
|
|
expect(r.row(0)).toEqual(["1","John","25"]);
|
|
|
|
|
|
expect(r.row(1)).toEqual(["2","Jessie","30"]);
|
|
|
|
|
|
expect(r.row(9)).toEqual(["10","Samantha","60"]);
|
|
|
|
|
|
expect(binding('friends.length')).toBe("10");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should update repeater when filter predicate changes', function() {
|
|
|
|
|
|
var r = using('.doc-example-live').repeater('ul li');
|
|
|
|
|
|
expect(r.count()).toBe(10);
|
|
|
|
|
|
|
|
|
|
|
|
input('q').enter('ma');
|
|
|
|
|
|
|
|
|
|
|
|
expect(r.count()).toBe(2);
|
|
|
|
|
|
expect(r.row(0)).toEqual(["1","Mary","28"]);
|
|
|
|
|
|
expect(r.row(1)).toEqual(["2","Samantha","60"]);
|
|
|
|
|
|
});
|
|
|
|
|
|
</file>
|
|
|
|
|
|
</example>
|
2012-03-08 23:00:38 +00:00
|
|
|
|
*/
|
2013-03-20 23:24:23 +00:00
|
|
|
|
var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
|
|
|
|
|
|
var NG_REMOVED = '$$NG_REMOVED';
|
2013-03-20 05:27:27 +00:00
|
|
|
|
return {
|
|
|
|
|
|
transclude: 'element',
|
|
|
|
|
|
priority: 1000,
|
|
|
|
|
|
terminal: true,
|
|
|
|
|
|
compile: function(element, attr, linker) {
|
|
|
|
|
|
return function($scope, $element, $attr){
|
2013-03-20 23:24:23 +00:00
|
|
|
|
var animate = $animator($scope, $attr);
|
2013-03-20 05:27:27 +00:00
|
|
|
|
var expression = $attr.ngRepeat;
|
|
|
|
|
|
var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
|
2013-04-11 23:28:42 +00:00
|
|
|
|
trackByExp, trackByExpGetter, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier,
|
2013-03-20 05:27:27 +00:00
|
|
|
|
hashFnLocals = {$id: hashKey};
|
|
|
|
|
|
|
|
|
|
|
|
if (!match) {
|
|
|
|
|
|
throw Error("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got '" +
|
|
|
|
|
|
expression + "'.");
|
2012-03-08 23:00:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2013-03-20 05:27:27 +00:00
|
|
|
|
lhs = match[1];
|
|
|
|
|
|
rhs = match[2];
|
|
|
|
|
|
trackByExp = match[4];
|
|
|
|
|
|
|
|
|
|
|
|
if (trackByExp) {
|
2013-04-11 23:28:42 +00:00
|
|
|
|
trackByExpGetter = $parse(trackByExp);
|
2013-03-20 05:27:27 +00:00
|
|
|
|
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;
|
2013-04-11 23:28:42 +00:00
|
|
|
|
return trackByExpGetter($scope, hashFnLocals);
|
2013-03-20 05:27:27 +00:00
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
trackByIdFn = function(key, value) {
|
|
|
|
|
|
return hashKey(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2013-04-02 23:41:16 +00:00
|
|
|
|
|
2013-03-20 05:27:27 +00:00
|
|
|
|
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
|
2013-03-20 23:24:23 +00:00
|
|
|
|
nextCursor,
|
2013-03-20 05:27:27 +00:00
|
|
|
|
// 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 = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
2013-04-30 23:19:44 +00:00
|
|
|
|
if (isArrayLike(collection)) {
|
2013-03-20 05:27:27 +00:00
|
|
|
|
collectionKeys = collection;
|
2012-03-08 23:00:38 +00:00
|
|
|
|
} else {
|
2013-03-20 05:27:27 +00:00
|
|
|
|
// 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();
|
2012-03-08 23:00:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2013-03-20 05:27:27 +00:00
|
|
|
|
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);
|
2013-04-11 23:28:42 +00:00
|
|
|
|
if(lastBlockMap.hasOwnProperty(trackById)) {
|
|
|
|
|
|
block = lastBlockMap[trackById]
|
2013-03-20 05:27:27 +00:00
|
|
|
|
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
|
2013-04-11 23:28:42 +00:00
|
|
|
|
throw new Error('Duplicates in a repeater are not allowed. Repeater: ' + expression +
|
|
|
|
|
|
' key: ' + trackById);
|
2013-03-20 05:27:27 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// new never before seen block
|
|
|
|
|
|
nextBlockOrder[index] = { id: trackById };
|
2013-04-11 23:28:42 +00:00
|
|
|
|
nextBlockMap[trackById] = false;
|
2013-03-20 05:27:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// remove existing items
|
|
|
|
|
|
for (key in lastBlockMap) {
|
|
|
|
|
|
if (lastBlockMap.hasOwnProperty(key)) {
|
|
|
|
|
|
block = lastBlockMap[key];
|
2013-03-20 23:24:23 +00:00
|
|
|
|
animate.leave(block.element);
|
|
|
|
|
|
block.element[0][NG_REMOVED] = true;
|
2013-03-20 05:27:27 +00:00
|
|
|
|
block.scope.$destroy();
|
|
|
|
|
|
}
|
2012-03-08 23:00:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2013-03-20 05:27:27 +00:00
|
|
|
|
// 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;
|
|
|
|
|
|
|
2013-03-20 23:24:23 +00:00
|
|
|
|
nextCursor = cursor[0];
|
|
|
|
|
|
do {
|
|
|
|
|
|
nextCursor = nextCursor.nextSibling;
|
|
|
|
|
|
} while(nextCursor && nextCursor[NG_REMOVED]);
|
|
|
|
|
|
|
|
|
|
|
|
if (block.element[0] == nextCursor) {
|
2013-03-20 05:27:27 +00:00
|
|
|
|
// do nothing
|
|
|
|
|
|
cursor = block.element;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// existing item which got moved
|
2013-03-20 23:24:23 +00:00
|
|
|
|
animate.move(block.element, null, cursor);
|
2013-03-20 05:27:27 +00:00
|
|
|
|
cursor = block.element;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// new item which we don't know about
|
|
|
|
|
|
childScope = $scope.$new();
|
2012-03-08 23:00:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2013-03-20 05:27:27 +00:00
|
|
|
|
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) {
|
2013-03-20 23:24:23 +00:00
|
|
|
|
linker(childScope, function(clone) {
|
|
|
|
|
|
animate.enter(clone, null, cursor);
|
2013-03-20 05:27:27 +00:00
|
|
|
|
cursor = clone;
|
|
|
|
|
|
block.scope = childScope;
|
|
|
|
|
|
block.element = clone;
|
|
|
|
|
|
nextBlockMap[block.id] = block;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
lastBlockMap = nextBlockMap;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}];
|
2013-03-20 23:24:23 +00:00
|
|
|
|
|