'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. 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:
I have {{friends.length}} friends. They are:
  • [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
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"]); });
*/ var ngRepeatDirective = ['$parse', function($parse) { 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, 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 // 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]; block.element.remove(); 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; if (block.element == cursor) { // do nothing cursor = block.element; } else { // existing item which got moved cursor.after(block.element); 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){ cursor.after(clone); cursor = clone; block.scope = childScope; block.element = clone; nextBlockMap[block.id] = block; }); } } lastBlockMap = nextBlockMap; }); }; } }; }];