Permalink
Comparing changes
Open a pull request
- 3 commits
- 5 files changed
- 0 commit comments
- 1 contributor
Commits on Oct 10, 2016
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)
Unified
Split
Showing
with
911 additions
and 17 deletions.
- +3 −2 docs/content/error/ngModel/numfmt.ngdoc
- +293 −13 src/ng/directive/input.js
- +2 −1 src/ng/directive/ngModel.js
- +612 −0 test/ng/directive/inputSpec.js
- +1 −1 test/ng/directive/validatorsSpec.js
| @@ -3,7 +3,8 @@ | ||
| @fullName Model is not of type `number` | ||
| @description | ||
|
|
||
| The number input directive `<input type="number">` requires the model to be a `number`. | ||
| The `input[type="number"]` and `input[type="range"][ng-input-range]` directives require the model to | ||
| be a `number`. | ||
|
|
||
| If the model is something else, this error will be thrown. | ||
|
|
||
| @@ -17,7 +18,7 @@ pipeline. | ||
| ## Example | ||
|
|
||
| In this example, our model stores the number as a string, so we provide the `stringToNumber` | ||
| directive to convert it into the format the `input[number]` directive expects. | ||
| directive to convert it into the format the `input[type="number"]` directive expects. | ||
|
|
||
|
|
||
| <example module="numfmt-error-module" name="number-format-error"> | ||
| @@ -1027,6 +1027,145 @@ var inputType = { | ||
| */ | ||
| 'radio': radioInputType, | ||
|
|
||
| /** | ||
| * @ngdoc input | ||
| * @name input[range] | ||
| * | ||
| * @description | ||
| * Native range input with validation and transformation. | ||
| * | ||
| * <div class="alert alert-warning"> | ||
| * <p> | ||
| * In v1.5.9+, in order to avoid interfering with already existing, custom directives for | ||
| * `input[range]`, you need to let Angular know that you want to enable its built-in support. | ||
| * You can do this by adding the `ng-input-range` attribute to the input element. E.g.: | ||
| * `<input type="range" ng-input-range ... />` | ||
| * </p><br /> | ||
| * <p> | ||
| * Input elements without the `ng-input-range` attibute will continue to be treated the same | ||
| * as in previous versions (e.g. their model value will be a string not a number and Angular | ||
| * will not take `min`/`max`/`step` attributes and properties into account). | ||
| * </p><br /> | ||
| * <p> | ||
| * **Note:** From v1.6.x onwards, the support for `input[range]` will be always enabled and | ||
| * the `ng-input-range` attribute will have no effect. | ||
| * </p><br /> | ||
| * <p> | ||
| * This documentation page refers to elements which have the built-in support enabled; i.e. | ||
| * elements _with_ the `ng-input-range` attribute. | ||
| * </p> | ||
| * </div> | ||
| * | ||
| * The model for the range input must always be a `Number`. | ||
| * | ||
| * IE9 and other browsers that do not support the `range` type fall back | ||
| * to a text input without any default values for `min`, `max` and `step`. Model binding, | ||
| * validation and number parsing are nevertheless supported. | ||
| * | ||
| * Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]` | ||
| * in a way that never allows the input to hold an invalid value. That means: | ||
| * - any non-numerical value is set to `(max + min) / 2`. | ||
| * - any numerical value that is less than the current min val, or greater than the current max val | ||
| * is set to the min / max val respectively. | ||
| * - additionally, the current `step` is respected, so the nearest value that satisfies a step | ||
| * is used. | ||
| * | ||
| * See the [HTML Spec on input[type=range]](https://www.w3.org/TR/html5/forms.html#range-state-(type=range)) | ||
| * for more info. | ||
| * | ||
| * This has the following consequences for Angular: | ||
| * | ||
| * Since the element value should always reflect the current model value, a range input | ||
| * will set the bound ngModel expression to the value that the browser has set for the | ||
| * input element. For example, in the following input `<input type="range" ng-input-range ng-model="model.value">`, | ||
| * if the application sets `model.value = null`, the browser will set the input to `'50'`. | ||
| * Angular will then set the model to `50`, to prevent input and model value being out of sync. | ||
| * | ||
| * That means the model for range will immediately be set to `50` after `ngModel` has been | ||
| * initialized. It also means a range input can never have the required error. | ||
| * | ||
| * This does not only affect changes to the model value, but also to the values of the `min`, | ||
| * `max`, and `step` attributes. When these change in a way that will cause the browser to modify | ||
| * the input value, Angular will also update the model value. | ||
| * | ||
| * Automatic value adjustment also means that a range input element can never have the `required`, | ||
| * `min`, or `max` errors. | ||
| * | ||
| * However, `step` is currently only fully implemented by Firefox. Other browsers have problems | ||
| * when the step value changes dynamically - they do not adjust the element value correctly, but | ||
| * instead may set the `stepMismatch` error. If that's the case, the Angular will set the `step` | ||
| * error on the input, and set the model to `undefined`. | ||
| * | ||
| * Note that `input[range]` is not compatible with `ngMax`, `ngMin`, and `ngStep`, because they do | ||
| * not set the `min` and `max` attributes, which means that the browser won't automatically adjust | ||
| * the input value based on their values, and will always assume min = 0, max = 100, and step = 1. | ||
| * | ||
| * @param ngInputRange The presense of this attribute enables the built-in support for | ||
| * `input[range]`. | ||
| * @param {string} ngModel Assignable angular expression to data-bind to. | ||
| * @param {string=} name Property name of the form under which the control is published. | ||
| * @param {string=} min Sets the `min` validation to ensure that the value entered is greater | ||
| * than `min`. Can be interpolated. | ||
| * @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`. | ||
| * Can be interpolated. | ||
| * @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step` | ||
| * Can be interpolated. | ||
| * @param {string=} ngChange Angular expression to be executed when the ngModel value changes due | ||
| * to user interaction with the input element. | ||
| * | ||
| * @example | ||
| <example name="range-input-directive" module="rangeExample"> | ||
| <file name="index.html"> | ||
| <script> | ||
| angular.module('rangeExample', []) | ||
| .controller('ExampleController', ['$scope', function($scope) { | ||
| $scope.value = 75; | ||
| $scope.min = 10; | ||
| $scope.max = 90; | ||
| }]); | ||
| </script> | ||
| <form name="myForm" ng-controller="ExampleController"> | ||
| Model as range: <input type="range" ng-input-range name="range" ng-model="value" min="{{min}}" max="{{max}}"> | ||
| <hr> | ||
| Model as number: <input type="number" ng-model="value"><br> | ||
| Min: <input type="number" ng-model="min"><br> | ||
| Max: <input type="number" ng-model="max"><br> | ||
| value = <code>{{value}}</code><br/> | ||
| myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/> | ||
| myForm.range.$error = <code>{{myForm.range.$error}}</code> | ||
| </form> | ||
| </file> | ||
| </example> | ||
| * ## Range Input with ngMin & ngMax attributes | ||
| * @example | ||
| <example name="range-input-directive-ng" module="rangeExample"> | ||
| <file name="index.html"> | ||
| <script> | ||
| angular.module('rangeExample', []) | ||
| .controller('ExampleController', ['$scope', function($scope) { | ||
| $scope.value = 75; | ||
| $scope.min = 10; | ||
| $scope.max = 90; | ||
| }]); | ||
| </script> | ||
| <form name="myForm" ng-controller="ExampleController"> | ||
| Model as range: <input type="range" ng-input-range name="range" ng-model="value" ng-min="min" ng-max="max"> | ||
| <hr> | ||
| Model as number: <input type="number" ng-model="value"><br> | ||
| Min: <input type="number" ng-model="min"><br> | ||
| Max: <input type="number" ng-model="max"><br> | ||
| value = <code>{{value}}</code><br/> | ||
| myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/> | ||
| myForm.range.$error = <code>{{myForm.range.$error}}</code> | ||
| </form> | ||
| </file> | ||
| </example> | ||
| */ | ||
| 'range': rangeInputType, | ||
|
|
||
| /** | ||
| * @ngdoc input | ||
| @@ -1378,10 +1517,7 @@ function badInputChecker(scope, element, attr, ctrl) { | ||
| } | ||
| } | ||
|
|
||
| function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
| badInputChecker(scope, element, attr, ctrl); | ||
| baseInputType(scope, element, attr, ctrl, $sniffer, $browser); | ||
|
|
||
| function numberFormatterParser(ctrl) { | ||
| ctrl.$$parserName = 'number'; | ||
| ctrl.$parsers.push(function(value) { | ||
| if (ctrl.$isEmpty(value)) return null; | ||
| @@ -1398,6 +1534,19 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
| } | ||
| return value; | ||
| }); | ||
| } | ||
|
|
||
| function parseNumberAttrVal(val) { | ||
| if (isDefined(val) && !isNumber(val)) { | ||
| val = parseFloat(val); | ||
| } | ||
| return !isNumberNaN(val) ? val : undefined; | ||
| } | ||
|
|
||
| function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
| badInputChecker(scope, element, attr, ctrl); | ||
| baseInputType(scope, element, attr, ctrl, $sniffer, $browser); | ||
| numberFormatterParser(ctrl); | ||
|
|
||
| if (isDefined(attr.min) || attr.ngMin) { | ||
| var minVal; | ||
| @@ -1406,10 +1555,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
| }; | ||
|
|
||
| attr.$observe('min', function(val) { | ||
| if (isDefined(val) && !isNumber(val)) { | ||
| val = parseFloat(val); | ||
| } | ||
| minVal = isNumber(val) && !isNaN(val) ? val : undefined; | ||
| minVal = parseNumberAttrVal(val); | ||
| // TODO(matsko): implement validateLater to reduce number of validations | ||
| ctrl.$validate(); | ||
| }); | ||
| @@ -1422,16 +1568,146 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
| }; | ||
|
|
||
| attr.$observe('max', function(val) { | ||
| if (isDefined(val) && !isNumber(val)) { | ||
| val = parseFloat(val); | ||
| } | ||
| maxVal = isNumber(val) && !isNaN(val) ? val : undefined; | ||
| maxVal = parseNumberAttrVal(val); | ||
| // TODO(matsko): implement validateLater to reduce number of validations | ||
| ctrl.$validate(); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
| badInputChecker(scope, element, attr, ctrl); | ||
| numberFormatterParser(ctrl); | ||
| baseInputType(scope, element, attr, ctrl, $sniffer, $browser); | ||
|
|
||
| var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range', | ||
| minVal = supportsRange ? 0 : undefined, | ||
| maxVal = supportsRange ? 100 : undefined, | ||
| stepVal = supportsRange ? 1 : undefined, | ||
| validity = element[0].validity, | ||
| hasMinAttr = isDefined(attr.min), | ||
| hasMaxAttr = isDefined(attr.max), | ||
| hasStepAttr = isDefined(attr.step); | ||
|
|
||
| var originalRender = ctrl.$render; | ||
|
|
||
| ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ? | ||
| //Browsers that implement range will set these values automatically, but reading the adjusted values after | ||
| //$render would cause the min / max validators to be applied with the wrong value | ||
| function rangeRender() { | ||
| originalRender(); | ||
| ctrl.$setViewValue(element.val()); | ||
| } : | ||
| originalRender; | ||
|
|
||
| if (hasMinAttr) { | ||
| ctrl.$validators.min = supportsRange ? | ||
| // Since all browsers set the input to a valid value, we don't need to check validity | ||
| function noopMinValidator() { return true; } : | ||
| // non-support browsers validate the min val | ||
| function minValidator(modelValue, viewValue) { | ||
| return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal; | ||
| }; | ||
|
|
||
| setInitialValueAndObserver('min', minChange); | ||
| } | ||
|
|
||
| if (hasMaxAttr) { | ||
| ctrl.$validators.max = supportsRange ? | ||
| // Since all browsers set the input to a valid value, we don't need to check validity | ||
| function noopMaxValidator() { return true; } : | ||
| // non-support browsers validate the max val | ||
| function maxValidator(modelValue, viewValue) { | ||
| return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal; | ||
| }; | ||
|
|
||
| setInitialValueAndObserver('max', maxChange); | ||
| } | ||
|
|
||
| if (hasStepAttr) { | ||
| ctrl.$validators.step = supportsRange ? | ||
| function nativeStepValidator() { | ||
| // Currently, only FF implements the spec on step change correctly (i.e. adjusting the | ||
| // input element value to a valid value). It's possible that other browsers set the stepMismatch | ||
| // validity error instead, so we can at least report an error in that case. | ||
| return !validity.stepMismatch; | ||
| } : | ||
| // ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would | ||
| function stepValidator(modelValue, viewValue) { | ||
| return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0; | ||
| }; | ||
|
|
||
| setInitialValueAndObserver('step', stepChange); | ||
| } | ||
|
|
||
| function setInitialValueAndObserver(htmlAttrName, changeFn) { | ||
| // interpolated attributes set the attribute value only after a digest, but we need the | ||
| // attribute value when the input is first rendered, so that the browser can adjust the | ||
| // input value based on the min/max value | ||
| element.attr(htmlAttrName, attr[htmlAttrName]); | ||
| attr.$observe(htmlAttrName, changeFn); | ||
| } | ||
|
|
||
| function minChange(val) { | ||
| minVal = parseNumberAttrVal(val); | ||
| // ignore changes before model is initialized | ||
| if (isNumberNaN(ctrl.$modelValue)) { | ||
| return; | ||
| } | ||
|
|
||
| if (supportsRange) { | ||
| var elVal = element.val(); | ||
| // IE11 doesn't set the el val correctly if the minVal is greater than the element value | ||
| if (minVal > elVal) { | ||
| elVal = minVal; | ||
| element.val(elVal); | ||
| } | ||
| ctrl.$setViewValue(elVal); | ||
| } else { | ||
| // TODO(matsko): implement validateLater to reduce number of validations | ||
| ctrl.$validate(); | ||
| } | ||
| } | ||
|
|
||
| function maxChange(val) { | ||
| maxVal = parseNumberAttrVal(val); | ||
| // ignore changes before model is initialized | ||
| if (isNumberNaN(ctrl.$modelValue)) { | ||
| return; | ||
| } | ||
|
|
||
| if (supportsRange) { | ||
| var elVal = element.val(); | ||
| // IE11 doesn't set the el val correctly if the maxVal is less than the element value | ||
| if (maxVal < elVal) { | ||
| element.val(maxVal); | ||
| // IE11 and Chrome don't set the value to the minVal when max < min | ||
| elVal = maxVal < minVal ? minVal : maxVal; | ||
| } | ||
| ctrl.$setViewValue(elVal); | ||
| } else { | ||
| // TODO(matsko): implement validateLater to reduce number of validations | ||
| ctrl.$validate(); | ||
| } | ||
| } | ||
|
|
||
| function stepChange(val) { | ||
| stepVal = parseNumberAttrVal(val); | ||
| // ignore changes before model is initialized | ||
| if (isNumberNaN(ctrl.$modelValue)) { | ||
| return; | ||
| } | ||
|
|
||
| // Some browsers don't adjust the input value correctly, but set the stepMismatch error | ||
| if (supportsRange && ctrl.$viewValue !== element.val()) { | ||
| ctrl.$setViewValue(element.val()); | ||
| } else { | ||
| // TODO(matsko): implement validateLater to reduce number of validations | ||
| ctrl.$validate(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { | ||
| // Note: no badInputChecker here by purpose as `url` is only a validation | ||
| // in browsers, i.e. we can always read out input.value even if it is not valid! | ||
| @@ -1719,7 +1995,11 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', | ||
| link: { | ||
| pre: function(scope, element, attr, ctrls) { | ||
| if (ctrls[0]) { | ||
| (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, | ||
| var type = lowercase(attr.type); | ||
| if ((type === 'range') && !attr.hasOwnProperty('ngInputRange')) { | ||
| type = 'text'; | ||
| } | ||
| (inputType[type] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, | ||
| $browser, $filter, $parse); | ||
| } | ||
| } | ||
| @@ -883,7 +883,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ | ||
| ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; | ||
| ctrl.$render(); | ||
|
|
||
| ctrl.$$runValidators(modelValue, viewValue, noop); | ||
| // It is possible that model and view value have been updated during render | ||
| ctrl.$$runValidators(ctrl.$modelValue, ctrl.$viewValue, noop); | ||
| } | ||
| } | ||
|
|
||
Oops, something went wrong.