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

Commit

Permalink
feat(input[number]): support step
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Narretz committed Aug 17, 2016
1 parent 9a8b8aa commit e1da4be
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/jqLite.js
Expand Up @@ -576,7 +576,8 @@ var ALIASED_ATTR = {
'ngMaxlength': 'maxlength',
'ngMin': 'min',
'ngMax': 'max',
'ngPattern': 'pattern'
'ngPattern': 'pattern',
'ngStep': 'step'
};

function getBooleanAttrName(element, name) {
Expand Down
23 changes: 23 additions & 0 deletions src/ng/directive/input.js
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
151 changes: 151 additions & 0 deletions test/ng/directive/inputSpec.js
Expand Up @@ -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() {

Expand Down

0 comments on commit e1da4be

Please sign in to comment.