Permalink
Browse files

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;
1 parent 26d91b6 commit 1be9bb9d3527e0758350c4f7417a4228d8571440 @matsko matsko committed May 27, 2014
Showing with 85 additions and 29 deletions.
  1. +6 −2 src/AngularPublic.js
  2. +30 −25 src/ng/directive/input.js
  3. +49 −2 test/ng/directive/inputSpec.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
}).
@@ -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',
@@ -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('<input type="text" ng-model="value" ng-pattern="regexp" />');
+
+ 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('<input type="text" name="input" ng-model="value" pattern="^\\d+$" />');
+
+ 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('<input type="text" ng-model="foo" ng-pattern="fooRegexp" />');
+ 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('<input type="text" ng-model="foo" ng-pattern="fooRegexp" />');
scope.$apply(function() {
- scope.fooRegexp = '/...';
+ scope.fooRegexp = {};
+ scope.foo = 'bar';
});
}).toThrowMatching(/^\[ngPattern:noregexp\] Expected fooRegexp to be a RegExp but was/);
});

0 comments on commit 1be9bb9

Please sign in to comment.