fix(input): fix step validation for input[number]/input[range]
Related to 9a8b8aa and #15257. Fixes the issue discussed in
https://github.com/angular/angular.js/commit/9a8b8aa#commitcomment-19108436.
Fixes #15257
Closes #15264
This commit is contained in:
@@ -1532,13 +1532,62 @@ function parseNumberAttrVal(val) {
|
||||
return !isNumberNaN(val) ? val : undefined;
|
||||
}
|
||||
|
||||
function isNumberInteger(num) {
|
||||
// See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066
|
||||
// (minus the assumption that `num` is a number)
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return (num | 0) === num;
|
||||
}
|
||||
|
||||
function countDecimals(num) {
|
||||
var numString = num.toString();
|
||||
var decimalSymbolIndex = numString.indexOf('.');
|
||||
|
||||
if (decimalSymbolIndex === -1) {
|
||||
if (-1 < num && num < 1) {
|
||||
// It may be in the exponential notation format (`1e-X`)
|
||||
var match = /e-(\d+)$/.exec(numString);
|
||||
|
||||
if (match) {
|
||||
return Number(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return numString.length - decimalSymbolIndex - 1;
|
||||
}
|
||||
|
||||
function isValidForStep(viewValue, stepBase, step) {
|
||||
// At this point `stepBase` and `step` are expected to be non-NaN values
|
||||
// and `viewValue` is expected to be a valid stringified number.
|
||||
var value = Number(viewValue);
|
||||
|
||||
// Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or
|
||||
// `0.5 % 0.1 !== 0`), we need to convert all numbers to integers.
|
||||
if (!isNumberInteger(value) || !isNumberInteger(stepBase) || !isNumberInteger(step)) {
|
||||
var decimalCount = Math.max(countDecimals(value), countDecimals(stepBase), countDecimals(step));
|
||||
var multiplier = Math.pow(10, decimalCount);
|
||||
|
||||
value = value * multiplier;
|
||||
stepBase = stepBase * multiplier;
|
||||
step = step * multiplier;
|
||||
}
|
||||
|
||||
return (value - stepBase) % step === 0;
|
||||
}
|
||||
|
||||
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
badInputChecker(scope, element, attr, ctrl);
|
||||
numberFormatterParser(ctrl);
|
||||
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
||||
|
||||
var minVal;
|
||||
var maxVal;
|
||||
|
||||
if (isDefined(attr.min) || attr.ngMin) {
|
||||
var minVal;
|
||||
ctrl.$validators.min = function(value) {
|
||||
return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal;
|
||||
};
|
||||
@@ -1551,7 +1600,6 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
}
|
||||
|
||||
if (isDefined(attr.max) || attr.ngMax) {
|
||||
var maxVal;
|
||||
ctrl.$validators.max = function(value) {
|
||||
return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal;
|
||||
};
|
||||
@@ -1566,7 +1614,8 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
if (isDefined(attr.step) || attr.ngStep) {
|
||||
var stepVal;
|
||||
ctrl.$validators.step = function(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0;
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
|
||||
isValidForStep(viewValue, minVal || 0, stepVal);
|
||||
};
|
||||
|
||||
attr.$observe('step', function(val) {
|
||||
@@ -1636,7 +1685,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
} :
|
||||
// ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would
|
||||
function stepValidator(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0;
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
|
||||
isValidForStep(viewValue, minVal || 0, stepVal);
|
||||
};
|
||||
|
||||
setInitialValueAndObserver('step', stepChange);
|
||||
|
||||
@@ -2703,6 +2703,91 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('10');
|
||||
expect($rootScope.form.alias.$error.step).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should use the correct "step base" when `[min]` is specified', function() {
|
||||
$rootScope.min = 5;
|
||||
$rootScope.step = 10;
|
||||
$rootScope.value = 10;
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="number" ng-model="value" min="{{min}}" ' + attrHtml + ' />');
|
||||
var ngModel = inputElm.controller('ngModel');
|
||||
|
||||
expect(inputElm.val()).toBe('10');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
|
||||
helper.changeInputValueTo('15');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(15);
|
||||
|
||||
$rootScope.$apply('step = 3');
|
||||
expect(inputElm.val()).toBe('15');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
|
||||
helper.changeInputValueTo('8');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(8);
|
||||
|
||||
$rootScope.$apply('min = 10; step = 20');
|
||||
helper.changeInputValueTo('30');
|
||||
expect(inputElm.val()).toBe('30');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(30);
|
||||
|
||||
$rootScope.$apply('min = 5');
|
||||
expect(inputElm.val()).toBe('30');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
|
||||
$rootScope.$apply('step = 0.00000001');
|
||||
expect(inputElm.val()).toBe('30');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(30);
|
||||
|
||||
// 0.3 - 0.2 === 0.09999999999999998
|
||||
$rootScope.$apply('min = 0.2; step = (0.3 - 0.2)');
|
||||
helper.changeInputValueTo('0.3');
|
||||
expect(inputElm.val()).toBe('0.3');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should correctly validate even in cases where the JS floating point arithmetic fails',
|
||||
function() {
|
||||
$rootScope.step = 0.1;
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="number" ng-model="value" ' + attrHtml + ' />');
|
||||
var ngModel = inputElm.controller('ngModel');
|
||||
|
||||
expect(inputElm.val()).toBe('');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
|
||||
helper.changeInputValueTo('0.3');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(0.3);
|
||||
|
||||
helper.changeInputValueTo('2.9999999999999996');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
|
||||
// 0.5 % 0.1 === 0.09999999999999998
|
||||
helper.changeInputValueTo('0.5');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(0.5);
|
||||
|
||||
// 3.5 % 0.1 === 0.09999999999999981
|
||||
helper.changeInputValueTo('3.5');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(3.5);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3516,6 +3601,87 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('10');
|
||||
expect(scope.form.alias.$error.step).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should use the correct "step base" when `[min]` is specified', function() {
|
||||
$rootScope.min = 5;
|
||||
$rootScope.step = 10;
|
||||
$rootScope.value = 10;
|
||||
var inputElm = helper.compileInput(
|
||||
'<input type="range" ng-model="value" min="{{min}}" step="{{step}}"" />');
|
||||
var ngModel = inputElm.controller('ngModel');
|
||||
|
||||
expect(inputElm.val()).toBe('10');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
|
||||
helper.changeInputValueTo('15');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(15);
|
||||
|
||||
$rootScope.$apply('step = 3');
|
||||
expect(inputElm.val()).toBe('15');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
|
||||
helper.changeInputValueTo('8');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(8);
|
||||
|
||||
$rootScope.$apply('min = 10; step = 20; value = 30');
|
||||
expect(inputElm.val()).toBe('30');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(30);
|
||||
|
||||
$rootScope.$apply('min = 5');
|
||||
expect(inputElm.val()).toBe('30');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
|
||||
$rootScope.$apply('step = 0.00000001');
|
||||
expect(inputElm.val()).toBe('30');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(30);
|
||||
|
||||
// 0.3 - 0.2 === 0.09999999999999998
|
||||
$rootScope.$apply('min = 0.2; step = 0.09999999999999998; value = 0.3');
|
||||
expect(inputElm.val()).toBe('0.3');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should correctly validate even in cases where the JS floating point arithmetic fails',
|
||||
function() {
|
||||
var inputElm = helper.compileInput('<input type="range" ng-model="value" step="0.1" />');
|
||||
var ngModel = inputElm.controller('ngModel');
|
||||
|
||||
expect(inputElm.val()).toBe('');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
|
||||
helper.changeInputValueTo('0.3');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(0.3);
|
||||
|
||||
helper.changeInputValueTo('2.9999999999999996');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
|
||||
// 0.5 % 0.1 === 0.09999999999999998
|
||||
helper.changeInputValueTo('0.5');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(0.5);
|
||||
|
||||
// 3.5 % 0.1 === 0.09999999999999981
|
||||
helper.changeInputValueTo('3.5');
|
||||
expect(inputElm).toBeValid();
|
||||
expect($rootScope.value).toBe(3.5);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user