diff --git a/docs/content/error/ngModel/numfmt.ngdoc b/docs/content/error/ngModel/numfmt.ngdoc
index 19e50f522ee3..068c50701bbd 100644
--- a/docs/content/error/ngModel/numfmt.ngdoc
+++ b/docs/content/error/ngModel/numfmt.ngdoc
@@ -3,7 +3,7 @@
@fullName Model is not of type `number`
@description
-The number input directive `` requires the model to be a `number`.
+The `input[number]` and `input[range]` directives require the model to be a `number`.
If the model is something else, this error will be thrown.
diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js
index df7af367e2d3..a01ce2fa662b 100644
--- a/src/ng/directive/input.js
+++ b/src/ng/directive/input.js
@@ -1031,6 +1031,113 @@ var inputType = {
*/
'radio': radioInputType,
+ /**
+ * @ngdoc input
+ * @name input[range]
+ *
+ * @description
+ * Native range input with validation and transformation.
+ *
+ * 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. 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.
+ *
+ * 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 ``,
+ * 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` and
+ * `max` 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, except when using `ngMax` and `ngMin`, which are not affected by automatic
+ * value adjustment, because they do not set the `min` and `max` attributes.
+ *
+ * @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=} ngMin Takes an expression. Sets the `min` validation to ensure that the value
+ * entered is greater than `min`. Does not set the `min` attribute and therefore
+ * adds no native HTML5 validation. It also means the browser won't adjust the
+ * element value in case `min` is greater than the current value.
+ * @param {string=} ngMax Takes an expression. Sets the `max` validation to ensure that the value
+ * entered is less than `max`. Does not set the `max` attribute and therefore
+ * adds no native HTML5 validation. It also means the browser won't adjust the
+ * element value in case `max` is less than the current value.
+ * @param {string=} ngChange Angular expression to be executed when the ngModel value changes due
+ * to user interaction with the input element.
+ *
+ * @example
+
+
+
+
+
+
+
+ * ## Range Input with ngMin & ngMax attributes
+
+ * @example
+
+
+
+
+
+
+
+ */
+ 'range': rangeInputType,
/**
* @ngdoc input
@@ -1382,10 +1489,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;
@@ -1402,6 +1506,12 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
return value;
});
+}
+
+function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
+ badInputChecker(scope, element, attr, ctrl);
+ numberFormatterParser(ctrl);
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
if (isDefined(attr.min) || attr.ngMin) {
var minVal;
@@ -1436,6 +1546,110 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
}
+function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
+ badInputChecker(scope, element, attr, ctrl);
+ numberFormatterParser(ctrl);
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+
+ var minVal = 0,
+ maxVal = 100,
+ supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range',
+ validity = element[0].validity;
+
+ 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;
+
+ function minChange(val) {
+ if (isDefined(val) && !isNumber(val)) {
+ val = parseFloat(val);
+ }
+ minVal = isNumber(val) && !isNaN(val) ? val : undefined;
+ // ignore changes before model is initialized
+ if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ if (supportsRange && minAttrType === 'min') {
+ var elVal = element.val();
+ // IE11 doesn't set the el val correctly if the minVal is greater than the element value
+ if (minVal > elVal) {
+ element.val(minVal);
+ elVal = minVal;
+ }
+ ctrl.$setViewValue(elVal);
+ } else {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ }
+
+ var minAttrType = isDefined(attr.ngMin) ? 'ngMin' : isDefined(attr.min) ? 'min' : false;
+ if (minAttrType) {
+ ctrl.$validators.min = isDefined(attr.min) && supportsRange ?
+ function noopMinValidator(value) {
+ // Since all browsers set the input to a valid value, we don't need to check validity
+ return true;
+ } :
+ // ngMin doesn't set the min attr, so the browser doesn't adjust the input value as setting min would
+ function minValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
+ };
+
+ // Assign minVal when the directive is linked. This won't run the validators as the model isn't ready yet
+ minChange(attr.min);
+ attr.$observe('min', minChange);
+ }
+
+ function maxChange(val) {
+ if (isDefined(val) && !isNumber(val)) {
+ val = parseFloat(val);
+ }
+ maxVal = isNumber(val) && !isNaN(val) ? val : undefined;
+ // ignore changes before model is initialized
+ if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ if (supportsRange && maxAttrType === 'max') {
+ 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);
+ elVal = minVal;
+ }
+ ctrl.$setViewValue(elVal);
+ } else {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ }
+ var maxAttrType = isDefined(attr.max) ? 'max' : attr.ngMax ? 'ngMax' : false;
+ if (maxAttrType) {
+ ctrl.$validators.max = isDefined(attr.max) && supportsRange ?
+ function noopMaxValidator() {
+ // Since all browsers set the input to a valid value, we don't need to check validity
+ return true;
+ } :
+ // ngMax doesn't set the max attr, so the browser doesn't adjust the input value as setting max would
+ function maxValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
+ };
+
+ // Assign maxVal when the directive is linked. This won't run the validators as the model isn't ready yet
+ maxChange(attr.max);
+ attr.$observe('max', maxChange);
+ }
+
+}
+
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!
diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js
index 90b4bafabcbc..dfb192b63bff 100644
--- a/src/ng/directive/ngModel.js
+++ b/src/ng/directive/ngModel.js
@@ -881,7 +881,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);
}
}
diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js
index 6b80cce7c1b3..23df77d88b6b 100644
--- a/test/ng/directive/inputSpec.js
+++ b/test/ng/directive/inputSpec.js
@@ -2819,6 +2819,430 @@ describe('input', function() {
});
});
+ describe('range', function() {
+
+ var scope;
+
+ var rangeTestEl = angular.element('');
+ var supportsRange = rangeTestEl[0].type === 'range';
+ beforeEach(function() {
+ scope = $rootScope;
+ });
+
+ if (supportsRange) {
+ // This behavior only applies to browsers that implement the range input, which do not
+ // allow to set a non-number value and will set the value of the input to 50 even when you
+ // change it directly on the element.
+ // Other browsers fall back to text inputs, where setting a model value of 50 does not make
+ // sense if the input value is a string. These browsers will mark the input as invalid instead.
+
+ it('should render as 50 if null', function() {
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('25');
+ expect(scope.age).toBe(25);
+
+ scope.$apply('age = null');
+
+ expect(inputElm.val()).toEqual('50');
+ });
+
+ it('should set model to 50 when no value specified', function() {
+ var inputElm = helper.compileInput('');
+
+ expect(inputElm.val()).toBe('50');
+
+ scope.$apply('age = null');
+
+ expect(scope.age).toBe(50);
+ });
+
+ it('should parse non-number values to 50', function() {
+ var inputElm = helper.compileInput('');
+
+ scope.$apply('age = 10');
+ expect(inputElm.val()).toBe('10');
+
+ helper.changeInputValueTo('');
+ expect(scope.age).toBe(50);
+ expect(inputElm).toBeValid();
+ });
+
+ } else {
+
+ it('should reset the model if view is invalid', function() {
+ var inputElm = helper.compileInput('');
+
+ scope.$apply('age = 100');
+ expect(inputElm.val()).toBe('100');
+
+ helper.changeInputValueTo('100X');
+ expect(inputElm.val()).toBe('100X');
+ expect(scope.age).toBeUndefined();
+ expect(inputElm).toBeInvalid();
+ });
+ }
+
+ it('should parse the input value to a Number', function() {
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('75');
+ expect(scope.age).toBe(75);
+ });
+
+
+ it('should only invalidate the model if suffering from bad input when the data is parsed', function() {
+ scope.age = 60;
+
+ var inputElm = helper.compileInput('', {
+ valid: false,
+ badInput: true
+ });
+
+ expect(inputElm).toBeValid();
+
+ helper.changeInputValueTo('this-will-fail-because-of-the-badInput-flag');
+
+ expect(scope.age).toBeUndefined();
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ it('should throw if the model value is not a number', function() {
+ expect(function() {
+ scope.value = 'one';
+ var inputElm = helper.compileInput('');
+ }).toThrowMinErr('ngModel', 'numfmt', 'Expected `one` to be a number');
+ });
+
+
+ describe('min', function() {
+
+ if (supportsRange) {
+ // Browsers that implement range will never allow you to set the value < min values
+ it('should validate', function() {
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('5');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(10);
+ expect(scope.form.alias.$error.min).toBeFalsy();
+
+ helper.changeInputValueTo('100');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(100);
+ expect(scope.form.alias.$error.min).toBeFalsy();
+ });
+
+ it('should adjust the element and model value when the min value changes on-the-fly', function() {
+ scope.min = 10;
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('15');
+ expect(inputElm).toBeValid();
+
+ scope.min = 20;
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(20);
+ expect(inputElm.val()).toBe('20');
+
+ scope.min = null;
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(20);
+ expect(inputElm.val()).toBe('20');
+
+ scope.min = '15';
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(20);
+ expect(inputElm.val()).toBe('20');
+
+ scope.min = 'abc';
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(20);
+ expect(inputElm.val()).toBe('20');
+ });
+
+ } else {
+ it('should validate if "range" is not implemented', function() {
+ // This will become type=text in browsers that don't support it
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('5');
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeUndefined();
+ expect(scope.form.alias.$error.min).toBeTruthy();
+
+ helper.changeInputValueTo('100');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(100);
+ expect(scope.form.alias.$error.min).toBeFalsy();
+ });
+
+ it('should validate even if the min value changes on-the-fly', function() {
+ scope.min = 10;
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('15');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(15);
+
+ scope.min = 20;
+ scope.$digest();
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeUndefined();
+ expect(inputElm.val()).toBe('15');
+
+ scope.min = null;
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(15);
+ expect(inputElm.val()).toBe('15');
+
+ scope.min = '16';
+ scope.$digest();
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeUndefined();
+ expect(inputElm.val()).toBe('15');
+
+ scope.min = 'abc';
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(15);
+ expect(inputElm.val()).toBe('15');
+ });
+
+ }
+ });
+
+ describe('ngMin', function() {
+
+ it('should validate', function() {
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('1');
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeFalsy();
+ expect(scope.form.alias.$error.min).toBeTruthy();
+
+ helper.changeInputValueTo('100');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(100);
+ expect(scope.form.alias.$error.min).toBeFalsy();
+ });
+
+ it('should validate even if the ngMin value changes on-the-fly', function() {
+ scope.min = 10;
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('15');
+ expect(inputElm).toBeValid();
+
+ scope.min = 20;
+ scope.$digest();
+ expect(inputElm).toBeInvalid();
+
+ scope.min = null;
+ scope.$digest();
+ expect(inputElm).toBeValid();
+
+ scope.min = '20';
+ scope.$digest();
+ expect(inputElm).toBeInvalid();
+
+ scope.min = 'abc';
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ });
+ });
+
+
+ describe('max', function() {
+
+ if (supportsRange) {
+ // Browsers that implement range will never allow you to set the value > max value
+ it('should validate', function() {
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('20');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(10);
+ expect(scope.form.alias.$error.max).toBeFalsy();
+
+ helper.changeInputValueTo('0');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(0);
+ expect(scope.form.alias.$error.max).toBeFalsy();
+ });
+
+ it('should set the model to the max val if it is more than the max val', function() {
+ scope.value = 90;
+ var inputElm = helper.compileInput('');
+
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('10');
+ expect(scope.value).toBe(10);
+ });
+
+ it('should adjust the element and model value if the max value changes on-the-fly', function() {
+ scope.max = 10;
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('5');
+ expect(inputElm).toBeValid();
+
+ scope.max = 0;
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(0);
+ expect(inputElm.val()).toBe('0');
+
+ scope.max = null;
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(0);
+ expect(inputElm.val()).toBe('0');
+
+ scope.max = '4';
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(0);
+ expect(inputElm.val()).toBe('0');
+
+ scope.max = 'abc';
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(0);
+ expect(inputElm.val()).toBe('0');
+ });
+
+ } else {
+ it('should validate if "range" is not implemented', function() {
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('20');
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeUndefined();
+ expect(scope.form.alias.$error.max).toBeTruthy();
+
+ helper.changeInputValueTo('0');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(0);
+ expect(scope.form.alias.$error.max).toBeFalsy();
+ });
+
+ it('should validate even if the max value changes on-the-fly', function() {
+ scope.max = 10;
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('5');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(5);
+
+ scope.max = 0;
+ scope.$digest();
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeUndefined();
+ expect(inputElm.val()).toBe('5');
+
+ scope.max = null;
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(5);
+ expect(inputElm.val()).toBe('5');
+
+ scope.max = '4';
+ scope.$digest();
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeUndefined();
+ expect(inputElm.val()).toBe('5');
+
+ scope.max = 'abc';
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(5);
+ expect(inputElm.val()).toBe('5');
+ });
+ }
+ });
+
+ describe('ngMax', function() {
+
+ it('should validate', function() {
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('20');
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeUndefined();
+ expect(scope.form.alias.$error.max).toBeTruthy();
+
+ helper.changeInputValueTo('0');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(0);
+ expect(scope.form.alias.$error.max).toBeFalsy();
+ });
+
+ it('should validate even if the ngMax value changes on-the-fly', function() {
+ scope.max = 10;
+ var inputElm = helper.compileInput('');
+
+ helper.changeInputValueTo('5');
+ expect(inputElm).toBeValid();
+
+ scope.max = 0;
+ scope.$digest();
+ expect(inputElm).toBeInvalid();
+
+ scope.max = null;
+ scope.$digest();
+ expect(inputElm).toBeValid();
+
+ scope.max = '4';
+ scope.$digest();
+ expect(inputElm).toBeInvalid();
+
+ scope.max = 'abc';
+ scope.$digest();
+ expect(inputElm).toBeValid();
+ });
+
+ });
+
+ if (supportsRange) {
+
+ describe('min and max', function() {
+
+ it('should keep the initial default value when min and max are specified', function() {
+ scope.max = 80;
+ scope.min = 40;
+ var inputElm = helper.compileInput('');
+
+ expect(inputElm.val()).toBe('50');
+ expect(scope.value).toBe(50);
+ });
+
+
+ it('should set element and model value to min if max is less than min', function() {
+ scope.min = 40;
+ var inputElm = helper.compileInput('');
+
+ expect(inputElm.val()).toBe('50');
+ expect(scope.value).toBe(50);
+
+ scope.max = 20;
+ scope.$digest();
+
+ expect(inputElm.val()).toBe('40');
+ expect(scope.value).toBe(40);
+ });
+ });
+
+ }
+
+ });
describe('email', function() {