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

Commit 64f6a61

Browse files
gkalpakpetebacondarwin
authored andcommitted
fix(input): fix step validation for input[number][ng-range-input]
Related to 9a8b8aa and #15257. Fixes the issue discussed in 9a8b8aa#commitcomment-19108436. Fixes #15257 Closes #15264
1 parent 07b8761 commit 64f6a61

File tree

2 files changed

+135
-8
lines changed

2 files changed

+135
-8
lines changed

src/ng/directive/input.js

+52-3
Original file line numberDiff line numberDiff line change
@@ -1543,13 +1543,62 @@ function parseNumberAttrVal(val) {
15431543
return !isNumberNaN(val) ? val : undefined;
15441544
}
15451545

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

1598+
var minVal;
1599+
var maxVal;
1600+
15511601
if (isDefined(attr.min) || attr.ngMin) {
1552-
var minVal;
15531602
ctrl.$validators.min = function(value) {
15541603
return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal;
15551604
};
@@ -1562,7 +1611,6 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15621611
}
15631612

15641613
if (isDefined(attr.max) || attr.ngMax) {
1565-
var maxVal;
15661614
ctrl.$validators.max = function(value) {
15671615
return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal;
15681616
};
@@ -1634,7 +1682,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
16341682
} :
16351683
// ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would
16361684
function stepValidator(modelValue, viewValue) {
1637-
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0;
1685+
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
1686+
isValidForStep(viewValue, minVal || 0, stepVal);
16381687
};
16391688

16401689
setInitialValueAndObserver('step', stepChange);

test/ng/directive/inputSpec.js

+83-5
Original file line numberDiff line numberDiff line change
@@ -2792,7 +2792,6 @@ describe('input', function() {
27922792
});
27932793

27942794
describe('range', function() {
2795-
27962795
var scope;
27972796

27982797
var rangeTestEl = angular.element('<input type="range">');
@@ -2859,7 +2858,6 @@ describe('input', function() {
28592858
expect(scope.age).toBe(50);
28602859
expect(inputElm).toBeValid();
28612860
});
2862-
28632861
} else {
28642862

28652863
it('should reset the model if view is invalid', function() {
@@ -3249,16 +3247,16 @@ describe('input', function() {
32493247
expect(scope.value).toBe(40);
32503248
});
32513249
});
3252-
32533250
}
32543251

32553252

32563253
describe('step', function() {
32573254

32583255
if (supportsRange) {
32593256
// Browsers that implement range will never allow you to set a value that doesn't match the step value
3260-
// However, currently only Firefox fully inplements the spec when setting the value after the step value changes.
3257+
// However, currently only Firefox fully implements the spec when setting the value after the step value changes.
32613258
// Other browsers fail in various edge cases, which is why they are not tested here.
3259+
32623260
it('should round the input value to the nearest step on user input', function() {
32633261
var inputElm = compileRangeInput('ng-model="value" name="alias" step="5"');
32643262

@@ -3321,8 +3319,8 @@ describe('input', function() {
33213319
expect(scope.value).toBe(10);
33223320
expect(scope.form.alias.$error.step).toBeFalsy();
33233321
});
3324-
33253322
} else {
3323+
33263324
it('should validate if "range" is not implemented', function() {
33273325
scope.step = 10;
33283326
scope.value = 20;
@@ -3395,6 +3393,86 @@ describe('input', function() {
33953393
expect(inputElm.val()).toBe('10');
33963394
expect(scope.form.alias.$error.step).toBeFalsy();
33973395
});
3396+
3397+
it('should use the correct "step base" when `[min]` is specified', function() {
3398+
$rootScope.min = 5;
3399+
$rootScope.step = 10;
3400+
$rootScope.value = 10;
3401+
var inputElm = compileRangeInput('ng-model="value" min="{{min}}" step="{{step}}"');
3402+
var ngModel = inputElm.controller('ngModel');
3403+
3404+
expect(inputElm.val()).toBe('10');
3405+
expect(inputElm).toBeInvalid();
3406+
expect(ngModel.$error.step).toBe(true);
3407+
expect($rootScope.value).toBeUndefined();
3408+
3409+
helper.changeInputValueTo('15');
3410+
expect(inputElm).toBeValid();
3411+
expect($rootScope.value).toBe(15);
3412+
3413+
$rootScope.$apply('step = 3');
3414+
expect(inputElm.val()).toBe('15');
3415+
expect(inputElm).toBeInvalid();
3416+
expect(ngModel.$error.step).toBe(true);
3417+
expect($rootScope.value).toBeUndefined();
3418+
3419+
helper.changeInputValueTo('8');
3420+
expect(inputElm).toBeValid();
3421+
expect($rootScope.value).toBe(8);
3422+
3423+
$rootScope.$apply('min = 10; step = 20; value = 30');
3424+
expect(inputElm.val()).toBe('30');
3425+
expect(inputElm).toBeValid();
3426+
expect($rootScope.value).toBe(30);
3427+
3428+
$rootScope.$apply('min = 5');
3429+
expect(inputElm.val()).toBe('30');
3430+
expect(inputElm).toBeInvalid();
3431+
expect(ngModel.$error.step).toBe(true);
3432+
expect($rootScope.value).toBeUndefined();
3433+
3434+
$rootScope.$apply('step = 0.00000001');
3435+
expect(inputElm.val()).toBe('30');
3436+
expect(inputElm).toBeValid();
3437+
expect($rootScope.value).toBe(30);
3438+
3439+
// 0.3 - 0.2 === 0.09999999999999998
3440+
$rootScope.$apply('min = 0.2; step = 0.09999999999999998; value = 0.3');
3441+
expect(inputElm.val()).toBe('0.3');
3442+
expect(inputElm).toBeInvalid();
3443+
expect(ngModel.$error.step).toBe(true);
3444+
expect($rootScope.value).toBeUndefined();
3445+
});
3446+
3447+
it('should correctly validate even in cases where the JS floating point arithmetic fails',
3448+
function() {
3449+
var inputElm = compileRangeInput('ng-model="value" step="0.1"');
3450+
var ngModel = inputElm.controller('ngModel');
3451+
3452+
expect(inputElm.val()).toBe('');
3453+
expect(inputElm).toBeValid();
3454+
expect($rootScope.value).toBeUndefined();
3455+
3456+
helper.changeInputValueTo('0.3');
3457+
expect(inputElm).toBeValid();
3458+
expect($rootScope.value).toBe(0.3);
3459+
3460+
helper.changeInputValueTo('2.9999999999999996');
3461+
expect(inputElm).toBeInvalid();
3462+
expect(ngModel.$error.step).toBe(true);
3463+
expect($rootScope.value).toBeUndefined();
3464+
3465+
// 0.5 % 0.1 === 0.09999999999999998
3466+
helper.changeInputValueTo('0.5');
3467+
expect(inputElm).toBeValid();
3468+
expect($rootScope.value).toBe(0.5);
3469+
3470+
// 3.5 % 0.1 === 0.09999999999999981
3471+
helper.changeInputValueTo('3.5');
3472+
expect(inputElm).toBeValid();
3473+
expect($rootScope.value).toBe(3.5);
3474+
}
3475+
);
33983476
}
33993477
});
34003478

0 commit comments

Comments
 (0)