Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 7039395

Browse files
committed
fix(input): fix step validation for input[number]/input[range]
Related to 9a8b8aa and #15257. Fixes the issue discussed in 9a8b8aa#commitcomment-19108436. Fixes #15257
1 parent 3b76233 commit 7039395

File tree

2 files changed

+214
-4
lines changed

2 files changed

+214
-4
lines changed

src/ng/directive/input.js

+54-4
Original file line numberDiff line numberDiff line change
@@ -1529,13 +1529,62 @@ function parseNumberAttrVal(val) {
15291529
return !isNumberNaN(val) ? val : undefined;
15301530
}
15311531

1532+
function isNumberInteger(num) {
1533+
// See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066
1534+
// (minus the assumption that `num` is a number)
1535+
1536+
// eslint-disable-next-line no-bitwise
1537+
return (num | 0) === num;
1538+
}
1539+
1540+
function countDecimals(num) {
1541+
var numString = num.toString();
1542+
var decimalSymbolIndex = numString.indexOf('.');
1543+
1544+
if (decimalSymbolIndex === -1) {
1545+
if (-1 < num && num < 1) {
1546+
// It may be in the exponentional notation format (`1e-X`)
1547+
var match = /e-(\d+)$/.exec(numString);
1548+
1549+
if (match) {
1550+
return Number(match[1]);
1551+
}
1552+
}
1553+
1554+
return 0;
1555+
}
1556+
1557+
return numString.length - decimalSymbolIndex - 1;
1558+
}
1559+
1560+
function isValidForStep(viewValue, stepBase, step) {
1561+
// At this point `stepBase` and `step` are expected to be non-NaN values
1562+
// and `viewValue` is expected to be a valid stringified number.
1563+
var value = Number(viewValue);
1564+
1565+
// Due to limitations is Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or
1566+
// `0.5 % 0.1 !== 0`), we need to make sure all numbers are integers before proceeding.
1567+
if (!isNumberInteger(value) || !isNumberInteger(stepBase) || !isNumberInteger(step)) {
1568+
var decimalCount = Math.max(countDecimals(value), countDecimals(stepBase), countDecimals(step));
1569+
var multiplier = Math.pow(10, decimalCount);
1570+
1571+
value = value * multiplier;
1572+
stepBase = stepBase * multiplier;
1573+
step = step * multiplier;
1574+
}
1575+
1576+
return (value - stepBase) % step === 0;
1577+
}
1578+
15321579
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15331580
badInputChecker(scope, element, attr, ctrl);
15341581
numberFormatterParser(ctrl);
15351582
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
15361583

1584+
var minVal;
1585+
var maxVal;
1586+
15371587
if (isDefined(attr.min) || attr.ngMin) {
1538-
var minVal;
15391588
ctrl.$validators.min = function(value) {
15401589
return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal;
15411590
};
@@ -1548,7 +1597,6 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15481597
}
15491598

