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

Commit e1da4be

Browse files
committed
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
1 parent 9a8b8aa commit e1da4be

File tree

3 files changed

+176
-1
lines changed

3 files changed

+176
-1
lines changed

src/jqLite.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,8 @@ var ALIASED_ATTR = {
576576
'ngMaxlength': 'maxlength',
577577
'ngMin': 'min',
578578
'ngMax': 'max',
579-
'ngPattern': 'pattern'
579+
'ngPattern': 'pattern',
580+
'ngStep': 'step'
580581
};
581582

582583
function getBooleanAttrName(element, name) {

src/ng/directive/input.js

+23
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,17 @@ var inputType = {
679679
* @param {string} ngModel Assignable angular expression to data-bind to.
680680
* @param {string=} name Property name of the form under which the control is published.
681681
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
682+
* Can be interpolated.
682683
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
684+
* Can be interpolated.
685+
* @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`,
686+
* but does not trigger HTML5 native validation. Takes an expression.
687+
* @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`,
688+
* but does not trigger HTML5 native validation. Takes an expression.
689+
* @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint.
690+
* Can be interpolated.
691+
* @param {string=} ngStep Like `step`, sets the `max` validation error key if the value entered does not fit the `ngStep` constraint,
692+
* but does not trigger HTML5 native validation. Takes an expression.
683693
* @param {string=} required Sets `required` validation error key if the value is not entered.
684694
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
685695
* 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) {
15491559
ctrl.$validate();
15501560
});
15511561
}
1562+
1563+
if (isDefined(attr.step) || attr.ngStep) {
1564+
var stepVal;
1565+
ctrl.$validators.step = function(modelValue, viewValue) {
1566+
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0;
1567+
};
1568+
1569+
attr.$observe('step', function(val) {
1570+
stepVal = parseNumberAttrVal(val);
1571+
// TODO(matsko): implement validateLater to reduce number of validations
1572+
ctrl.$validate();
1573+
});
1574+
}
15521575
}
15531576

15541577
function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {

test/ng/directive/inputSpec.js

+151
Original file line numberDiff line numberDiff line change
@@ -2621,6 +2621,157 @@ describe('input', function() {
26212621
});
26222622
});
26232623

