Skip to content
Permalink
Browse files

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
  • Loading branch information
gkalpak committed Oct 19, 2016
1 parent 9bda994 commit e73a853ee89e2896bc010a73018ad86ff3c8b9cf
Showing with 135 additions and 8 deletions.
  1. +52 −3 src/ng/directive/input.js
  2. +83 −5 test/ng/directive/inputSpec.js
@@ -1546,13 +1546,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);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
numberFormatterParser(ctrl);

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;
};
@@ -1565,7 +1614,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;
};
@@ -1637,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);
@@ -2792,7 +2792,6 @@ describe('input', function() {
});

describe('range', function() {

var scope;

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

} else {

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

}


describe('step', function() {

if (supportsRange) {
// Browsers that implement range will never allow you to set a value that doesn't match the step value
// However, currently only Firefox fully inplements the spec when setting the value after the step value changes.
// However, currently only Firefox fully implements the spec when setting the value after the step value changes.
// Other browsers fail in various edge cases, which is why they are not tested here.

it('should round the input value to the nearest step on user input', function() {
var inputElm = compileRangeInput('ng-model="value" name="alias" step="5"');

@@ -3321,8 +3319,8 @@ describe('input', function() {
expect(scope.value).toBe(10);
expect(scope.form.alias.$error.step).toBeFalsy();
});

} else {

it('should validate if "range" is not implemented', function() {
scope.step = 10;
scope.value = 20;
@@ -3395,6 +3393,86 @@ 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 = compileRangeInput('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 = compileRangeInput('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);
}
);
}
});

0 comments on commit e73a853

Please sign in to comment.
You can’t perform that action at this time.