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

Commit 081d06f

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 Closes #15264
1 parent d6c91ea commit 081d06f

File tree

2 files changed

+220
-4
lines changed

2 files changed

+220
-4
lines changed

src/ng/directive/input.js

+54-4
Original file line numberDiff line numberDiff line change
@@ -1532,13 +1532,62 @@ function parseNumberAttrVal(val) {
15321532
return !isNumberNaN(val) ? val : undefined;
15331533
}
15341534

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

1587+
var minVal;
1588+
var maxVal;
1589+
15401590
if (isDefined(attr.min) || attr.ngMin) {
1541-
var minVal;
15421591
ctrl.$validators.min = function(value) {
15431592
return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal;
15441593
};
@@ -1551,7 +1600,6 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15511600
}
15521601

15531602
if (isDefined(attr.max) || attr.ngMax) {
1554-
var maxVal;
15551603
ctrl.$validators.max = function(value) {
15561604
return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal;
15571605
};
@@ -1566,7 +1614,8 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15661614
if (isDefined(attr.step) || attr.ngStep) {
15671615
var stepVal;
15681616
ctrl.$validators.step = function(modelValue, viewValue) {
1569-
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0;
1617+
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
1618+
isValidForStep(viewValue, minVal || 0, stepVal);
15701619
};
15711620

15721621
attr.$observe('step', function(val) {
@@ -1636,7 +1685,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
16361685
} :
16371686
// ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would
16381687
function stepValidator(modelValue, viewValue) {
1639-
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0;
1688+
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
1689+
isValidForStep(viewValue, minVal || 0, stepVal);
16401690
};
16411691

16421692
setInitialValueAndObserver('step', stepChange);

test/ng/directive/inputSpec.js

+166
Original file line numberDiff line numberDiff line change
@@ -2703,6 +2703,91 @@ 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');
2735+
helper.changeInputValueTo('30');
2736+
expect(inputElm.val()).toBe('30');
2737+
expect(inputElm).toBeValid();
2738+
expect($rootScope.value).toBe(30);
2739+
2740+
$rootScope.$apply('min = 5');
2741+
expect(inputElm.val()).toBe('30');
2742+
expect(inputElm).toBeInvalid();
2743+
expect(ngModel.$error.step).toBe(true);
2744+
expect($rootScope.value).toBeUndefined();
2745+
2746+
$rootScope.$apply('step = 0.00000001');
2747+
expect(inputElm.val()).toBe('30');
2748+
expect(inputElm).toBeValid();
2749+
expect($rootScope.value).toBe(30);
2750+
2751+
// 0.3 - 0.2 === 0.09999999999999998
2752+
$rootScope.$apply('min = 0.2; step = (0.3 - 0.2)');
2753+
helper.changeInputValueTo('0.3');
2754+
expect(inputElm.val()).toBe('0.3');
2755+
expect(inputElm).toBeInvalid();
2756+
expect(ngModel.$error.step).toBe(true);
2757+
expect($rootScope.value).toBeUndefined();
2758+
});
2759+
2760+
it('should correctly validate even in cases where the JS floating point arithmetic fails',
2761+
function() {
2762+
$rootScope.step = 0.1;
2763+
var inputElm = helper.compileInput(
2764+
'<input type="number" ng-model="value" ' + attrHtml + ' />');
2765+
var ngModel = inputElm.controller('ngModel');
2766+
2767+
expect(inputElm.val()).toBe('');
2768+
expect(inputElm).toBeValid();
2769+
expect($rootScope.value).toBeUndefined();
2770+
2771+
helper.changeInputValueTo('0.3');
2772+
expect(inputElm).toBeValid();
2773+
expect($rootScope.value).toBe(0.3);
2774+
2775+
helper.changeInputValueTo('2.9999999999999996');
2776+
expect(inputElm).toBeInvalid();
2777+
expect(ngModel.$error.step).toBe(true);
2778+
expect($rootScope.value).toBeUndefined();
2779+
2780+
// 0.5 % 0.1 === 0.09999999999999998
2781+
helper.changeInputValueTo('0.5');
2782+
expect(inputElm).toBeValid();
2783+
expect($rootScope.value).toBe(0.5);
2784+
2785+
// 3.5 % 0.1 === 0.09999999999999981
2786+
helper.changeInputValueTo('3.5');
2787+
expect(inputElm).toBeValid();
2788+
expect($rootScope.value).toBe(3.5);
2789+
}
2790+
);
27062791
});
27072792
});
27082793

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

0 commit comments

Comments
 (0)