diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js
index 7e26ca16f6d1..4c9cdc360924 100644
--- a/src/ng/directive/input.js
+++ b/src/ng/directive/input.js
@@ -1529,13 +1529,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;
};
@@ -1548,7 +1597,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;
};
@@ -1563,7 +1611,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) {
@@ -1633,7 +1682,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);
diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js
index 15471354c56f..40ceff4ac879 100644
--- a/test/ng/directive/inputSpec.js
+++ b/test/ng/directive/inputSpec.js
@@ -2621,154 +2621,173 @@ describe('input', function() {
});
});
- describe('step', function() {
- it('should validate', function() {
- $rootScope.step = 10;
- $rootScope.value = 20;
- var inputElm = helper.compileInput('');
- expect(inputElm.val()).toBe('20');
- expect(inputElm).toBeValid();
- expect($rootScope.value).toBe(20);
- expect($rootScope.form.alias.$error.step).toBeFalsy();
+ forEach({
+ step: 'step="{{step}}"',
+ ngStep: 'ng-step="step"'
+ }, function(attrHtml, attrName) {
- helper.changeInputValueTo('18');
- expect(inputElm).toBeInvalid();
- expect(inputElm.val()).toBe('18');
- expect($rootScope.value).toBeUndefined();
- expect($rootScope.form.alias.$error.step).toBeTruthy();
+ describe(attrName, function() {
- helper.changeInputValueTo('10');
- expect(inputElm).toBeValid();
- expect(inputElm.val()).toBe('10');
- expect($rootScope.value).toBe(10);
- expect($rootScope.form.alias.$error.step).toBeFalsy();
+ it('should validate', function() {
+ $rootScope.step = 10;
+ $rootScope.value = 20;
+ var inputElm = helper.compileInput(
+ '');
- $rootScope.$apply('value = 12');
- expect(inputElm).toBeInvalid();
- expect(inputElm.val()).toBe('12');
- expect($rootScope.value).toBe(12);
- expect($rootScope.form.alias.$error.step).toBeTruthy();
- });
+ expect(inputElm.val()).toBe('20');
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(20);
+ expect($rootScope.form.alias.$error.step).toBeFalsy();
- it('should validate even if the step value changes on-the-fly', function() {
- $rootScope.step = 10;
- var inputElm = helper.compileInput('');
+ helper.changeInputValueTo('18');
+ expect(inputElm).toBeInvalid();
+ expect(inputElm.val()).toBe('18');
+ expect($rootScope.value).toBeUndefined();
+ expect($rootScope.form.alias.$error.step).toBeTruthy();
- helper.changeInputValueTo('10');
- expect(inputElm).toBeValid();
- expect($rootScope.value).toBe(10);
+ helper.changeInputValueTo('10');
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('10');
+ expect($rootScope.value).toBe(10);
+ expect($rootScope.form.alias.$error.step).toBeFalsy();
- // Step changes, but value matches
- $rootScope.$apply('step = 5');
- expect(inputElm.val()).toBe('10');
- expect(inputElm).toBeValid();
- expect($rootScope.value).toBe(10);
- expect($rootScope.form.alias.$error.step).toBeFalsy();
+ $rootScope.$apply('value = 12');
+ expect(inputElm).toBeInvalid();
+ expect(inputElm.val()).toBe('12');
+ expect($rootScope.value).toBe(12);
+ expect($rootScope.form.alias.$error.step).toBeTruthy();
+ });
- // Step changes, value does not match
- $rootScope.$apply('step = 6');
- expect(inputElm).toBeInvalid();
- expect($rootScope.value).toBeUndefined();
- expect(inputElm.val()).toBe('10');
- expect($rootScope.form.alias.$error.step).toBeTruthy();
+ it('should validate even if the step value changes on-the-fly', function() {
+ $rootScope.step = 10;
+ var inputElm = helper.compileInput(
+ '');
- // null = valid
- $rootScope.$apply('step = null');
- expect(inputElm).toBeValid();
- expect($rootScope.value).toBe(10);
- expect(inputElm.val()).toBe('10');
- expect($rootScope.form.alias.$error.step).toBeFalsy();
+ helper.changeInputValueTo('10');
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(10);
- // Step val as string
- $rootScope.$apply('step = "7"');
- expect(inputElm).toBeInvalid();
- expect($rootScope.value).toBeUndefined();
- expect(inputElm.val()).toBe('10');
- expect($rootScope.form.alias.$error.step).toBeTruthy();
+ // Step changes, but value matches
+ $rootScope.$apply('step = 5');
+ expect(inputElm.val()).toBe('10');
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(10);
+ expect($rootScope.form.alias.$error.step).toBeFalsy();
- // unparsable string is ignored
- $rootScope.$apply('step = "abc"');
- expect(inputElm).toBeValid();
- expect($rootScope.value).toBe(10);
- expect(inputElm.val()).toBe('10');
- expect($rootScope.form.alias.$error.step).toBeFalsy();
- });
- });
+ // Step changes, value does not match
+ $rootScope.$apply('step = 6');
+ expect(inputElm).toBeInvalid();
+ expect($rootScope.value).toBeUndefined();
+ expect(inputElm.val()).toBe('10');
+ expect($rootScope.form.alias.$error.step).toBeTruthy();
+ // null = valid
+ $rootScope.$apply('step = null');
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(10);
+ expect(inputElm.val()).toBe('10');
+ expect($rootScope.form.alias.$error.step).toBeFalsy();
- describe('ngStep', function() {
- it('should validate', function() {
- $rootScope.step = 10;
- $rootScope.value = 20;
- var inputElm = helper.compileInput('');
+ // Step val as string
+ $rootScope.$apply('step = "7"');
+ expect(inputElm).toBeInvalid();
+ expect($rootScope.value).toBeUndefined();
+ expect(inputElm.val()).toBe('10');
+ expect($rootScope.form.alias.$error.step).toBeTruthy();
- expect(inputElm.val()).toBe('20');
- expect(inputElm).toBeValid();
- expect($rootScope.value).toBe(20);
- expect($rootScope.form.alias.$error.step).toBeFalsy();
+ // unparsable string is ignored
+ $rootScope.$apply('step = "abc"');
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(10);
+ expect(inputElm.val()).toBe('10');
+ expect($rootScope.form.alias.$error.step).toBeFalsy();
+ });
- helper.changeInputValueTo('18');
- expect(inputElm).toBeInvalid();
- expect(inputElm.val()).toBe('18');
- expect($rootScope.value).toBeUndefined();
- expect($rootScope.form.alias.$error.step).toBeTruthy();
+ 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(
+ '');
+ var ngModel = inputElm.controller('ngModel');
- helper.changeInputValueTo('10');
- expect(inputElm).toBeValid();
- expect(inputElm.val()).toBe('10');
- expect($rootScope.value).toBe(10);
- expect($rootScope.form.alias.$error.step).toBeFalsy();
+ expect(inputElm.val()).toBe('10');
+ expect(inputElm).toBeInvalid();
+ expect(ngModel.$error.step).toBe(true);
+ expect($rootScope.value).toBeUndefined();
- $rootScope.$apply('value = 12');
- expect(inputElm).toBeInvalid();
- expect(inputElm.val()).toBe('12');
- expect($rootScope.value).toBe(12);
- expect($rootScope.form.alias.$error.step).toBeTruthy();
- });
+ helper.changeInputValueTo('15');
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(15);
- it('should validate even if the step value changes on-the-fly', function() {
- $rootScope.step = 10;
- var inputElm = helper.compileInput('');
+ $rootScope.$apply('step = 3');
+ expect(inputElm.val()).toBe('15');
+ expect(inputElm).toBeInvalid();
+ expect(ngModel.$error.step).toBe(true);
+ expect($rootScope.value).toBeUndefined();
- helper.changeInputValueTo('10');
- expect(inputElm).toBeValid();
- expect($rootScope.value).toBe(10);
+ helper.changeInputValueTo('8');
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(8);
- // Step changes, but value matches
- $rootScope.$apply('step = 5');
- expect(inputElm.val()).toBe('10');
- expect(inputElm).toBeValid();
- expect($rootScope.value).toBe(10);
- expect($rootScope.form.alias.$error.step).toBeFalsy();
+ $rootScope.$apply('min = 10; step = 20');
+ helper.changeInputValueTo('30');
+ expect(inputElm.val()).toBe('30');
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(30);
- // Step changes, value does not match
- $rootScope.$apply('step = 6');
- expect(inputElm).toBeInvalid();
- expect($rootScope.value).toBeUndefined();
- expect(inputElm.val()).toBe('10');
- expect($rootScope.form.alias.$error.step).toBeTruthy();
+ $rootScope.$apply('min = 5');
+ expect(inputElm.val()).toBe('30');
+ expect(inputElm).toBeInvalid();
+ expect(ngModel.$error.step).toBe(true);
+ expect($rootScope.value).toBeUndefined();
- // null = valid
- $rootScope.$apply('step = null');
- expect(inputElm).toBeValid();
- expect($rootScope.value).toBe(10);
- expect(inputElm.val()).toBe('10');
- expect($rootScope.form.alias.$error.step).toBeFalsy();
+ $rootScope.$apply('step = 0.00000001');
+ expect(inputElm.val()).toBe('30');
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(30);
- // Step val as string
- $rootScope.$apply('step = "7"');
- expect(inputElm).toBeInvalid();
- expect($rootScope.value).toBeUndefined();
- expect(inputElm.val()).toBe('10');
- expect($rootScope.form.alias.$error.step).toBeTruthy();
+ // 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();
+ });
- // unparsable string is ignored
- $rootScope.$apply('step = "abc"');
- expect(inputElm).toBeValid();
- expect($rootScope.value).toBe(10);
- expect(inputElm.val()).toBe('10');
- expect($rootScope.form.alias.$error.step).toBeFalsy();
+ it('should correctly validate even in cases where the JS floating point arithmetic fails',
+ function() {
+ $rootScope.step = 0.1;
+ var inputElm = helper.compileInput(
+ '');
+ 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);
+ }
+ );
});
});
@@ -3001,7 +3020,6 @@ describe('input', function() {
});
describe('range', function() {
-
var scope;
var rangeTestEl = angular.element('');
@@ -3048,7 +3066,6 @@ describe('input', function() {
expect(scope.age).toBe(50);
expect(inputElm).toBeValid();
});
-
} else {
it('should reset the model if view is invalid', function() {
@@ -3438,7 +3455,6 @@ describe('input', function() {
expect(scope.value).toBe(40);
});
});
-
}
@@ -3448,6 +3464,7 @@ describe('input', function() {
// Browsers that implement range will never allow you to set a value that doesn't match the step value
// 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 = helper.compileInput('');
@@ -3510,8 +3527,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;
@@ -3584,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(
+ '');
+ 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('');
+ 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);
+ }
+ );
}
});
});