diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index f0c2660faa5c..dc9ff3a9eaaf 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -19,6 +19,8 @@ * | `$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). | + * | `$prev` | {@type *} | value of previous element (undefined if it doesn't exist). | + * | `$next` | {@type *} | value of next element (undefined if it doesn't exist). | * * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}. * This may be useful when, for instance, nesting ngRepeats. @@ -212,7 +214,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var NG_REMOVED = '$$NG_REMOVED'; var ngRepeatMinErr = minErr('ngRepeat'); - var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) { + var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength, prevValue, nextValue) { // TODO(perf): generate setters to shave off ~40ms or 1-1.5% scope[valueIdentifier] = value; if (keyIdentifier) scope[keyIdentifier] = key; @@ -223,6 +225,8 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // jshint bitwise: false scope.$odd = !(scope.$even = (index&1) === 0); // jshint bitwise: true + scope.$prev = prevValue; + scope.$next = nextValue; }; var getBlockStart = function(block) { @@ -318,7 +322,8 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // lastBlockMap on the next iteration. nextBlockMap = createMap(), collectionLength, - key, value, // key/value of iteration + key, value, useCollectionKeysAsIndex, // key/value of iteration + prevValue, nextValue, // prev/next value trackById, trackByIdFn, collectionKeys, @@ -347,10 +352,11 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { collectionLength = collectionKeys.length; nextBlockOrder = new Array(collectionLength); + useCollectionKeysAsIndex = collection !== collectionKeys; // locate existing items for (index = 0; index < collectionLength; index++) { - key = (collection === collectionKeys) ? index : collectionKeys[index]; + key = useCollectionKeysAsIndex ? collectionKeys[index] : index; value = collection[key]; trackById = trackByIdFn(key, value, index); if (lastBlockMap[trackById]) { @@ -391,10 +397,14 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // we are not using forEach for perf reasons (trying to avoid #call) for (index = 0; index < collectionLength; index++) { - key = (collection === collectionKeys) ? index : collectionKeys[index]; + key = useCollectionKeysAsIndex ? collectionKeys[index] : index; value = collection[key]; block = nextBlockOrder[index]; + // assign previous and next value to local vars and pass them to scope + prevValue = index === 0 ? undefined : collection[useCollectionKeysAsIndex ? collectionKeys[index - 1] : index - 1]; + nextValue = index === collectionLength ? undefined : collection[useCollectionKeysAsIndex ? collectionKeys[index + 1] : index + 1]; + if (block.scope) { // if we have already seen this object, then we need to reuse the // associated scope/element @@ -411,7 +421,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode)); } previousNode = getBlockEnd(block); - updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength, prevValue, nextValue); } else { // new item which we don't know about $transclude(function ngRepeatTransclude(clone, scope) { @@ -428,7 +438,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // by a directive with templateUrl when its template arrives. block.clone = clone; nextBlockMap[block.id] = block; - updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength, prevValue, nextValue); }); } } diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index 00e36be69a7f..363a2c5b49c8 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -713,6 +713,132 @@ describe('ngRepeat', function() { }); + it('should be able to access $prev and $next value on every iteration over an array', function() { + element = $compile( + '')(scope); + + // INIT AND FIRST CHECK + scope.items = ['banana', 'apple', 'orange']; + scope.$digest(); + expect(element.text()). + toEqual('banana:-apple|apple:banana-orange|orange:apple-|'); + + // PUSH AND CHECK + scope.items.push('peach'); + scope.$digest(); + expect(element.text()). + toEqual('banana:-apple|apple:banana-orange|orange:apple-peach|peach:orange-|'); + + // POP'N'SHIFT AND CHECK + scope.items.pop(); + scope.items.shift(); + scope.$digest(); + expect(element.text()). + toEqual('apple:-orange|orange:apple-|'); + }); + + + it('should be able to access $prev and $next value on every iteration over an array of objects', function() { + element = $compile( + '')(scope); + + // INIT AND FIRST CHECK + scope.items = [{name: 'banana'}, {name: 'apple'}, {name: 'orange'}]; + scope.$digest(); + expect(element.text()). + toEqual('banana:-apple|apple:banana-orange|orange:apple-|'); + + // PUSH AND CHECK + scope.items.push({name: 'peach'}); + scope.$digest(); + expect(element.text()). + toEqual('banana:-apple|apple:banana-orange|orange:apple-peach|peach:orange-|'); + + // POP'N'SHIFT AND CHECK + scope.items.pop(); + scope.items.shift(); + scope.$digest(); + expect(element.text()). + toEqual('apple:-orange|orange:apple-|'); + }); + + + it('should be able to access $prev and $next value on every iteration over an object', function() { + element = $compile( + '')(scope); + + // INIT AND FIRST CHECK + scope.items = {first: 'great', second: 'nice', third: 'awesome'}; + scope.$digest(); + expect(element.text()). + toEqual('great:-nice|nice:great-awesome|awesome:nice-|'); + + // ADD AND CHECK + scope.items.fourth = 'marvelous'; + scope.$digest(); + expect(element.text()). + toEqual('great:-marvelous|marvelous:great-nice|nice:marvelous-awesome|awesome:nice-|'); + + // DELETE AND CHECK + delete scope.items.fourth; + delete scope.items.first; + scope.$digest(); + expect(element.text()). + toEqual('nice:-awesome|awesome:nice-|'); + }); + + + it('should be able to access $prev and $next value correctly after filtering out bad keys', function() { + element = $compile( + '')(scope); + + // INIT AND FIRST CHECK + scope.items = {first: 'great', second: 'nice', $toBeFilteredOut: 'bad', third: 'awesome'}; + scope.$digest(); + expect(element.text()). + toEqual('great:-nice|nice:great-awesome|awesome:nice-|'); + + // ADD AND CHECK + scope.items.fourth = 'marvelous'; + scope.$digest(); + expect(element.text()). + toEqual('great:-marvelous|marvelous:great-nice|nice:marvelous-awesome|awesome:nice-|'); + + // DELETE AND CHECK + delete scope.items.fourth; + delete scope.items.first; + scope.$digest(); + expect(element.text()). + toEqual('nice:-awesome|awesome:nice-|'); + }); + + it('should ignore $ and $$ properties', function() { element = $compile('')(scope); scope.items = ['a', 'b', 'c'];