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

Commit 2e7121b

Browse files
gkalpakpetebacondarwin
authored andcommitted
feat(input): re-add support for binding to input[range]
This commit re-applies the related (previously reverted) commits. A follow-up commit will make the support opt-in in order to avoid a breaking change. Included commits: - 296da4b - `feat(input): add support for binding to input[type=range]` (previously reverted with 6a167e8) - b78539b - `fix(input[range]): correctly handle min/max; remove ngMin/ngMax support` (previously reverted with aa60491) - 90c08b8 - `feat(input[range]): support step` (previously reverted with 5b633d8)
1 parent da3f2df commit 2e7121b

File tree

4 files changed

+854
-14
lines changed

4 files changed

+854
-14
lines changed

docs/content/error/ngModel/numfmt.ngdoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
@fullName Model is not of type `number`
44
@description
55

6-
The number input directive `<input type="number">` requires the model to be a `number`.
6+
The `input[number]` and `input[range]` directives require the model to be a `number`.
77

88
If the model is something else, this error will be thrown.
99

src/ng/directive/input.js

+264-12
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,121 @@ var inputType = {
10271027
*/
10281028
'radio': radioInputType,
10291029

1030+
/**
1031+
* @ngdoc input
1032+
* @name input[range]
1033+
*
1034+
* @description
1035+
* Native range input with validation and transformation.
1036+
*
1037+
* The model for the range input must always be a `Number`.
1038+
*
1039+
* IE9 and other browsers that do not support the `range` type fall back
1040+
* to a text input without any default values for `min`, `max` and `step`. Model binding,
1041+
* validation and number parsing are nevertheless supported.
1042+
*
1043+
* Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]`
1044+
* in a way that never allows the input to hold an invalid value. That means:
1045+
* - any non-numerical value is set to `(max + min) / 2`.
1046+
* - any numerical value that is less than the current min val, or greater than the current max val
1047+
* 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.
1053+
*
1054+
* This has the following consequences for Angular:
1055+
*
1056+
* Since the element value should always reflect the current model value, a range input
1057+
* will set the bound ngModel expression to the value that the browser has set for the
1058+
* input element. For example, in the following input `<input type="range" ng-model="model.value">`,
1059+
* if the application sets `model.value = null`, the browser will set the input to `'50'`.
1060+
* Angular will then set the model to `50`, to prevent input and model value being out of sync.
1061+
*
1062+
* That means the model for range will immediately be set to `50` after `ngModel` has been
1063+
* initialized. It also means a range input can never have the required error.
1064+
*
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.
1068+
*
1069+
* Automatic value adjustment also means that a range input element can never have the `required`,
1070+
* `min`, or `max` errors.
1071+
*
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.
1080+
*
1081+
* @param {string} ngModel Assignable angular expression to data-bind to.
1082+
* @param {string=} name Property name of the form under which the control is published.
1083+
* @param {string=} min Sets the `min` validation to ensure that the value entered is greater
1084+
* than `min`. Can be interpolated.
1085+
* @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`.
1086+
* Can be interpolated.
1087+
* @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step`
1088+
* Can be interpolated.
1089+
* @param {string=} ngChange Angular expression to be executed when the ngModel value changes due
1090+
* to user interaction with the input element.
1091+
*
1092+
* @example
1093+
<example name="range-input-directive" module="rangeExample">
1094+
<file name="index.html">
1095+
<script>
1096+
angular.module('rangeExample', [])
1097+
.controller('ExampleController', ['$scope', function($scope) {
1098+
$scope.value = 75;
1099+
$scope.min = 10;
1100+
$scope.max = 90;
1101+
}]);
1102+
</script>
1103+
<form name="myForm" ng-controller="ExampleController">
1104+
1105+
Model as range: <input type="range" name="range" ng-model="value" min="{{min}}" max="{{max}}">
1106+
<hr>
1107+
Model as number: <input type="number" ng-model="value"><br>
1108+
Min: <input type="number" ng-model="min"><br>
1109+
Max: <input type="number" ng-model="max"><br>
1110+
value = <code>{{value}}</code><br/>
1111+
myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/>
1112+
myForm.range.$error = <code>{{myForm.range.$error}}</code>
1113+
</form>
1114+
</file>
1115+
</example>
1116+
1117+
* ## Range Input with ngMin & ngMax attributes
1118+
1119+
* @example
1120+
<example name="range-input-directive-ng" module="rangeExample">
1121+
<file name="index.html">
1122+
<script>
1123+
angular.module('rangeExample', [])
1124+
.controller('ExampleController', ['$scope', function($scope) {
1125+
$scope.value = 75;
1126+
$scope.min = 10;
1127+
$scope.max = 90;
1128+
}]);
1129+
</script>
1130+
<form name="myForm" ng-controller="ExampleController">
1131+
Model as range: <input type="range" name="range" ng-model="value" ng-min="min" ng-max="max">
1132+
<hr>
1133+
Model as number: <input type="number" ng-model="value"><br>
1134+
Min: <input type="number" ng-model="min"><br>
1135+
Max: <input type="number" ng-model="max"><br>
1136+
value = <code>{{value}}</code><br/>
1137+
myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/>
1138+
myForm.range.$error = <code>{{myForm.range.$error}}</code>
1139+
</form>
1140+
</file>
1141+
</example>
1142+
1143+
*/
1144+
'range': rangeInputType,
10301145

10311146
/**
10321147
* @ngdoc input
@@ -1378,10 +1493,7 @@ function badInputChecker(scope, element, attr, ctrl) {
13781493
}
13791494
}
13801495

1381-
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1382-
badInputChecker(scope, element, attr, ctrl);
1383-
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
1384-
1496+
function numberFormatterParser(ctrl) {
13851497
ctrl.$$parserName = 'number';
13861498
ctrl.$parsers.push(function(value) {
13871499
if (ctrl.$isEmpty(value)) return null;
@@ -1398,6 +1510,19 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
13981510
}
13991511
return value;
14001512
});
1513+
}
1514+
1515+
function parseNumberAttrVal(val) {
1516+
if (isDefined(val) && !isNumber(val)) {
1517+
val = parseFloat(val);
1518+
}
1519+
return !isNumberNaN(val) ? val : undefined;
1520+
}
1521+
1522+
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1523+
badInputChecker(scope, element, attr, ctrl);
1524+
numberFormatterParser(ctrl);
1525+
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
14011526

14021527
if (isDefined(attr.min) || attr.ngMin) {
14031528
var minVal;
@@ -1406,10 +1531,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
14061531
};
14071532

14081533
attr.$observe('min', function(val) {
1409-
if (isDefined(val) && !isNumber(val)) {
1410-
val = parseFloat(val);
1411-
}
1412-
minVal = isNumber(val) && !isNaN(val) ? val : undefined;
1534+
minVal = parseNumberAttrVal(val);
14131535
// TODO(matsko): implement validateLater to reduce number of validations
14141536
ctrl.$validate();
14151537
});
@@ -1422,16 +1544,146 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
14221544
};
14231545

14241546
attr.$observe('max', function(val) {
1425-
if (isDefined(val) && !isNumber(val)) {
1426-
val = parseFloat(val);
1427-
}
1428-
maxVal = isNumber(val) && !isNaN(val) ? val : undefined;
1547+
maxVal = parseNumberAttrVal(val);
14291548
// TODO(matsko): implement validateLater to reduce number of validations
14301549
ctrl.$validate();
14311550
});
14321551
}
14331552
}
14341553

1554+
function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1555+
badInputChecker(scope, element, attr, ctrl);
1556+
numberFormatterParser(ctrl);
1557+
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
1558+
1559+
var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range',
1560+
minVal = supportsRange ? 0 : undefined,
1561+
maxVal = supportsRange ? 100 : undefined,
1562+
stepVal = supportsRange ? 1 : undefined,
1563+
validity = element[0].validity,
1564+
hasMinAttr = isDefined(attr.min),
1565+
hasMaxAttr = isDefined(attr.max),
1566+
hasStepAttr = isDefined(attr.step);
1567+
1568+
var originalRender = ctrl.$render;
1569+
1570+
ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ?
1571+
//Browsers that implement range will set these values automatically, but reading the adjusted values after
1572+
//$render would cause the min / max validators to be applied with the wrong value
1573+
function rangeRender() {
1574+
originalRender();
1575+
ctrl.$setViewValue(element.val());
1576+
} :
1577+
originalRender;
1578+
1579+
if (hasMinAttr) {
1580+
ctrl.$validators.min = supportsRange ?
1581+
// Since all browsers set the input to a valid value, we don't need to check validity
1582+
function noopMinValidator() { return true; } :
1583+
// non-support browsers validate the min val
1584+
function minValidator(modelValue, viewValue) {
1585+
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
1586+
};
1587+
1588+
setInitialValueAndObserver('min', minChange);
1589+
}
1590+
1591+
if (hasMaxAttr) {
1592+
ctrl.$validators.max = supportsRange ?
1593+
// Since all browsers set the input to a valid value, we don't need to check validity
1594+
function noopMaxValidator() { return true; } :
1595+
// non-support browsers validate the max val
1596+
function maxValidator(modelValue, viewValue) {
1597+
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
1598+
};
1599+
1600+
setInitialValueAndObserver('max', maxChange);
1601+
}
1602+
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+
1619+
function setInitialValueAndObserver(htmlAttrName, changeFn) {
1620+
// interpolated attributes set the attribute value only after a digest, but we need the
1621+
// attribute value when the input is first rendered, so that the browser can adjust the
1622+
// input value based on the min/max value
1623+
element.attr(htmlAttrName, attr[htmlAttrName]);
1624+
attr.$observe(htmlAttrName, changeFn);
1625+
}
1626+
1627+
function minChange(val) {
1628+
minVal = parseNumberAttrVal(val);
1629+
// ignore changes before model is initialized
1630+
if (isNumberNaN(ctrl.$modelValue)) {
1631+
return;
1632+
}
1633+
1634+
if (supportsRange) {
1635+
var elVal = element.val();
1636+
// IE11 doesn't set the el val correctly if the minVal is greater than the element value
1637+
if (minVal > elVal) {
1638+
elVal = minVal;
1639+
element.val(elVal);
1640+
}
1641+
ctrl.$setViewValue(elVal);
1642+
} else {
1643+
// TODO(matsko): implement validateLater to reduce number of validations
1644+
ctrl.$validate();
1645+
}
1646+
}
1647+
1648+
function maxChange(val) {
1649+
maxVal = parseNumberAttrVal(val);
1650+
// ignore changes before model is initialized
1651+
if (isNumberNaN(ctrl.$modelValue)) {
1652+
return;
1653+
}
1654+
1655+
if (supportsRange) {
1656+
var elVal = element.val();
1657+
// IE11 doesn't set the el val correctly if the maxVal is less than the element value
1658+
if (maxVal < elVal) {
1659+
element.val(maxVal);
1660+
// IE11 and Chrome don't set the value to the minVal when max < min
1661+
elVal = maxVal < minVal ? minVal : maxVal;
1662+
}
1663+
ctrl.$setViewValue(elVal);
1664+
} else {
1665+
// TODO(matsko): implement validateLater to reduce number of validations
1666+
ctrl.$validate();
1667+
}
1668+
}
1669+
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+
}
1685+
}
1686+
14351687
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
14361688
// Note: no badInputChecker here by purpose as `url` is only a validation
14371689
// in browsers, i.e. we can always read out input.value even if it is not valid!

src/ng/directive/ngModel.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
883883
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
884884
ctrl.$render();
885885

886-
ctrl.$$runValidators(modelValue, viewValue, noop);
886+
// It is possible that model and view value have been updated during render
887+
ctrl.$$runValidators(ctrl.$modelValue, ctrl.$viewValue, noop);
887888
}
888889
}
889890

0 commit comments

Comments
 (0)