From 1be9bb9d3527e0758350c4f7417a4228d8571440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Tue, 27 May 2014 15:52:38 -0400 Subject: [PATCH] fix(NgModel): ensure pattern and ngPattern use the same validator When the pattern and ng-pattern attributes are used with an input element containing a ngModel directive then they should both use the same validator and the validation errors of the model should be placed on model.$error.pattern. BREAKING CHANGE: If an expression is used on ng-pattern (such as `ng-pattern="exp"`) or on the pattern attribute (something like on `pattern="{{ exp }}"`) and the expression itself evaluates to a string then the validator will not parse the string as a literal regular expression object (a value like `/abc/i`). Instead, the entire string will be created as the regular expression to test against. This means that any expression flags will not be placed on the RegExp object. To get around this limitation, use a regular expression object as the value for the expression. //before $scope.exp = '/abc/i'; //after $scope.exp = /abc/i; --- src/AngularPublic.js | 8 +++-- src/ng/directive/input.js | 55 ++++++++++++++++++---------------- test/ng/directive/inputSpec.js | 51 +++++++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 29 deletions(-) diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 76bbf4a83ed8..4f2474fba9f7 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -43,6 +43,8 @@ ngModelDirective, ngListDirective, ngChangeDirective, + patternDirective, + patternDirective, requiredDirective, requiredDirective, minlengthDirective, @@ -186,12 +188,14 @@ function publishExternalAPI(angular){ ngModel: ngModelDirective, ngList: ngListDirective, ngChange: ngChangeDirective, + pattern: patternDirective, + ngPattern: patternDirective, required: requiredDirective, ngRequired: requiredDirective, - ngMinlength: minlengthDirective, minlength: minlengthDirective, - ngMaxlength: maxlengthDirective, + ngMinlength: minlengthDirective, maxlength: maxlengthDirective, + ngMaxlength: maxlengthDirective, ngValue: ngValueDirective, ngModelOptions: ngModelOptionsDirective }). diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 17a9dd5a04a3..ad4713928487 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -975,31 +975,6 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { ctrl.$render = function() { element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); }; - - // pattern validator - if (attr.ngPattern) { - var regexp, patternExp = attr.ngPattern; - attr.$observe('pattern', function(regex) { - if(isString(regex)) { - var match = regex.match(REGEX_STRING_REGEXP); - if(match) { - regex = new RegExp(match[1], match[2]); - } - } - - if (regex && !regex.test) { - throw minErr('ngPattern')('noregexp', - 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp, - regex, startingTag(element)); - } - - regexp = regex || undefined; - }); - - ctrl.$validators.pattern = function(value) { - return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value); - }; - } } function weekParser(isoWeek) { @@ -2167,6 +2142,36 @@ var requiredDirective = function() { }; +var patternDirective = function() { + return { + require: '?ngModel', + link: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + + var regexp, patternExp = attr.ngPattern || attr.pattern; + attr.$observe('pattern', function(regex) { + if(isString(regex) && regex.length > 0) { + regex = new RegExp(regex); + } + + if (regex && !regex.test) { + throw minErr('ngPattern')('noregexp', + 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp, + regex, startingTag(elm)); + } + + regexp = regex || undefined; + ctrl.$validate(); + }); + + ctrl.$validators.pattern = function(value) { + return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value); + }; + } + }; +}; + + var maxlengthDirective = function() { return { require: '?ngModel', diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 0f08ee2c627f..e48a2a082672 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -1331,12 +1331,59 @@ describe('input', function() { expect(inputElm).toBeInvalid(); }); + it('should perform validations when the ngPattern scope value changes', function() { + scope.regexp = /^[a-z]+$/; + compileInput(''); + + changeInputValueTo('abcdef'); + expect(inputElm).toBeValid(); + + changeInputValueTo('123'); + expect(inputElm).toBeInvalid(); + + scope.$apply(function() { + scope.regexp = /^\d+$/; + }); + + expect(inputElm).toBeValid(); + + changeInputValueTo('abcdef'); + expect(inputElm).toBeInvalid(); + + scope.$apply(function() { + scope.regexp = ''; + }); + + expect(inputElm).toBeValid(); + }); + + it('should register "pattern" with the model validations when the pattern attribute is used', function() { + compileInput(''); + + changeInputValueTo('abcd'); + expect(inputElm).toBeInvalid(); + expect(scope.form.input.$error.pattern).toBe(true); + + changeInputValueTo('12345'); + expect(inputElm).toBeValid(); + expect(scope.form.input.$error.pattern).not.toBe(true); + }); + + it('should not throw an error when scope pattern can\'t be found', function() { + expect(function() { + compileInput(''); + scope.$apply(function() { + scope.foo = 'bar'; + }); + }).not.toThrowMatching(/^\[ngPattern:noregexp\] Expected fooRegexp to be a RegExp but was/); + }); - it('should throw an error when scope pattern is invalid', function() { + it('should throw an error when the scope pattern is not a regular expression', function() { expect(function() { compileInput(''); scope.$apply(function() { - scope.fooRegexp = '/...'; + scope.fooRegexp = {}; + scope.foo = 'bar'; }); }).toThrowMatching(/^\[ngPattern:noregexp\] Expected fooRegexp to be a RegExp but was/); });