fix(input[number]): validate min/max against viewValue

This brings the validation in line with HTML5 validation, i.e. what the user has entered
is validated, and not a possibly transformed value.

Fixes #12761
Closes #16325

BREAKING CHANGE

`input[type=number]` with `ngModel` now validates the input for the `max`/`min` restriction against
the `ngModelController.$viewValue` instead of against the `ngModelController.$modelValue`.

This affects apps that use `$parsers` or `$formatters` to transform the input / model value.

If you rely on the $modelValue validation, you can overwrite the `min`/`max` validator from a custom directive, as seen in the following example directive definition object:

```
{
  restrict: 'A',
  require: 'ngModel',
  link: function(scope, element, attrs, ctrl) {
    var maxValidator = ctrl.$validators.max;

    ctrk.$validators.max = function(modelValue, viewValue) {
      return maxValidator(modelValue, modelValue);
    };
  }
}
```
This commit is contained in:
Martin Staffa
2017-11-17 12:28:03 +01:00
committed by GitHub
parent 12cf994fcc
commit aa3f951330
2 changed files with 102 additions and 4 deletions
+4 -4
View File
@@ -1604,8 +1604,8 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
var maxVal;
if (isDefined(attr.min) || attr.ngMin) {
ctrl.$validators.min = function(value) {
return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal;
ctrl.$validators.min = function(modelValue, viewValue) {
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
};
attr.$observe('min', function(val) {
@@ -1616,8 +1616,8 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
if (isDefined(attr.max) || attr.ngMax) {
ctrl.$validators.max = function(value) {
return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal;
ctrl.$validators.max = function(modelValue, viewValue) {
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
};
attr.$observe('max', function(val) {
+98
View File
@@ -2284,6 +2284,15 @@ describe('input', function() {
describe('number', function() {
// Helpers for min / max tests
var subtract = function(value) {
return value - 5;
};
var add = function(value) {
return value + 5;
};
it('should reset the model if view is invalid', function() {
var inputElm = helper.compileInput('<input type="number" ng-model="age"/>');
@@ -2465,6 +2474,29 @@ describe('input', function() {
expect($rootScope.form.alias.$error.min).toBeFalsy();
});
it('should validate against the viewValue', function() {
var inputElm = helper.compileInput(
'<input type="number" ng-model-options="{allowInvalid: true}" ng-model="value" name="alias" min="10" />');
var ngModelCtrl = inputElm.controller('ngModel');
ngModelCtrl.$parsers.push(subtract);
helper.changeInputValueTo('10');
expect(inputElm).toBeValid();
expect($rootScope.value).toBe(5);
expect($rootScope.form.alias.$error.min).toBeFalsy();
ngModelCtrl.$parsers.pop();
ngModelCtrl.$parsers.push(add);
helper.changeInputValueTo('5');
expect(inputElm).toBeInvalid();
expect($rootScope.form.alias.$error.min).toBeTruthy();
expect($rootScope.value).toBe(10);
});
it('should validate even if min value changes on-the-fly', function() {
$rootScope.min = undefined;
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" min="{{min}}" />');
@@ -2511,6 +2543,28 @@ describe('input', function() {
expect($rootScope.form.alias.$error.min).toBeFalsy();
});
it('should validate against the viewValue', function() {
var inputElm = helper.compileInput(
'<input type="number" ng-model-options="{allowInvalid: true}" ng-model="value" name="alias" ng-min="10" />');
var ngModelCtrl = inputElm.controller('ngModel');
ngModelCtrl.$parsers.push(subtract);
helper.changeInputValueTo('10');
expect(inputElm).toBeValid();
expect($rootScope.value).toBe(5);
expect($rootScope.form.alias.$error.min).toBeFalsy();
ngModelCtrl.$parsers.pop();
ngModelCtrl.$parsers.push(add);
helper.changeInputValueTo('5');
expect(inputElm).toBeInvalid();
expect($rootScope.form.alias.$error.min).toBeTruthy();
expect($rootScope.value).toBe(10);
});
it('should validate even if the ngMin value changes on-the-fly', function() {
$rootScope.min = undefined;
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" ng-min="min" />');
@@ -2558,6 +2612,28 @@ describe('input', function() {
expect($rootScope.form.alias.$error.max).toBeFalsy();
});
it('should validate against the viewValue', function() {
var inputElm = helper.compileInput('<input type="number"' +
'ng-model-options="{allowInvalid: true}" ng-model="value" name="alias" max="10" />');
var ngModelCtrl = inputElm.controller('ngModel');
ngModelCtrl.$parsers.push(add);
helper.changeInputValueTo('10');
expect(inputElm).toBeValid();
expect($rootScope.value).toBe(15);
expect($rootScope.form.alias.$error.max).toBeFalsy();
ngModelCtrl.$parsers.pop();
ngModelCtrl.$parsers.push(subtract);
helper.changeInputValueTo('15');
expect(inputElm).toBeInvalid();
expect($rootScope.form.alias.$error.max).toBeTruthy();
expect($rootScope.value).toBe(10);
});
it('should validate even if max value changes on-the-fly', function() {
$rootScope.max = undefined;
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" max="{{max}}" />');
@@ -2604,6 +2680,28 @@ describe('input', function() {
expect($rootScope.form.alias.$error.max).toBeFalsy();
});
it('should validate against the viewValue', function() {
var inputElm = helper.compileInput('<input type="number"' +
'ng-model-options="{allowInvalid: true}" ng-model="value" name="alias" ng-max="10" />');
var ngModelCtrl = inputElm.controller('ngModel');
ngModelCtrl.$parsers.push(add);
helper.changeInputValueTo('10');
expect(inputElm).toBeValid();
expect($rootScope.value).toBe(15);
expect($rootScope.form.alias.$error.max).toBeFalsy();
ngModelCtrl.$parsers.pop();
ngModelCtrl.$parsers.push(subtract);
helper.changeInputValueTo('15');
expect(inputElm).toBeInvalid();
expect($rootScope.form.alias.$error.max).toBeTruthy();
expect($rootScope.value).toBe(10);
});
it('should validate even if the ngMax value changes on-the-fly', function() {
$rootScope.max = undefined;
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" ng-max="max" />');