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

Commit fb2d1ab

Browse files
committed
revert: revert: feat(input[range]): support step
This reverts commit 5b633d8.
1 parent fe29638 commit fb2d1ab

File tree

2 files changed

+212
-27
lines changed

2 files changed

+212
-27
lines changed

src/ng/directive/input.js

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,13 +1037,19 @@ var inputType = {
10371037
* The model for the range input must always be a `Number`.
10381038
*
10391039
* IE9 and other browsers that do not support the `range` type fall back
1040-
* to a text input. Model binding, validation and number parsing are nevertheless supported.
1040+
* to a text input without any default values for `min`, `max` and `step`. Model binding,
1041+
* validation and number parsing are nevertheless supported.
10411042
*
10421043
* Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]`
10431044
* in a way that never allows the input to hold an invalid value. That means:
10441045
* - any non-numerical value is set to `(max + min) / 2`.
10451046
* - any numerical value that is less than the current min val, or greater than the current max val
10461047
* is set to the min / max val respectively.
1048+
* - additionally, the current `step` is respected, so the nearest value that satisfies a step
1049+
* is used.
1050+
*
1051+
* See the [HTML Spec on input[type=range]](https://www.w3.org/TR/html5/forms.html#range-state-(type=range))
1052+
* for more info.
10471053
*
10481054
* This has the following consequences for Angular:
10491055
*
@@ -1056,23 +1062,30 @@ var inputType = {
10561062
* That means the model for range will immediately be set to `50` after `ngModel` has been
10571063
* initialized. It also means a range input can never have the required error.
10581064
*
1059-
* This does not only affect changes to the model value, but also to the values of the `min` and
1060-
* `max` attributes. When these change in a way that will cause the browser to modify the input value,
1061-
* Angular will also update the model value.
1065+
* This does not only affect changes to the model value, but also to the values of the `min`,
1066+
* `max`, and `step` attributes. When these change in a way that will cause the browser to modify
1067+
* the input value, Angular will also update the model value.
10621068
*
10631069
* Automatic value adjustment also means that a range input element can never have the `required`,
10641070
* `min`, or `max` errors.
10651071
*
1066-
* Note that `input[range]` is not compatible with`ngMax` and `ngMin`, because they do not set the
1067-
* `min` and `max` attributes, which means that the browser won't automatically adjust the input
1068-
* value based on their values, and will always assume min = 0 and max = 100.
1072+
* However, `step` is currently only fully implemented by Firefox. Other browsers have problems
1073+
* when the step value changes dynamically - they do not adjust the element value correctly, but
1074+
* instead may set the `stepMismatch` error. If that's the case, the Angular will set the `step`
1075+
* error on the input, and set the model to `undefined`.
1076+
*
1077+
* Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do
1078+
* not set the `min` and `max` attributes, which means that the browser won't automatically adjust
1079+
* the input value based on their values, and will always assume min = 0, max = 100, and step = 1.
10691080
*
10701081
* @param {string} ngModel Assignable angular expression to data-bind to.
10711082
* @param {string=} name Property name of the form under which the control is published.
10721083
* @param {string=} min Sets the `min` validation to ensure that the value entered is greater
10731084
* than `min`. Can be interpolated.
10741085
* @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`.
10751086
* Can be interpolated.
1087+
* @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step`
1088+
* Can be interpolated.
10761089
* @param {string=} ngChange Angular expression to be executed when the ngModel value changes due
10771090
* to user interaction with the input element.
10781091
*
@@ -1499,6 +1512,13 @@ function numberFormatterParser(ctrl) {
14991512
});
15001513
}
15011514

1515+
function parseNumberAttrVal(val) {
1516+
if (isDefined(val) && !isNumber(val)) {
1517+
val = parseFloat(val);
1518+
}
1519+
return !isNumberNaN(val) ? val : undefined;
1520+
}
1521+
15021522
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15031523
badInputChecker(scope, element, attr, ctrl);
15041524
numberFormatterParser(ctrl);
@@ -1511,10 +1531,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15111531
};
15121532

15131533
attr.$observe('min', function(val) {
1514-
if (isDefined(val) && !isNumber(val)) {
1515-
val = parseFloat(val);
1516-
}
1517-
minVal = isNumber(val) && !isNaN(val) ? val : undefined;
1534+
minVal = parseNumberAttrVal(val);
15181535
// TODO(matsko): implement validateLater to reduce number of validations
15191536
ctrl.$validate();
15201537
});
@@ -1527,10 +1544,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15271544
};
15281545

15291546
attr.$observe('max', function(val) {
1530-
if (isDefined(val) && !isNumber(val)) {
1531-
val = parseFloat(val);
1532-
}
1533-
maxVal = isNumber(val) && !isNaN(val) ? val : undefined;
1547+
maxVal = parseNumberAttrVal(val);
15341548
// TODO(matsko): implement validateLater to reduce number of validations
15351549
ctrl.$validate();
15361550
});
@@ -1545,9 +1559,11 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15451559
var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range',
15461560
minVal = supportsRange ? 0 : undefined,
15471561
maxVal = supportsRange ? 100 : undefined,
1562+
stepVal = supportsRange ? 1 : undefined,
15481563
validity = element[0].validity,
15491564
hasMinAttr = isDefined(attr.min),
1550-
hasMaxAttr = isDefined(attr.max);
1565+
hasMaxAttr = isDefined(attr.max),
1566+
hasStepAttr = isDefined(attr.step);
15511567

15521568
var originalRender = ctrl.$render;
15531569

@@ -1564,7 +1580,7 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15641580
ctrl.$validators.min = supportsRange ?
15651581
// Since all browsers set the input to a valid value, we don't need to check validity
15661582
function noopMinValidator() { return true; } :
1567-
// non-support browsers validate the range
1583+
// non-support browsers validate the min val
15681584
function minValidator(modelValue, viewValue) {
15691585
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
15701586
};
@@ -1576,28 +1592,40 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15761592
ctrl.$validators.max = supportsRange ?
15771593
// Since all browsers set the input to a valid value, we don't need to check validity
15781594
function noopMaxValidator() { return true; } :
1579-
// ngMax doesn't set the max attr, so the browser doesn't adjust the input value as setting max would
1595+
// non-support browsers validate the max val
15801596
function maxValidator(modelValue, viewValue) {
15811597
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
15821598
};
15831599

15841600
setInitialValueAndObserver('max', maxChange);
15851601
}
15861602

1603+
if (hasStepAttr) {
1604+
ctrl.$validators.step = supportsRange ?
1605+
function nativeStepValidator() {
1606+
// Currently, only FF implements the spec on step change correctly (i.e. adjusting the
1607+
// input element value to a valid value). It's possible that other browsers set the stepMismatch
1608+
// validity error instead, so we can at least report an error in that case.
1609+
return !validity.stepMismatch;
1610+
} :
1611+
// ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would
1612+
function stepValidator(modelValue, viewValue) {
1613+
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0;
1614+
};
1615+
1616+
setInitialValueAndObserver('step', stepChange);
1617+
}
1618+
15871619
function setInitialValueAndObserver(htmlAttrName, changeFn) {
15881620
// interpolated attributes set the attribute value only after a digest, but we need the
15891621
// attribute value when the input is first rendered, so that the browser can adjust the
15901622
// input value based on the min/max value
15911623
element.attr(htmlAttrName, attr[htmlAttrName]);
1592-
15931624
attr.$observe(htmlAttrName, changeFn);
15941625
}
15951626

15961627
function minChange(val) {
1597-
if (isDefined(val) && !isNumber(val)) {
1598-
val = parseFloat(val);
1599-
}
1600-
minVal = isNumber(val) && !isNaN(val) ? val : undefined;
1628+
minVal = parseNumberAttrVal(val);
16011629
// ignore changes before model is initialized
16021630
if (isNumberNaN(ctrl.$modelValue)) {
16031631
return;
@@ -1618,10 +1646,7 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
16181646
}
16191647

16201648
function maxChange(val) {
1621-
if (isDefined(val) && !isNumber(val)) {
1622-
val = parseFloat(val);
1623-
}
1624-
maxVal = isNumber(val) && !isNaN(val) ? val : undefined;
1649+
maxVal = parseNumberAttrVal(val);
16251650
// ignore changes before model is initialized
16261651
if (isNumberNaN(ctrl.$modelValue)) {
16271652
return;
@@ -1642,6 +1667,21 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
16421667
}
16431668
}
16441669

1670+
function stepChange(val) {
1671+
stepVal = parseNumberAttrVal(val);
1672+
// ignore changes before model is initialized
1673+
if (isNumberNaN(ctrl.$modelValue)) {
1674+
return;
1675+
}
1676+
1677+
// Some browsers don't adjust the input value correctly, but set the stepMismatch error
1678+
if (supportsRange && ctrl.$viewValue !== element.val()) {
1679+
ctrl.$setViewValue(element.val());
1680+
} else {
1681+
// TODO(matsko): implement validateLater to reduce number of validations
1682+
ctrl.$validate();
1683+
}
1684+
}
16451685
}
16461686

16471687
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {

test/ng/directive/inputSpec.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3232,6 +3232,151 @@ describe('input', function() {
32323232

32333233
}
32343234

3235+
3236+
describe('step', function() {
3237+
3238+
if (supportsRange) {
3239+
// Browsers that implement range will never allow you to set a value that doesn't match the step value
3240+
// However, currently only Firefox fully inplements the spec when setting the value after the step value changes.
3241+
// Other browsers fail in various edge cases, which is why they are not tested here.
3242+
it('should round the input value to the nearest step on user input', function() {
3243+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" step="5" />');
3244+
3245+
helper.changeInputValueTo('5');
3246+
expect(inputElm).toBeValid();
3247+
expect(scope.value).toBe(5);
3248+
expect(scope.form.alias.$error.step).toBeFalsy();
3249+
3250+
helper.changeInputValueTo('10');
3251+
expect(inputElm).toBeValid();
3252+
expect(scope.value).toBe(10);
3253+
expect(scope.form.alias.$error.step).toBeFalsy();
3254+
3255+
helper.changeInputValueTo('9');
3256+
expect(inputElm).toBeValid();
3257+
expect(scope.value).toBe(10);
3258+
expect(scope.form.alias.$error.step).toBeFalsy();
3259+
3260+
helper.changeInputValueTo('7');
3261+
expect(inputElm).toBeValid();
3262+
expect(scope.value).toBe(5);
3263+
expect(scope.form.alias.$error.step).toBeFalsy();
3264+
3265+
helper.changeInputValueTo('7.5');
3266+
expect(inputElm).toBeValid();
3267+
expect(scope.value).toBe(10);
3268+
expect(scope.form.alias.$error.step).toBeFalsy();
3269+
});
3270+
3271+
it('should round the input value to the nearest step when setting the model', function() {
3272+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" step="5" />');
3273+
3274+
scope.$apply('value = 10');
3275+
expect(inputElm.val()).toBe('10');
3276+
expect(inputElm).toBeValid();
3277+
expect(scope.value).toBe(10);
3278+
expect(scope.form.alias.$error.step).toBeFalsy();
3279+
3280+
scope.$apply('value = 5');
3281+
expect(inputElm.val()).toBe('5');
3282+
expect(inputElm).toBeValid();
3283+
expect(scope.value).toBe(5);
3284+
expect(scope.form.alias.$error.step).toBeFalsy();
3285+
3286+
scope.$apply('value = 7.5');
3287+
expect(inputElm.val()).toBe('10');
3288+
expect(inputElm).toBeValid();
3289+
expect(scope.value).toBe(10);
3290+
expect(scope.form.alias.$error.step).toBeFalsy();
3291+
3292+
scope.$apply('value = 7');
3293+
expect(inputElm.val()).toBe('5');
3294+
expect(inputElm).toBeValid();
3295+
expect(scope.value).toBe(5);
3296+
expect(scope.form.alias.$error.step).toBeFalsy();
3297+
3298+
scope.$apply('value = 9');
3299+
expect(inputElm.val()).toBe('10');
3300+
expect(inputElm).toBeValid();
3301+
expect(scope.value).toBe(10);
3302+
expect(scope.form.alias.$error.step).toBeFalsy();
3303+
});
3304+
3305+
} else {
3306+
it('should validate if "range" is not implemented', function() {
3307+
scope.step = 10;
3308+
scope.value = 20;
3309+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" step="{{step}}" />');
3310+
3311+
expect(inputElm.val()).toBe('20');
3312+
expect(inputElm).toBeValid();
3313+
expect(scope.value).toBe(20);
3314+
expect(scope.form.alias.$error.step).toBeFalsy();
3315+
3316+
helper.changeInputValueTo('18');
3317+
expect(inputElm).toBeInvalid();
3318+
expect(inputElm.val()).toBe('18');
3319+
expect(scope.value).toBeUndefined();
3320+
expect(scope.form.alias.$error.step).toBeTruthy();
3321+
3322+
helper.changeInputValueTo('10');
3323+
expect(inputElm).toBeValid();
3324+
expect(inputElm.val()).toBe('10');
3325+
expect(scope.value).toBe(10);
3326+
expect(scope.form.alias.$error.step).toBeFalsy();
3327+
3328+
scope.$apply('value = 12');
3329+
expect(inputElm).toBeInvalid();
3330+
expect(inputElm.val()).toBe('12');
3331+
expect(scope.value).toBe(12);
3332+
expect(scope.form.alias.$error.step).toBeTruthy();
3333+
});
3334+
3335+
it('should validate even if the step value changes on-the-fly', function() {
3336+
scope.step = 10;
3337+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" step="{{step}}" />');
3338+
3339+
helper.changeInputValueTo('10');
3340+
expect(inputElm).toBeValid();
3341+
expect(scope.value).toBe(10);
3342+
3343+
// Step changes, but value matches
3344+
scope.$apply('step = 5');
3345+
expect(inputElm.val()).toBe('10');
3346+
expect(inputElm).toBeValid();
3347+
expect(scope.value).toBe(10);
3348+
expect(scope.form.alias.$error.step).toBeFalsy();
3349+
3350+
// Step changes, value does not match
3351+
scope.$apply('step = 6');
3352+
expect(inputElm).toBeInvalid();
3353+
expect(scope.value).toBeUndefined();
3354+
expect(inputElm.val()).toBe('10');
3355+
expect(scope.form.alias.$error.step).toBeTruthy();
3356+
3357+
// null = valid
3358+
scope.$apply('step = null');
3359+
expect(inputElm).toBeValid();
3360+
expect(scope.value).toBe(10);
3361+
expect(inputElm.val()).toBe('10');
3362+
expect(scope.form.alias.$error.step).toBeFalsy();
3363+
3364+
// Step val as string
3365+
scope.$apply('step = "7"');
3366+
expect(inputElm).toBeInvalid();
3367+
expect(scope.value).toBeUndefined();
3368+
expect(inputElm.val()).toBe('10');
3369+
expect(scope.form.alias.$error.step).toBeTruthy();
3370+
3371+
// unparsable string is ignored
3372+
scope.$apply('step = "abc"');
3373+
expect(inputElm).toBeValid();
3374+
expect(scope.value).toBe(10);
3375+
expect(inputElm.val()).toBe('10');
3376+
expect(scope.form.alias.$error.step).toBeFalsy();
3377+
});
3378+
}
3379+
});
32353380
});
32363381

32373382
describe('email', function() {

0 commit comments

Comments
 (0)