'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: * * | Variable | Type | Details | * |-----------|-----------------|-----------------------------------------------------------------------------| * | `$index` | {@type number} | iterator offset of the repeated element (0..length-1) | * | `$first` | {@type boolean} | true if the repeated element is first in the iterator. | * | `$middle` | {@type boolean} | true if the repeated element is between the first and last in the iterator. | * | `$last` | {@type boolean} | true if the repeated element is last in the iterator. | * | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). | * | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). | * * * # Special repeat start and end points * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending * the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively. * The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on) * up to and including the ending HTML tag where **ng-repeat-end** is placed. * * The example below makes use of this feature: *
 *   
* Header {{ item }} *
*
* Body {{ item }} *
* *
* * And with an input of {@type ['A','B']} for the items variable in the example above, the output will evaluate to: *
 *   
* Header A *
*
* Body A *
* *
* Header B *
*
* Body B *
* *
* * The custom start and end points for ngRepeat also support all other HTML directive syntax flavors provided in AngularJS (such * as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**). * * @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: `album in artist.albums`. * * * `(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 tracking function * is specified the ng-repeat associates elements by identity in the collection. It is an error to have * more than one tracking 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.) Filters should be applied to the expression, * before specifying a tracking expression. * * 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. * * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter * to items in conjunction with a tracking expression. * * @example * This example initializes the scope to a list of names and * then uses `ngRepeat` to display every person:
I have {{friends.length}} friends. They are:
  • [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
.animate-repeat { -webkit-transition:all linear 0.5s; -moz-transition:all linear 0.5s; -o-transition:all linear 0.5s; transition:all linear 0.5s; } .animate-repeat.ng-enter { line-height:0; opacity:0; } .animate-repeat.ng-enter.ng-enter-active { line-height:20px; opacity:1; } .animate-repeat.ng-leave { opacity:1; line-height:20px; } .animate-repeat.ng-leave.ng-leave-active { opacity:0; line-height:0; } .animate-repeat.ng-move { } .animate-repeat.ng-move.ng-move-active { } 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"]); });
*/ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var NG_REMOVED = '$$NG_REMOVED'; var ngRepeatMinErr = minErr('ngRepeat'); return { transclude: 'element', priority: 1000, terminal: true, compile: function(element, attr, linker) { return function($scope, $element, $attr){ var expression = $attr.ngRepeat; var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/), trackByExp, trackByExpGetter, trackByIdFn, trackByIdArrayFn, trackByIdObjFn, lhs, rhs, valueIdentifier, keyIdentifier, hashFnLocals = {$id: hashKey}; if (!match) { throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", expression); } lhs = match[1]; rhs = match[2]; trackByExp = match[4]; if (trackByExp) { trackByExpGetter = $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 trackByExpGetter($scope, hashFnLocals); }; } else { trackByIdArrayFn = function(key, value) { return hashKey(value); } trackByIdObjFn = function(key) { return key; } } match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); if (!match) { throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", 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, previousNode = $element[0], // current position of the node nextNode, // 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 (isArrayLike(collection)) { collectionKeys = collection; trackByIdFn = trackByIdFn || trackByIdArrayFn; } else { trackByIdFn = trackByIdFn || trackByIdObjFn; // 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(lastBlockMap.hasOwnProperty(trackById)) { 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.startNode) lastBlockMap[block.id] = block; }); // This is a duplicate and we need to throw an error throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", expression, trackById); } else { // new never before seen block nextBlockOrder[index] = { id: trackById }; nextBlockMap[trackById] = false; } } // remove existing items for (key in lastBlockMap) { if (lastBlockMap.hasOwnProperty(key)) { block = lastBlockMap[key]; $animate.leave(block.elements); forEach(block.elements, function(element) { element[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.startNode) { // if we have already seen this object, then we need to reuse the // associated scope/element childScope = block.scope; nextNode = previousNode; do { nextNode = nextNode.nextSibling; } while(nextNode && nextNode[NG_REMOVED]); if (block.startNode == nextNode) { // do nothing } else { // existing item which got moved $animate.move(block.elements, null, jqLite(previousNode)); } previousNode = block.endNode; } 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); childScope.$odd = !(childScope.$even = index%2==0); if (!block.startNode) { linker(childScope, function(clone) { $animate.enter(clone, null, jqLite(previousNode)); previousNode = clone; block.scope = childScope; block.startNode = clone[0]; block.elements = clone; block.endNode = clone[clone.length - 1]; nextBlockMap[block.id] = block; }); } } lastBlockMap = nextBlockMap; }); }; } }; }];