From aeb186a9f9111f6d8bcb3c39219cc78a695050eb Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Sat, 15 Aug 2015 22:32:13 +0200 Subject: [PATCH] fix(ngModel): let aliased validator directives work on any element `ng(Pattern|Minlength|Maxlength)` directives will now validate the `ngModel` when on an element that is not an `input` or a `textarea`. Previously, only their HTML5 counterparts worked on every element. This is because the three validators were extracted into separate directives (see 26d91b653ac224d9d4166fea855346f5e4c4a7b4 and 1be9bb9d3527e0758350c4f7417a4228d8571440), whereas the aliased attribute handling assumes the validators will only exist on `input|textarea` (see d9b90d7c10a8e1bacbee0aeb7e86093cca9e8ed2 and 25541c1f876a16c892d71faae11727bec7bba98c). Since `ngMin` and `ngMax` are also aliased attributes, this means observers of `min` and `max` will be fired if `ngMin` and `ngMax` change. This will happen on any element, even if it does not have an `ngModel`. However, since min/max validators are only ever added as part of the `input[number|textarea]` types, even if the element has an `ngModel`, no validators will be added. Finally the commit also tests that `ng-required` works on any element, although that validator worked on all elements before this fix. Fixes #12158 Closes #12658 --- src/jqLite.js | 5 +- src/ng/compile.js | 2 +- test/ng/directive/validatorsSpec.js | 73 +++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index 2dd1d6102bae..4b6d3ed39e99 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -556,9 +556,8 @@ function getBooleanAttrName(element, name) { return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr; } -function getAliasedAttrName(element, name) { - var nodeName = element.nodeName; - return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name]; +function getAliasedAttrName(name) { + return ALIASED_ATTR[name]; } forEach({ diff --git a/src/ng/compile.js b/src/ng/compile.js index d8ed0c5e0e3d..9802ed55f8d2 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1077,7 +1077,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var node = this.$$element[0], booleanKey = getBooleanAttrName(node, key), - aliasedKey = getAliasedAttrName(node, key), + aliasedKey = getAliasedAttrName(key), observer = key, nodeName; diff --git a/test/ng/directive/validatorsSpec.js b/test/ng/directive/validatorsSpec.js index 6b20d6e11086..8f603cf306a5 100644 --- a/test/ng/directive/validatorsSpec.js +++ b/test/ng/directive/validatorsSpec.js @@ -219,6 +219,24 @@ describe('validators', function() { expect($rootScope.form.test.$modelValue).toBe('12340'); expect(inputElm).toBeValid(); }); + + + it('should validate on non-input elements', inject(function($compile) { + $rootScope.pattern = '\\d{4}'; + var elm = $compile('')($rootScope); + var elmNg = $compile('')($rootScope); + var ctrl = elm.controller('ngModel'); + var ctrlNg = elmNg.controller('ngModel'); + + expect(ctrl.$error.pattern).not.toBe(true); + expect(ctrlNg.$error.pattern).not.toBe(true); + + ctrl.$setViewValue('12'); + ctrlNg.$setViewValue('12'); + + expect(ctrl.$error.pattern).toBe(true); + expect(ctrlNg.$error.pattern).toBe(true); + })); }); @@ -283,6 +301,24 @@ describe('validators', function() { helper.changeInputValueTo('12345'); expect(ctrl.$isEmpty).toHaveBeenCalledWith('12345'); }); + + + it('should validate on non-input elements', inject(function($compile) { + $rootScope.min = 3; + var elm = $compile('')($rootScope); + var elmNg = $compile('')($rootScope); + var ctrl = elm.controller('ngModel'); + var ctrlNg = elmNg.controller('ngModel'); + + expect(ctrl.$error.minlength).not.toBe(true); + expect(ctrlNg.$error.minlength).not.toBe(true); + + ctrl.$setViewValue('12'); + ctrlNg.$setViewValue('12'); + + expect(ctrl.$error.minlength).toBe(true); + expect(ctrlNg.$error.minlength).toBe(true); + })); }); @@ -453,6 +489,24 @@ describe('validators', function() { helper.changeInputValueTo('12345'); expect(ctrl.$isEmpty).toHaveBeenCalledWith('12345'); }); + + + it('should validate on non-input elements', inject(function($compile) { + $rootScope.max = 3; + var elm = $compile('')($rootScope); + var elmNg = $compile('')($rootScope); + var ctrl = elm.controller('ngModel'); + var ctrlNg = elmNg.controller('ngModel'); + + expect(ctrl.$error.maxlength).not.toBe(true); + expect(ctrlNg.$error.maxlength).not.toBe(true); + + ctrl.$setViewValue('1234'); + ctrlNg.$setViewValue('1234'); + + expect(ctrl.$error.maxlength).toBe(true); + expect(ctrlNg.$error.maxlength).toBe(true); + })); }); @@ -547,6 +601,7 @@ describe('validators', function() { expect(inputElm).toBeValid(); }); + it('should validate emptiness against the viewValue', function() { var inputElm = helper.compileInput(''); @@ -560,5 +615,23 @@ describe('validators', function() { helper.changeInputValueTo('12345'); expect(ctrl.$isEmpty).toHaveBeenCalledWith('12345'); }); + + + it('should validate on non-input elements', inject(function($compile) { + $rootScope.value = '12'; + var elm = $compile('')($rootScope); + var elmNg = $compile('')($rootScope); + var ctrl = elm.controller('ngModel'); + var ctrlNg = elmNg.controller('ngModel'); + + expect(ctrl.$error.required).not.toBe(true); + expect(ctrlNg.$error.required).not.toBe(true); + + ctrl.$setViewValue(''); + ctrlNg.$setViewValue(''); + + expect(ctrl.$error.required).toBe(true); + expect(ctrlNg.$error.required).toBe(true); + })); }); });