feat(input[number]): support step

input[number] will now set the step error if the input value
(ngModel $viewValue) does not fit the step constraint set in the step / ngStep attribute.

Fixes #10597
This commit is contained in:
Martin Staffa
2016-08-15 23:01:53 +02:00
parent 9a8b8aaa96
commit e1da4bed8e
3 changed files with 176 additions and 1 deletions
+2 -1
View File
@@ -576,7 +576,8 @@ var ALIASED_ATTR = {
'ngMaxlength': 'maxlength',
'ngMin': 'min',
'ngMax': 'max',
'ngPattern': 'pattern'
'ngPattern': 'pattern',
'ngStep': 'step'
};
function getBooleanAttrName(element, name) {
+23
View File
@@ -679,7 +679,17 @@ var inputType = {
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
* Can be interpolated.
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
* Can be interpolated.
* @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`,
* but does not trigger HTML5 native validation. Takes an expression.
* @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`,
* but does not trigger HTML5 native validation. Takes an expression.
* @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint.
* Can be interpolated.
* @param {string=} ngStep Like `step`, sets the `max` validation error key if the value entered does not fit the `ngStep` constraint,
* but does not trigger HTML5 native validation. Takes an expression.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
@@ -1549,6 +1559,19 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
ctrl.$validate();
});
}
if (isDefined(attr.step) || attr.ngStep) {
var stepVal;
ctrl.$validators.step = function(modelValue, viewValue) {
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0;
};
attr.$observe('step', function(val) {
stepVal = parseNumberAttrVal(val);
// TODO(matsko): implement validateLater to reduce number of validations
ctrl.$validate();
});
}
}
function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
+151
View File
@@ -2621,6 +2621,157 @@ describe('input', function() {
});
});
describe('step', function() {
it('should validate', function() {
$rootScope.step = 10;
$rootScope.value = 20;
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" step="{{step}}" />');
expect(inputElm.val()).toBe('20');
expect(inputElm).toBeValid();
expect($rootScope.value).toBe(20);
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();
helper.changeInputValueTo('10');
expect(inputElm).toBeValid();
expect(inputElm.val()).toBe('10');
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();
});
it('should validate even if the step value changes on-the-fly', function() {
$rootScope.step = 10;
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" step="{{step}}" />');
helper.changeInputValueTo('10');
expect(inputElm).toBeValid();
expect($rootScope.value).toBe(10);
// 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();
// 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();
// 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();
// 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();
});
});
describe('ngStep', function() {
it('should validate', function() {
$rootScope.step = 10;
$rootScope.value = 20;
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" ng-step="step" />');
expect(inputElm.val()).toBe('20');
expect(inputElm).toBeValid();
expect($rootScope.value).toBe(20);
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();
helper.changeInputValueTo('10');
expect(inputElm).toBeValid();
expect(inputElm.val()).toBe('10');
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();
});
it('should validate even if the step value changes on-the-fly', function() {
$rootScope.step = 10;
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" ng-step="step" />');
helper.changeInputValueTo('10');
expect(inputElm).toBeValid();
expect($rootScope.value).toBe(10);
// 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();
// 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();
// 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();
// 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();
});
});
describe('required', function() {