Skip to content

Commit

Permalink
fix(input[number]): validate min/max against viewValue
Browse files Browse the repository at this point in the history
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 angular#12761
Closes angular#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);
    };
  }
}
```
  • Loading branch information
Narretz committed Nov 17, 2017
1 parent 12cf994 commit aa3f951
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 4 deletions.
8 changes: 4 additions & 4 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
98 changes: 98 additions & 0 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>');

Expand Down Expand Up @@ -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}}" />');
Expand Down Expand Up @@ -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" />');
Expand Down Expand Up @@ -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}}" />');
Expand Down Expand Up @@ -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" />');
Expand Down

0 comments on commit aa3f951

Please sign in to comment.