From e2da8b0e46de498e0e176897e22c2c6e07c87f7a Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Sat, 5 Oct 2013 23:49:21 -0400 Subject: [PATCH] fix(input): determine input[type=number] validity with ValidityState On Chromium / Safari / Opera, currently: When a non-numeric string is entered into an input[type=number], the browser reports the value and innerText as the empty string. Without the ngRequire directive, the empty string is considered a valid value. This leads to false-positives. The aim of this patch is to suppress these false positives on browsers which implement the `ValidityState` object. Input directives may subscribe to check for `ValidityState` changes, and use the ValidityState during their processing. This allows the differentiation between "valid" and "invalid" empty strings. Closes #2144 --- src/ng/directive/input.js | 33 ++++++++++++++++++++++++++------- test/ng/directive/inputSpec.js | 11 +++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 50a7471bb28f..310f746d459c 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -388,6 +388,13 @@ function isEmpty(value) { return isUndefined(value) || value === '' || value === null || value !== value; } +function validityChanged(ctrl, element) { + return ctrl.$checkValidity && !equals(ctrl.$validityState, element.prop('validity')); +} + +function isBadInput(ctrl) { + return ctrl.$validityState && ctrl.$validityState.badInput; +} function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { @@ -401,7 +408,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { value = trim(value); } - if (ctrl.$viewValue !== value) { + if (ctrl.$viewValue !== value || validityChanged(ctrl, element)) { scope.$apply(function() { ctrl.$setViewValue(value); }); @@ -522,12 +529,21 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { } } +function numberIsBadInput(ctrl) { + if (!isEmpty(ctrl.$viewValue) && NUMBER_REGEXP.test(ctrl.$viewValue)) { + return false; + } + return ctrl.$validityState && ctrl.$validityState.badInput; +} + + function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { + ctrl.$checkValidity = true; textInputType(scope, element, attr, ctrl, $sniffer, $browser); ctrl.$parsers.push(function(value) { var empty = isEmpty(value); - if (empty || NUMBER_REGEXP.test(value)) { + if (!numberIsBadInput(ctrl) && (empty || NUMBER_REGEXP.test(value))) { ctrl.$setValidity('number', true); return value === '' ? null : (empty ? value : parseFloat(value)); } else { @@ -546,7 +562,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { if (!isEmpty(value) && value < min) { ctrl.$setValidity('min', false); return undefined; - } else { + } else if (!isEmpty(value)) { ctrl.$setValidity('min', true); return value; } @@ -562,7 +578,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { if (!isEmpty(value) && value > max) { ctrl.$setValidity('max', false); return undefined; - } else { + } else if (!isEmpty(value)) { ctrl.$setValidity('max', true); return value; } @@ -574,7 +590,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { ctrl.$formatters.push(function(value) { - if (isEmpty(value) || isNumber(value)) { + if (!isBadInput(ctrl) && (isEmpty(value) || isNumber(value))) { ctrl.$setValidity('number', true); return value; } else { @@ -972,6 +988,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ this.$valid = true; this.$invalid = false; this.$name = $attr.name; + this.$validityState = copy($element.prop('validity')); var ngModelGet = $parse($attr.ngModel), ngModelSet = ngModelGet.assign; @@ -1116,15 +1133,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ var ctrl = this; $scope.$watch(function ngModelWatch() { - var value = ngModelGet($scope); + var value = ngModelGet($scope), validity = $element.prop('validity'); // if scope model value and ngModel value are out of sync - if (ctrl.$modelValue !== value) { + if (ctrl.$modelValue !== value || + (ctrl.$checkValidity && !equals(validity, ctrl.$validityState))) { var formatters = ctrl.$formatters, idx = formatters.length; ctrl.$modelValue = value; + ctrl.$validityState = ctrl.$checkValidity && copy(validity); while(idx--) { value = formatters[idx](value); } diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index facc2b806bc9..b5a44d949862 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -651,6 +651,17 @@ describe('input', function() { }); + it('should invalidate non-numeric values', function() { + compileInput(''); + + scope.$apply(function() { + scope.age = 'gerbils'; + }); + scope.$digest(); + expect(inputElm).toBeInvalid(); + }); + + describe('min', function() { it('should validate', function() {