2624+
describe('step', function() {
2625+
it('should validate', function() {
2626+
$rootScope.step = 10;
2627+
$rootScope.value = 20;
2628+
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" step="{{step}}" />');
2629+
2630+
expect(inputElm.val()).toBe('20');
2631+
expect(inputElm).toBeValid();
2632+
expect($rootScope.value).toBe(20);
2633+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2634+
2635+
helper.changeInputValueTo('18');
2636+
expect(inputElm).toBeInvalid();
2637+
expect(inputElm.val()).toBe('18');
2638+
expect($rootScope.value).toBeUndefined();
2639+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2640+
2641+
helper.changeInputValueTo('10');
2642+
expect(inputElm).toBeValid();
2643+
expect(inputElm.val()).toBe('10');
2644+
expect($rootScope.value).toBe(10);
2645+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2646+
2647+
$rootScope.$apply('value = 12');
2648+
expect(inputElm).toBeInvalid();
2649+
expect(inputElm.val()).toBe('12');
2650+
expect($rootScope.value).toBe(12);
2651+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2652+
});
2653+
2654+
it('should validate even if the step value changes on-the-fly', function() {
2655+
$rootScope.step = 10;
2656+
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" step="{{step}}" />');
2657+
2658+
helper.changeInputValueTo('10');
2659+
expect(inputElm).toBeValid();
2660+
expect($rootScope.value).toBe(10);
2661+
2662+
// Step changes, but value matches
2663+
$rootScope.$apply('step = 5');
2664+
expect(inputElm.val()).toBe('10');
2665+
expect(inputElm).toBeValid();
2666+
expect($rootScope.value).toBe(10);
2667+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2668+
2669+
// Step changes, value does not match
2670+
$rootScope.$apply('step = 6');
2671+
expect(inputElm).toBeInvalid();
2672+
expect($rootScope.value).toBeUndefined();
2673+
expect(inputElm.val()).toBe('10');
2674+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2675+
2676+
// null = valid
2677+
$rootScope.$apply('step = null');
2678+
expect(inputElm).toBeValid();
2679+
expect($rootScope.value).toBe(10);
2680+
expect(inputElm.val()).toBe('10');
2681+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2682+
2683+
// Step val as string
2684+
$rootScope.$apply('step = "7"');
2685+
expect(inputElm).toBeInvalid();
2686+
expect($rootScope.value).toBeUndefined();
2687+
expect(inputElm.val()).toBe('10');
2688+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2689+
2690+
// unparsable string is ignored
2691+
$rootScope.$apply('step = "abc"');
2692+
expect(inputElm).toBeValid();
2693+
expect($rootScope.value).toBe(10);
2694+
expect(inputElm.val()).toBe('10');
2695+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2696+
});
2697+
});
2698+
2699+
2700+
describe('ngStep', function() {
2701+
it('should validate', function() {
2702+
$rootScope.step = 10;
2703+
$rootScope.value = 20;
2704+
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" ng-step="step" />');
2705+
2706+
expect(inputElm.val()).toBe('20');
2707+
expect(inputElm).toBeValid();
2708+
expect($rootScope.value).toBe(20);
2709+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2710+
2711+
helper.changeInputValueTo('18');
2712+
expect(inputElm).toBeInvalid();
2713+
expect(inputElm.val()).toBe('18');
2714+
expect($rootScope.value).toBeUndefined();
2715+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2716+
2717+
helper.changeInputValueTo('10');
2718+
expect(inputElm).toBeValid();
2719+
expect(inputElm.val()).toBe('10');
2720+
expect($rootScope.value).toBe(10);
2721+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2722+
2723+
$rootScope.$apply('value = 12');
2724+
expect(inputElm).toBeInvalid();
2725+
expect(inputElm.val()).toBe('12');
2726+
expect($rootScope.value).toBe(12);
2727+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2728+
});
2729+
2730+
it('should validate even if the step value changes on-the-fly', function() {
2731+
$rootScope.step = 10;
2732+
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" ng-step="step" />');
2733+
2734+
helper.changeInputValueTo('10');
2735+
expect(inputElm).toBeValid();
2736+
expect($rootScope.value).toBe(10);
2737+
2738+
// Step changes, but value matches
2739+
$rootScope.$apply('step = 5');
2740+
expect(inputElm.val()).toBe('10');
2741+
expect(inputElm).toBeValid();
2742+
expect($rootScope.value).toBe(10);
2743+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2744+
2745+
// Step changes, value does not match
2746+
$rootScope.$apply('step = 6');
2747+
expect(inputElm).toBeInvalid();
2748+
expect($rootScope.value).toBeUndefined();
2749+
expect(inputElm.val()).toBe('10');
2750+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2751+
2752+
// null = valid
2753+
$rootScope.$apply('step = null');
2754+
expect(inputElm).toBeValid();
2755+
expect($rootScope.value).toBe(10);
2756+
expect(inputElm.val()).toBe('10');
2757+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2758+
2759+
// Step val as string
2760+
$rootScope.$apply('step = "7"');
2761+
expect(inputElm).toBeInvalid();
2762+
expect($rootScope.value).toBeUndefined();
2763+
expect(inputElm.val()).toBe('10');
2764+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2765+
2766+
// unparsable string is ignored
2767+
$rootScope.$apply('step = "abc"');
2768+
expect(inputElm).toBeValid();
2769+
expect($rootScope.value).toBe(10);
2770+
expect(inputElm.val()).toBe('10');
2771+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2772+
});
2773+
});
2774+
26242775

26252776
describe('required', function() {
26262777

0 commit comments

Comments
 (0)