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

Commit 9130166

Browse files
authored
feat(input): add support for binding to input[type=range] (#14870)
Thanks to @cironunes for the initial implementation in #9715 Adds support for binding to input[range] with the following behavior / features: - Like input[number], it requires the model to be a Number, and will set the model to a Number - it supports setting the min/max values via the min/max and ngMin/ngMax attributes - it follows the browser behavior of never allowing an invalid value. That means, when the browser converts an invalid value (empty: null, undefined, false ..., out of bounds: greater than max, less than min) to a valid value, the input will in turn set the model to this new valid value via $setViewValue. -- this means a range input will never be required and never have a non-Number model value, once the ngModel directive is initialized. -- this behavior is supported when the model changes and when the min/max attributes change in a way that prompts the browser to update the input value. -- ngMin / ngMax do not prompt the browser to update the values, as they don't set the attribute values. Instead, they will set the min / max errors when appropriate - browsers that do not support input[range] (IE9) handle the input like a number input (with validation etc.) Closes #5892 Closes #9715 Close #14870
1 parent cd2f6d9 commit 9130166

File tree

4 files changed

+645
-6
lines changed

4 files changed

+645
-6
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

+218-4
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,113 @@ var inputType = {
10341034
*/
10351035
'radio': radioInputType,
10361036

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

10381145
/**
10391146
* @ngdoc input
@@ -1385,10 +1492,7 @@ function badInputChecker(scope, element, attr, ctrl) {
13851492
}
13861493
}
13871494

1388-
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1389-
badInputChecker(scope, element, attr, ctrl);
1390-
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
1391-
1495+
function numberFormatterParser(ctrl) {
13921496
ctrl.$$parserName = 'number';
13931497
ctrl.$parsers.push(function(value) {
13941498
if (ctrl.$isEmpty(value)) return null;
@@ -1405,6 +1509,12 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
14051509
}
14061510
return value;
14071511
});
1512+
}
1513+
1514+
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1515+
badInputChecker(scope, element, attr, ctrl);
1516+
numberFormatterParser(ctrl);
1517+
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
14081518

14091519
if (isDefined(attr.min) || attr.ngMin) {
14101520
var minVal;
@@ -1439,6 +1549,110 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
14391549
}
14401550
}
14411551

1552+
function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1553+
badInputChecker(scope, element, attr, ctrl);
1554+
numberFormatterParser(ctrl);
1555+
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
1556+
1557+
var minVal = 0,
1558+
maxVal = 100,
1559+
supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range',
1560+
validity = element[0].validity;
1561+
1562+
var originalRender = ctrl.$render;
1563+
1564+
ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ?
1565+
//Browsers that implement range will set these values automatically, but reading the adjusted values after
1566+
//$render would cause the min / max validators to be applied with the wrong value
1567+
function rangeRender() {
1568+
originalRender();
1569+
ctrl.$setViewValue(element.val());
1570+
} :
1571+
originalRender;
1572+
1573+
function minChange(val) {
1574+
if (isDefined(val) && !isNumber(val)) {
1575+
val = parseFloat(val);
1576+
}
1577+
minVal = isNumber(val) && !isNaN(val) ? val : undefined;
1578+
// ignore changes before model is initialized
1579+
if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
1580+
return;
1581+
}
1582+
1583+
if (supportsRange && minAttrType === 'min') {
1584+
var elVal = element.val();
1585+
// IE11 doesn't set the el val correctly if the minVal is greater than the element value
1586+
if (minVal > elVal) {
1587+
element.val(minVal);
1588+
elVal = minVal;
1589+
}
1590+
ctrl.$setViewValue(elVal);
1591+
} else {
1592+
// TODO(matsko): implement validateLater to reduce number of validations
1593+
ctrl.$validate();
1594+
}
1595+
}
1596+
1597+
var minAttrType = isDefined(attr.ngMin) ? 'ngMin' : isDefined(attr.min) ? 'min' : false;
1598+
if (minAttrType) {
1599+
ctrl.$validators.min = isDefined(attr.min) && supportsRange ?
1600+
function noopMinValidator(value) {
1601+
// Since all browsers set the input to a valid value, we don't need to check validity
1602+
return true;
1603+
} :
1604+
// ngMin doesn't set the min attr, so the browser doesn't adjust the input value as setting min would
1605+
function minValidator(modelValue, viewValue) {
1606+
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
1607+
};
1608+
1609+
// Assign minVal when the directive is linked. This won't run the validators as the model isn't ready yet
1610+
minChange(attr.min);
1611+
attr.$observe('min', minChange);
1612+
}
1613+
1614+
function maxChange(val) {
1615+
if (isDefined(val) && !isNumber(val)) {
1616+
val = parseFloat(val);
1617+
}
1618+
maxVal = isNumber(val) && !isNaN(val) ? val : undefined;
1619+
// ignore changes before model is initialized
1620+
if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
1621+
return;
1622+
}
1623+
1624+
if (supportsRange && maxAttrType === 'max') {
1625+
var elVal = element.val();
1626+
// IE11 doesn't set the el val correctly if the maxVal is less than the element value
1627+
if (maxVal < elVal) {
1628+
element.val(maxVal);
1629+
elVal = minVal;
1630+
}
1631+
ctrl.$setViewValue(elVal);
1632+
} else {
1633+
// TODO(matsko): implement validateLater to reduce number of validations
1634+
ctrl.$validate();
1635+
}
1636+
}
1637+
var maxAttrType = isDefined(attr.max) ? 'max' : attr.ngMax ? 'ngMax' : false;
1638+
if (maxAttrType) {
1639+
ctrl.$validators.max = isDefined(attr.max) && supportsRange ?
1640+
function noopMaxValidator() {
1641+
// Since all browsers set the input to a valid value, we don't need to check validity
1642+
return true;
1643+
} :
1644+
// ngMax doesn't set the max attr, so the browser doesn't adjust the input value as setting max would
1645+
function maxValidator(modelValue, viewValue) {
1646+
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
1647+
};
1648+
1649+
// Assign maxVal when the directive is linked. This won't run the validators as the model isn't ready yet
1650+
maxChange(attr.max);
1651+
attr.$observe('max', maxChange);
1652+
}
1653+
1654+
}
1655+
14421656
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
14431657
// Note: no badInputChecker here by purpose as `url` is only a validation
14441658
// 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
@@ -881,7 +881,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
881881
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
882882
ctrl.$render();
883883

884-
ctrl.$$runValidators(modelValue, viewValue, noop);
884+
// It is possible that model and view value have been updated during render
885+
ctrl.$$runValidators(ctrl.$modelValue, ctrl.$viewValue, noop);
885886
}
886887
}
887888

0 commit comments

Comments
 (0)