From 21ea9672782398f8071514340075119d1026c700 Mon Sep 17 00:00:00 2001 From: Gias Kay Lee Date: Sat, 21 Dec 2013 20:53:01 +0800 Subject: [PATCH 1/2] refactor(ngRepeat): tiny refactor A tiny refactor to make the codeflow more fluent by following the order of the expression. --- src/ng/directive/ngRepeat.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 43db58ba5ab5..abd302be906d 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -203,22 +203,29 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { 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}; + 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, + trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, + hashFnLocals = {$id: hashKey}; - if (!match) { - throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", - expression); - } + 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[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 (trackByExp) { trackByExpGetter = $parse(trackByExp); trackByIdExpFn = function(key, value, index) { @@ -226,6 +233,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 +245,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 From 35e701cf5e094bbf043c97b97c387e2b4368cb2b Mon Sep 17 00:00:00 2001 From: Gias Kay Lee Date: Sat, 28 Dec 2013 21:55:58 +0800 Subject: [PATCH 2/2] feat(ngRepeat): add a `$range` function for creating integer arrays Provide a built-in `$range(_from_, _to_)` function as a shorthand method to quickly iterate over an array of integers. Closes #3861 Closes #5268 Closes #5557 --- docs/content/error/ngRepeat/range.ngdoc | 24 ++++++++ src/ng/directive/ngRepeat.js | 46 +++++++++++++-- test/ng/directive/ngRepeatSpec.js | 75 ++++++++++++++++++++++++- 3 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 docs/content/error/ngRepeat/range.ngdoc 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 abd302be906d..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,8 +201,9 @@ */ 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, @@ -208,11 +214,13 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { 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}; + hashFnLocals = {$id: hashKey}, + watchExpression; if (!match) throw ngRepeatMinErr('iexp', - "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", expression); + "Expected expression in the form of '_item_ in _collection_[ track by _id_]', but got '{0}'.", expression); lhs = match[1]; rhs = match[2]; @@ -226,6 +234,34 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { 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) { @@ -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( + '')(scope); + + scope.from = -3; + scope.to = 3; + scope.$digest(); + + expect(element.text()).toEqual('-3/-2/-1/0/1/2/3/'); + }); + + it('should handle both ascending and descending orders as well as reversion of order', function() { + element = $compile( + '')(scope); + + scope.from = 0; + scope.to = 5; + scope.$digest(); + + expect(element.text()).toEqual('0/1/2/3/4/5/'); + + scope.from = 3; + scope.to = -3; + scope.$digest(); + + expect(element.text()).toEqual('3/2/1/0/-1/-2/-3/'); + }); + + it('should work with filter', function() { + element = $compile( + '')(scope); + + scope.positive = function(x) {return x > 0;}; + scope.from = -1; + scope.to = 2; + scope.$digest(); + + expect(element.text()).toEqual('1/2/'); + + scope.from = 5; + scope.to = -3; + scope.$digest(); + + expect(element.text()).toEqual('5/4/3/2/1/'); + }); + + it("should throw an exception if the range can't be parsed", function() { + element = $compile( + '')(scope); + + scope.from = 0; + scope.to = 2; + scope.$digest(); + + expect(element.text()).toEqual('0/1/2/'); + + scope.from = 'what'; + scope.to = 'to'; + scope.$digest(); + + expect($exceptionHandler.errors.shift().message).toMatch(/^\[ngRepeat:range\] Expected expression in the form of '\$range\(_from_, _to_\)' with valid parameters, but got \$range\(what, to\)./); + }); + }); + describe('track by', function() { it('should track using expression function', function() { element = $compile( @@ -390,7 +463,7 @@ describe('ngRepeat', function() { element = jqLite(''); $compile(element)(scope); expect($exceptionHandler.errors.shift()[0].message). - toMatch(/^\[ngRepeat:iexp\] Expected expression in form of '_item_ in _collection_\[ track by _id_\]' but got 'i dont parse'\./); + toMatch(/^\[ngRepeat:iexp\] Expected expression in the form of '_item_ in _collection_\[ track by _id_\]', but got 'i dont parse'\./); });