Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
fix(NgModel): ensure pattern and ngPattern use the same validator
Browse files Browse the repository at this point in the history
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;
  • Loading branch information
matsko committed Jun 13, 2014
1 parent 26d91b6 commit 1be9bb9
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 29 deletions.
8 changes: 6 additions & 2 deletions src/AngularPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
ngModelDirective,
ngListDirective,
ngChangeDirective,
patternDirective,
patternDirective,
requiredDirective,
requiredDirective,
minlengthDirective,
Expand Down Expand Up @@ -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
}).
Expand Down
55 changes: 30 additions & 25 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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',
Expand Down
51 changes: 49 additions & 2 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
Expand Down

0 comments on commit 1be9bb9

Please sign in to comment.