15501599
if (isDefined(attr.max) || attr.ngMax) {
1551-
var maxVal;
15521600
ctrl.$validators.max = function(value) {
15531601
return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal;
15541602
};
@@ -1563,7 +1611,8 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15631611
if (isDefined(attr.step) || attr.ngStep) {
15641612
var stepVal;
15651613
ctrl.$validators.step = function(modelValue, viewValue) {
1566-
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0;
1614+
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
1615+
isValidForStep(viewValue, minVal || 0, stepVal);
15671616
};
15681617

15691618
attr.$observe('step', function(val) {
@@ -1633,7 +1682,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
16331682
} :
16341683
// ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would
16351684
function stepValidator(modelValue, viewValue) {
1636-
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0;
1685+
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
1686+
isValidForStep(viewValue, minVal || 0, stepVal);
16371687
};
16381688

16391689
setInitialValueAndObserver('step', stepChange);

test/ng/directive/inputSpec.js

+160
Original file line numberDiff line numberDiff line change
@@ -2703,6 +2703,87 @@ describe('input', function() {
27032703
expect(inputElm.val()).toBe('10');
27042704
expect($rootScope.form.alias.$error.step).toBeFalsy();
27052705
});
2706+
2707+
it('should use the correct "step base" when `[min]` is specified', function() {
2708+
$rootScope.min = 5;
2709+
$rootScope.step = 10;
2710+
$rootScope.value = 10;
2711+
var inputElm = helper.compileInput(
2712+
'<input type="number" ng-model="value" min="{{min}}" ' + attrHtml + ' />');
2713+
var ngModel = inputElm.controller('ngModel');
2714+
2715+
expect(inputElm.val()).toBe('10');
2716+
expect(inputElm).toBeInvalid();
2717+
expect(ngModel.$error.step).toBe(true);
2718+
expect($rootScope.value).toBeUndefined();
2719+
2720+
helper.changeInputValueTo('15');
2721+
expect(inputElm).toBeValid();
2722+
expect($rootScope.value).toBe(15);
2723+
2724+
$rootScope.$apply('step = 3');
2725+
expect(inputElm.val()).toBe('15');
2726+
expect(inputElm).toBeInvalid();
2727+
expect(ngModel.$error.step).toBe(true);
2728+
expect($rootScope.value).toBeUndefined();
2729+
2730+
helper.changeInputValueTo('8');
2731+
expect(inputElm).toBeValid();
2732+
expect($rootScope.value).toBe(8);
2733+
2734+
$rootScope.$apply('min = 10; step = 20; value = 30');
2735+
expect(inputElm.val()).toBe('30');
2736+
expect(inputElm).toBeValid();
2737+
expect($rootScope.value).toBe(30);
2738+
2739+
$rootScope.$apply('min = 5');
2740+
expect(inputElm.val()).toBe('30');
2741+
expect(inputElm).toBeInvalid();
2742+
expect(ngModel.$error.step).toBe(true);
2743+
expect($rootScope.value).toBeUndefined();
2744+
2745+
$rootScope.$apply('step = 0.00000001');
2746+
expect(inputElm.val()).toBe('30');
2747+
expect(inputElm).toBeValid();
2748+
expect($rootScope.value).toBe(30);
2749+
2750+
// 0.3 - 0.2 === 0.09999999999999998
2751+
$rootScope.$apply('min = 0.2; step = 0.09999999999999998; value = 0.3');
2752+
expect(inputElm.val()).toBe('0.3');
2753+
expect(inputElm).toBeInvalid();
2754+
expect(ngModel.$error.step).toBe(true);
2755+
expect($rootScope.value).toBeUndefined();
2756+
});
2757+
2758+
it('should correctly validate even in cases where `(x*y % x !== 0)`', function() {
2759+
$rootScope.step = 0.1;
2760+
var inputElm = helper.compileInput(
2761+
'<input type="number" ng-model="value" ' + attrHtml + ' />');
2762+
var ngModel = inputElm.controller('ngModel');
2763+
2764+
expect(inputElm.val()).toBe('');
2765+
expect(inputElm).toBeValid();
2766+
expect($rootScope.value).toBeUndefined();
2767+
2768+
helper.changeInputValueTo('0.3');
2769+
expect(inputElm).toBeValid();
2770+
expect($rootScope.value).toBe(0.3);
2771+
2772+
helper.changeInputValueTo('2.9999999999999996');
2773+
expect(inputElm).toBeInvalid();
2774+
expect(ngModel.$error.step).toBe(true);
2775+
expect($rootScope.value).toBeUndefined();
2776+
2777+
// 0.5 % 0.1 === 0.09999999999999998
2778+
helper.changeInputValueTo('0.5');
2779+
expect(inputElm).toBeValid();
2780+
expect($rootScope.value).toBe(0.5);
2781+
2782+
// 3.5 % 0.1 === 0.09999999999999981
2783+
helper.changeInputValueTo('3.5');
2784+
expect(inputElm).toBeValid();
2785+
expect($rootScope.value).toBe(3.5);
2786+
});
27062787
});
27072788
});
27082789

