diff --git a/docs/content/error/ngRepeat/range.ngdoc b/docs/content/error/ngRepeat/range.ngdoc new file mode 100644 index 000000000000..78a98b6cd953 --- /dev/null +++ b/docs/content/error/ngRepeat/range.ngdoc @@ -0,0 +1,24 @@ +@ngdoc error +@name ngRepeat:range +@fullName Invalid Range +@description + +Occurs when there is an error with the $range function of an {@link api/ng.directive:ngRepeat ngRepeat}'s expression. + +To resolve, specify a valid range in the form of $range(_from_, _to_) where both _from_ and _to_ evaluate to integers. + +Examples of *invalid* syntax: + +``` +
+ +``` + +Examples of *valid* syntax: + +``` + + +``` + +Please consult the api documentation of {@link api/ng.directive:ngRepeat ngRepeat} to learn more about valid syntax. diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 43db58ba5ab5..f01ca93185c5 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -88,6 +88,11 @@ * * For example: `(name, age) in {'adam':10, 'amalie':12}`. * + * * `variable in $range(from, to)` - where `from` and `to` specify the range of an integer array inclusively, + * and `variable` being the current element. + * + * For example: `number in $range(-3, 3)`. + * * * `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 @@ -196,29 +201,67 @@ */ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { - var NG_REMOVED = '$$NG_REMOVED'; - var ngRepeatMinErr = minErr('ngRepeat'); + var NG_REMOVED = '$$NG_REMOVED', + ngRepeatMinErr = minErr('ngRepeat'); + return { transclude: 'element', priority: 1000, terminal: true, $$tlb: true, - link: function($scope, $element, $attr, ctrl, $transclude){ - var expression = $attr.ngRepeat; - var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/), - trackByExp, trackByExpGetter, trackByIdExpFn, 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); - } + link: function($scope, $element, $attr, ctrl, $transclude) { + var expression = $attr.ngRepeat, + match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/), + lhs, rhs, trackByExp, + valueIdentifier, keyIdentifier, + rangeFrom, rangeTo, rangeArray, + trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, + hashFnLocals = {$id: hashKey}, + watchExpression; + + if (!match) throw ngRepeatMinErr('iexp', + "Expected expression in the form of '_item_ in _collection_[ track by _id_]', but got '{0}'.", expression); lhs = match[1]; rhs = match[2]; trackByExp = match[3]; + 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]; + + if (/^\$range/.test(rhs)) { + watchExpression = function() { + return $scope.$eval(rhs, { + $range: function(from, to) { + var diff, + step; + + if (!isNumber(from) || !isNumber(to)) throw ngRepeatMinErr('range', + "Expected expression in the form of '$range(_from_, _to_)' with valid parameters, but got $range({0}, {1}).", from, to); + + from = Math.round(from); + to = Math.round(to); + diff = Math.abs(to - from); + step = to > from ? 1 : -1; + + // return the cached array if the range didn't change + if (from !== rangeFrom || to !== rangeTo) { + for (rangeArray = []; rangeArray.push(from) <= diff; from += step) {continue;} + } + + return rangeArray; + } + }); + }; + } else { + watchExpression = rhs; + } + if (trackByExp) { trackByExpGetter = $parse(trackByExp); trackByIdExpFn = function(key, value, index) { @@ -226,6 +269,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { if (keyIdentifier) hashFnLocals[keyIdentifier] = key; hashFnLocals[valueIdentifier] = value; hashFnLocals.$index = index; + return trackByExpGetter($scope, hashFnLocals); }; } else { @@ -237,14 +281,6 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { }; } - 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 @@ -253,7 +289,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var lastBlockMap = {}; //watch props - $scope.$watchCollection(rhs, function ngRepeatAction(collection){ + $scope.$watchCollection(watchExpression, function ngRepeatAction(collection){ var index, length, previousNode = $element[0], // current position of the node nextNode, diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index 638f082c1474..96234057fd70 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -119,6 +119,79 @@ describe('ngRepeat', function() { expect(element.text()).toEqual('age:20|codename:20|dogname:Bingo|prodname:Bingo|wealth:20|'); }); + describe('$range', function() { + it('should iterate over a range of integers', function() { + element = $compile( + '