Skip to content

Commit e2da8b0

Browse files
author
Caitlin Potter
committed
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 angular#2144
1 parent 47f7bd7 commit e2da8b0

File tree

2 files changed

+37
-7
lines changed

2 files changed

+37
-7
lines changed

src/ng/directive/input.js

+26-7
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,13 @@ function isEmpty(value) {
388388
return isUndefined(value) || value === '' || value === null || value !== value;
389389
}
390390

391+
function validityChanged(ctrl, element) {
392+
return ctrl.$checkValidity && !equals(ctrl.$validityState, element.prop('validity'));
393+
}
394+
395+
function isBadInput(ctrl) {
396+
return ctrl.$validityState && ctrl.$validityState.badInput;
397+
}
391398

392399
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
393400

@@ -401,7 +408,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
401408
value = trim(value);
402409
}
403410

404-
if (ctrl.$viewValue !== value) {
411+
if (ctrl.$viewValue !== value || validityChanged(ctrl, element)) {
405412
scope.$apply(function() {
406413
ctrl.$setViewValue(value);
407414
});
@@ -522,12 +529,21 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
522529
}
523530
}
524531

532+
function numberIsBadInput(ctrl) {
533+
if (!isEmpty(ctrl.$viewValue) && NUMBER_REGEXP.test(ctrl.$viewValue)) {
534+
return false;
535+
}
536+
return ctrl.$validityState && ctrl.$validityState.badInput;
537+
}
538+
539+
525540
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
541+
ctrl.$checkValidity = true;
526542
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
527543

528544
ctrl.$parsers.push(function(value) {
529545
var empty = isEmpty(value);
530-
if (empty || NUMBER_REGEXP.test(value)) {
546+
if (!numberIsBadInput(ctrl) && (empty || NUMBER_REGEXP.test(value))) {
531547
ctrl.$setValidity('number', true);
532548
return value === '' ? null : (empty ? value : parseFloat(value));
533549
} else {
@@ -546,7 +562,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
546562
if (!isEmpty(value) && value < min) {
547563
ctrl.$setValidity('min', false);
548564
return undefined;
549-
} else {
565+
} else if (!isEmpty(value)) {
550566
ctrl.$setValidity('min', true);
551567
return value;
552568
}
@@ -562,7 +578,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
562578
if (!isEmpty(value) && value > max) {
563579
ctrl.$setValidity('max', false);
564580
return undefined;
565-
} else {
581+
} else if (!isEmpty(value)) {
566582
ctrl.$setValidity('max', true);
567583
return value;
568584
}
@@ -574,7 +590,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
574590

575591
ctrl.$formatters.push(function(value) {
576592

577-
if (isEmpty(value) || isNumber(value)) {
593+
if (!isBadInput(ctrl) && (isEmpty(value) || isNumber(value))) {
578594
ctrl.$setValidity('number', true);
579595
return value;
580596
} else {
@@ -972,6 +988,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
972988
this.$valid = true;
973989
this.$invalid = false;
974990
this.$name = $attr.name;
991+
this.$validityState = copy($element.prop('validity'));
975992

976993
var ngModelGet = $parse($attr.ngModel),
977994
ngModelSet = ngModelGet.assign;
@@ -1116,15 +1133,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
11161133
var ctrl = this;
11171134

11181135
$scope.$watch(function ngModelWatch() {
1119-
var value = ngModelGet($scope);
1136+
var value = ngModelGet($scope), validity = $element.prop('validity');
11201137

11211138
// if scope model value and ngModel value are out of sync
1122-
if (ctrl.$modelValue !== value) {
1139+
if (ctrl.$modelValue !== value ||
1140+
(ctrl.$checkValidity && !equals(validity, ctrl.$validityState))) {
11231141

11241142
var formatters = ctrl.$formatters,
11251143
idx = formatters.length;
11261144

11271145
ctrl.$modelValue = value;
1146+
ctrl.$validityState = ctrl.$checkValidity && copy(validity);
11281147
while(idx--) {
11291148
value = formatters[idx](value);
11301149
}

test/ng/directive/inputSpec.js

+11
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,17 @@ describe('input', function() {
651651
});
652652

653653

654+
it('should invalidate non-numeric values', function() {
655+
compileInput('<input type="number" ng-model="age" />');
656+
657+
scope.$apply(function() {
658+
scope.age = 'gerbils';
659+
});
660+
scope.$digest();
661+
expect(inputElm).toBeInvalid();
662+
});
663+
664+
654665
describe('min', function() {
655666

656667
it('should validate', function() {

0 commit comments

Comments
 (0)