@@ -3516,6 +3597,85 @@ describe('input', function() {
35163597
expect(inputElm.val()).toBe('10');
35173598
expect(scope.form.alias.$error.step).toBeFalsy();
35183599
});
3600+
3601+
it('should use the correct "step base" when `[min]` is specified', function() {
3602+
$rootScope.min = 5;
3603+
$rootScope.step = 10;
3604+
$rootScope.value = 10;
3605+
var inputElm = helper.compileInput(
3606+
'<input type="range" ng-model="value" min="{{min}}" step="{{step}}"" />');
3607+
var ngModel = inputElm.controller('ngModel');
3608+
3609+
expect(inputElm.val()).toBe('10');
3610+
expect(inputElm).toBeInvalid();
3611+
expect(ngModel.$error.step).toBe(true);
3612+
expect($rootScope.value).toBeUndefined();
3613+
3614+
helper.changeInputValueTo('15');
3615+
expect(inputElm).toBeValid();
3616+
expect($rootScope.value).toBe(15);
3617+
3618+
$rootScope.$apply('step = 3');
3619+
expect(inputElm.val()).toBe('15');
3620+
expect(inputElm).toBeInvalid();
3621+
expect(ngModel.$error.step).toBe(true);
3622+
expect($rootScope.value).toBeUndefined();
3623+
3624+
helper.changeInputValueTo('8');
3625+
expect(inputElm).toBeValid();
3626+
expect($rootScope.value).toBe(8);
3627+
3628+
$rootScope.$apply('min = 10; step = 20; value = 30');
3629+
expect(inputElm.val()).toBe('30');
3630+
expect(inputElm).toBeValid();
3631+
expect($rootScope.value).toBe(30);
3632+
3633+
$rootScope.$apply('min = 5');
3634+
expect(inputElm.val()).toBe('30');
3635+
expect(inputElm).toBeInvalid();
3636+
expect(ngModel.$error.step).toBe(true);
3637+
expect($rootScope.value).toBeUndefined();
3638+
3639+
$rootScope.$apply('step = 0.00000001');
3640+
expect(inputElm.val()).toBe('30');
3641+
expect(inputElm).toBeValid();
3642+
expect($rootScope.value).toBe(30);
3643+
3644+
// 0.3 - 0.2 === 0.09999999999999998
3645+
$rootScope.$apply('min = 0.2; step = 0.09999999999999998; value = 0.3');
3646+
expect(inputElm.val()).toBe('0.3');
3647+
expect(inputElm).toBeInvalid();
3648+
expect(ngModel.$error.step).toBe(true);
3649+
expect($rootScope.value).toBeUndefined();
3650+
});
3651+
3652+
it('should correctly validate even in cases where `(x*y % x !== 0)`', function() {
3653+
var inputElm = helper.compileInput('<input type="range" ng-model="value" step="0.1" />');
3654+
var ngModel = inputElm.controller('ngModel');
3655+
3656+
expect(inputElm.val()).toBe('');
3657+
expect(inputElm).toBeValid();
3658+
expect($rootScope.value).toBeUndefined();
3659+
3660+
helper.changeInputValueTo('0.3');
3661+
expect(inputElm).toBeValid();
3662+
expect($rootScope.value).toBe(0.3);
3663+
3664+
helper.changeInputValueTo('2.9999999999999996');
3665+
expect(inputElm).toBeInvalid();
3666+
expect(ngModel.$error.step).toBe(true);
3667+
expect($rootScope.value).toBeUndefined();
3668+
3669+
// 0.5 % 0.1 === 0.09999999999999998
3670+
helper.changeInputValueTo('0.5');
3671+
expect(inputElm).toBeValid();
3672+
expect($rootScope.value).toBe(0.5);
3673+
3674+
// 3.5 % 0.1 === 0.09999999999999981
3675+
helper.changeInputValueTo('3.5');
3676+
expect(inputElm).toBeValid();
3677+
expect($rootScope.value).toBe(3.5);
3678+
});
35193679
}
35203680
});
35213681
});

0 commit comments

Comments
 (0)