Skip to content

Commit 910878e

Browse files
Narretzpetebacondarwin
authored andcommitted
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 angular#10597
1 parent ce8002c commit 910878e

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
@@ -583,7 +583,8 @@ var ALIASED_ATTR = {
583583
'ngMaxlength': 'maxlength',
584584
'ngMin': 'min',
585585
'ngMax': 'max',
586-
'ngPattern': 'pattern'
586+
'ngPattern': 'pattern',
587+
'ngStep': 'step'
587588
};
588589

589590
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
@@ -2563,6 +2563,157 @@ describe('input', function() {
25632563
});
25642564
});
25652565

2566+
describe('step', function() {
2567+
it('should validate', function() {
2568+
$rootScope.step = 10;
2569+
$rootScope.value = 20;
2570+
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" step="{{step}}" />');
2571+
2572+
expect(inputElm.val()).toBe('20');
2573+
expect(inputElm).toBeValid();
2574+
expect($rootScope.value).toBe(20);
2575+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2576+
2577+
helper.changeInputValueTo('18');
2578+
expect(inputElm).toBeInvalid();
2579+
expect(inputElm.val()).toBe('18');
2580+
expect($rootScope.value).toBeUndefined();
2581+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2582+
2583+
helper.changeInputValueTo('10');
2584+
expect(inputElm).toBeValid();
2585+
expect(inputElm.val()).toBe('10');
2586+
expect($rootScope.value).toBe(10);
2587+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2588+
2589+
$rootScope.$apply('value = 12');
2590+
expect(inputElm).toBeInvalid();
2591+
expect(inputElm.val()).toBe('12');
2592+
expect($rootScope.value).toBe(12);
2593+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2594+
});
2595+
2596+
it('should validate even if the step value changes on-the-fly', function() {
2597+
$rootScope.step = 10;
2598+
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" step="{{step}}" />');
2599+
2600+
helper.changeInputValueTo('10');
2601+
expect(inputElm).toBeValid();
2602+
expect($rootScope.value).toBe(10);
2603+
2604+
// Step changes, but value matches
2605+
$rootScope.$apply('step = 5');
2606+
expect(inputElm.val()).toBe('10');
2607+
expect(inputElm).toBeValid();
2608+
expect($rootScope.value).toBe(10);
2609+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2610+
2611+
// Step changes, value does not match
2612+
$rootScope.$apply('step = 6');
2613+
expect(inputElm).toBeInvalid();
2614+
expect($rootScope.value).toBeUndefined();
2615+
expect(inputElm.val()).toBe('10');
2616+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2617+
2618+
// null = valid
2619+
$rootScope.$apply('step = null');
2620+
expect(inputElm).toBeValid();
2621+
expect($rootScope.value).toBe(10);
2622+
expect(inputElm.val()).toBe('10');
2623+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2624+
2625+
// Step val as string
2626+
$rootScope.$apply('step = "7"');
2627+
expect(inputElm).toBeInvalid();
2628+
expect($rootScope.value).toBeUndefined();
2629+
expect(inputElm.val()).toBe('10');
2630+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2631+
2632+
// unparsable string is ignored
2633+
$rootScope.$apply('step = "abc"');
2634+
expect(inputElm).toBeValid();
2635+
expect($rootScope.value).toBe(10);
2636+
expect(inputElm.val()).toBe('10');
2637+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2638+
});
2639+
});
2640+
2641+
2642+
describe('ngStep', function() {
2643+
it('should validate', function() {
2644+
$rootScope.step = 10;
2645+
$rootScope.value = 20;
2646+
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" ng-step="step" />');
2647+
2648+
expect(inputElm.val()).toBe('20');
2649+
expect(inputElm).toBeValid();
2650+
expect($rootScope.value).toBe(20);
2651+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2652+
2653+
helper.changeInputValueTo('18');
2654+
expect(inputElm).toBeInvalid();
2655+
expect(inputElm.val()).toBe('18');
2656+
expect($rootScope.value).toBeUndefined();
2657+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2658+
2659+
helper.changeInputValueTo('10');
2660+
expect(inputElm).toBeValid();
2661+
expect(inputElm.val()).toBe('10');
2662+
expect($rootScope.value).toBe(10);
2663+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2664+
2665+
$rootScope.$apply('value = 12');
2666+
expect(inputElm).toBeInvalid();
2667+
expect(inputElm.val()).toBe('12');
2668+
expect($rootScope.value).toBe(12);
2669+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2670+
});
2671+
2672+
it('should validate even if the step value changes on-the-fly', function() {
2673+
$rootScope.step = 10;
2674+
var inputElm = helper.compileInput('<input type="number" ng-model="value" name="alias" ng-step="step" />');
2675+
2676+
helper.changeInputValueTo('10');
2677+
expect(inputElm).toBeValid();
2678+
expect($rootScope.value).toBe(10);
2679+
2680+
// Step changes, but value matches
2681+
$rootScope.$apply('step = 5');
2682+
expect(inputElm.val()).toBe('10');
2683+
expect(inputElm).toBeValid();
2684+
expect($rootScope.value).toBe(10);
2685+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2686+
2687+
// Step changes, value does not match
2688+
$rootScope.$apply('step = 6');
2689+
expect(inputElm).toBeInvalid();
2690+
expect($rootScope.value).toBeUndefined();
2691+
expect(inputElm.val()).toBe('10');
2692+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2693+
2694+
// null = valid
2695+
$rootScope.$apply('step = null');
2696+
expect(inputElm).toBeValid();
2697+
expect($rootScope.value).toBe(10);
2698+
expect(inputElm.val()).toBe('10');
2699+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2700+
2701+
// Step val as string
2702+
$rootScope.$apply('step = "7"');
2703+
expect(inputElm).toBeInvalid();
2704+
expect($rootScope.value).toBeUndefined();
2705+
expect(inputElm.val()).toBe('10');
2706+
expect($rootScope.form.alias.$error.step).toBeTruthy();
2707+
2708+
// unparsable string is ignored
2709+
$rootScope.$apply('step = "abc"');
2710+
expect(inputElm).toBeValid();
2711+
expect($rootScope.value).toBe(10);
2712+
expect(inputElm.val()).toBe('10');
2713+
expect($rootScope.form.alias.$error.step).toBeFalsy();
2714+
});
2715+
});
2716+
25662717

25672718
describe('required', function() {
25682719

0 commit comments

Comments
 (0)