From 19da9974f8640a3a29ec267da8e18588be2dd8eb Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Sat, 15 Aug 2015 18:55:34 +0200 Subject: [PATCH] fix(ngModel): validate pattern against the viewValue Since the HTML5 pattern validation constraint validates the input value, we should also validate against the viewValue. While this worked in core up to Angular 1.2, in 1.3, we changed not only validation, but the way `input[date]` and `input[number]` are handled - they parse their input values into `Date` and `Number` respectively, which cannot be validated by a regex. Fixes #12344 BREAKING CHANGE: The `ngPattern` and `pattern` directives will validate the regex against the `viewValue` of `ngModel`, i.e. the value of the model before the $parsers are applied. Previously, the modelValue (the result of the $parsers) was validated. This fixes issues where `input[date]` and `input[number]` cannot be validated because the viewValue string is parsed into `Date` and `Number` respectively (starting with Angular 1.3). It also brings the directives in line with HTML5 constraint validation, which validates against the input value. This change is unlikely to cause applications to fail, because even in Angular 1.2, the value that was validated by pattern could have been manipulated by the $parsers, as all validation was done inside this pipeline. If you rely on the pattern being validated against the modelValue, you must create your own validator directive that overwrites the built-in pattern validator: ``` .directive('patternModelOverwrite', function patternModelOverwriteDirective() { return { restrict: 'A', require: '?ngModel', priority: 1, compile: function() { var regexp, patternExp; return { pre: function(scope, elm, attr, ctrl) { if (!ctrl) return; attr.$observe('pattern', function(regex) { /** * The built-in directive will call our overwritten validator * (see below). We just need to update the regex. * The preLink fn guaranetees our observer is called first. */ if (isString(regex) && regex.length > 0) { regex = new RegExp('^' + regex + '$'); } if (regex && !regex.test) { //The built-in validator will throw at this point return; } regexp = regex || undefined; }); }, post: function(scope, elm, attr, ctrl) { if (!ctrl) return; regexp, patternExp = attr.ngPattern || attr.pattern; //The postLink fn guarantees we overwrite the built-in pattern validator ctrl.$validators.pattern = function(value) { return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value); }; } }; } }; }); ``` --- src/ng/directive/validators.js | 5 +++-- test/ng/directive/validatorsSpec.js | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ng/directive/validators.js b/src/ng/directive/validators.js index ef7650f10ceb..474402b107cc 100644 --- a/src/ng/directive/validators.js +++ b/src/ng/directive/validators.js @@ -43,8 +43,9 @@ var patternDirective = function() { ctrl.$validate(); }); - ctrl.$validators.pattern = function(value) { - return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value); + ctrl.$validators.pattern = function(modelValue, viewValue) { + // HTML5 pattern constraint validates the input value, so we validate the viewValue + return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue); }; } }; diff --git a/test/ng/directive/validatorsSpec.js b/test/ng/directive/validatorsSpec.js index 048f8cc0756c..6b20d6e11086 100644 --- a/test/ng/directive/validatorsSpec.js +++ b/test/ng/directive/validatorsSpec.js @@ -204,6 +204,21 @@ describe('validators', function() { expect($rootScope.form.test.$error.pattern).toBe(true); expect(inputElm).not.toBeValid(); }); + + + it('should validate the viewValue and not the modelValue', function() { + var inputElm = helper.compileInput(''); + var ctrl = inputElm.controller('ngModel'); + + ctrl.$parsers.push(function(value) { + return (value * 10) + ''; + }); + + helper.changeInputValueTo('1234'); + expect($rootScope.form.test.$error.pattern).not.toBe(true); + expect($rootScope.form.test.$modelValue).toBe('12340'); + expect(inputElm).toBeValid(); + }); });