diff --git a/docs/content/error/ngModel/numfmt.ngdoc b/docs/content/error/ngModel/numfmt.ngdoc
index d59248f79393..9a361a74158e 100644
--- a/docs/content/error/ngModel/numfmt.ngdoc
+++ b/docs/content/error/ngModel/numfmt.ngdoc
@@ -3,7 +3,8 @@
@fullName Model is not of type `number`
@description
-The number input directive `` 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.
diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js
index 81e11f9309e1..9c305b53db6b 100644
--- a/src/ng/directive/input.js
+++ b/src/ng/directive/input.js
@@ -1027,6 +1027,145 @@ var inputType = {
*/
'radio': radioInputType,
+ /**
+ * @ngdoc input
+ * @name input[range]
+ *
+ * @description
+ * Native range input with validation and transformation.
+ *
+ *
+ *
+ * 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 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).
+ *
+ *
+ * **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.
+ *
+ *
+ * This documentation page refers to elements which have the built-in support enabled; i.e.
+ * elements _with_ the `ng-input-range` attribute.
+ *
+ *
+ *
+ * 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 ``,
+ * 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
+
+
+
+
+
+
+
+ * ## Range Input with ngMin & ngMax attributes
+
+ * @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);
}
}
diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js
index b5ff080393fd..18365350774a 100644
--- a/src/ng/directive/ngModel.js
+++ b/src/ng/directive/ngModel.js
@@ -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);
}
}
diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js
index 8c6dce8b8a6e..1d81f9a5efcf 100644
--- a/test/ng/directive/inputSpec.js
+++ b/test/ng/directive/inputSpec.js
@@ -2791,6 +2791,618 @@ describe('input', function() {
});
});
+ describe('range', function() {
+
+ var scope;
+
+ var rangeTestEl = angular.element('');
+ var supportsRange = rangeTestEl[0].type === 'range';
+ beforeEach(function() {
+ scope = $rootScope;
+ });
+
+ it('should be treated as `input[text]` without the `ng-input-range` attribute', function() {
+ var inputElm = helper.compileInput('');
+ var ngModel = inputElm.controller('ngModel');
+
+ helper.changeInputValueTo(25);
+
+ expect(scope.age).toBe('25');
+ expect(ngModel.$$parserName).toBeUndefined();
+ });
+
+ it('should not be treated as `input[text]` with the `ng-input-range` attribute', function() {
+ var inputElm = helper.compileInput('');
+ var ngModel = inputElm.controller('ngModel');
+
+ helper.changeInputValueTo('25');
+
+ expect(scope.age).toBe(25);
+ expect(ngModel.$$parserName).toBe('number');
+ });
+
+ 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 = compileRangeInput('ng-model="age"');
+
+ 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 and default min/max', function() {
+ var inputElm = compileRangeInput('ng-model="age"');
+
+ expect(inputElm.val()).toBe('50');
+
+ scope.$apply('age = null');
+
+ expect(scope.age).toBe(50);
+ });
+
+ it('should parse non-number values to 50 when default min/max', function() {
+ var inputElm = compileRangeInput('ng-model="age"');
+
+ 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 = compileRangeInput('ng-model="age"');
+
+ 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 = compileRangeInput('ng-model="age"');
+
+ 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 = compileRangeInput('ng-model="age"', {
+ 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 = compileRangeInput('ng-model="value"');
+ }).toThrowMinErr('ngModel', 'numfmt', 'Expected `one` to be a number');
+ });
+
+
+ describe('min', function() {
+
+ if (supportsRange) {
+
+ it('should initialize correctly with non-default model and min value', function() {
+ scope.value = -3;
+ scope.min = -5;
+ var inputElm = compileRangeInput('ng-model="value" name="alias" min="{{min}}"');
+
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('-3');
+ expect(scope.value).toBe(-3);
+ expect(scope.form.alias.$error.min).toBeFalsy();
+ });
+
+ // Browsers that implement range will never allow you to set the value < min values
+ it('should adjust invalid input values', function() {
+ var inputElm = compileRangeInput('ng-model="value" name="alias" min="10"');
+
+ 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 set the model to the min val if it is less than the min val', function() {
+ scope.value = -10;
+ // Default min is 0
+ var inputElm = compileRangeInput('ng-model="value" name="alias" min="{{min}}"');
+
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('0');
+ expect(scope.value).toBe(0);
+
+ scope.$apply('value = 5; min = 10');
+
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('10');
+ expect(scope.value).toBe(10);
+ });
+
+ it('should adjust the element and model value when the min value changes on-the-fly', function() {
+ scope.min = 10;
+ var inputElm = compileRangeInput('ng-model="value" name="alias" min="{{min}}"');
+
+ 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 {
+ // input[type=range] will become type=text in browsers that don't support it
+
+ it('should validate if "range" is not implemented', function() {
+ var inputElm = compileRangeInput('ng-model="value" name="alias" min="10"');
+
+ 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 not assume a min val of 0 if the min interpolates to a non-number', function() {
+ scope.value = -10;
+ var inputElm = compileRangeInput('ng-model="value" name="alias" min="{{min}}"');
+
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('-10');
+ expect(scope.value).toBe(-10);
+ expect(scope.form.alias.$error.min).toBeFalsy();
+
+ helper.changeInputValueTo('-5');
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('-5');
+ expect(scope.value).toBe(-5);
+ expect(scope.form.alias.$error.min).toBeFalsy();
+
+ scope.$apply('max = "null"');
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('-5');
+ expect(scope.value).toBe(-5);
+ expect(scope.form.alias.$error.max).toBeFalsy();
+
+ scope.$apply('max = "asdf"');
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('-5');
+ expect(scope.value).toBe(-5);
+ expect(scope.form.alias.$error.max).toBeFalsy();
+ });
+
+ it('should validate even if the min value changes on-the-fly', function() {
+ scope.min = 10;
+ var inputElm = compileRangeInput('ng-model="value" name="alias" min="{{min}}"');
+
+ 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('max', function() {
+
+ if (supportsRange) {
+ // Browsers that implement range will never allow you to set the value > max value
+ it('should initialize correctly with non-default model and max value', function() {
+ scope.value = 130;
+ scope.max = 150;
+ var inputElm = compileRangeInput('ng-model="value" name="alias" max="{{max}}"');
+
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('130');
+ expect(scope.value).toBe(130);
+ expect(scope.form.alias.$error.max).toBeFalsy();
+ });
+
+ it('should validate', function() {
+ var inputElm = compileRangeInput('ng-model="value" name="alias" max="10"');
+
+ 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 greater than the max val', function() {
+ scope.value = 110;
+ // Default max is 100
+ var inputElm = compileRangeInput('ng-model="value" name="alias" max="{{max}}"');
+
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('100');
+ expect(scope.value).toBe(100);
+
+ scope.$apply('value = 90; max = 10');
+
+ 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 = compileRangeInput('ng-model="value" name="alias" max="{{max}}"');
+
+ 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 = compileRangeInput('ng-model="value" name="alias" max="10"');
+
+ 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 not assume a max val of 100 if the max attribute interpolates to a non-number', function() {
+ scope.value = 120;
+ var inputElm = compileRangeInput('ng-model="value" name="alias" max="{{max}}"');
+
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('120');
+ expect(scope.value).toBe(120);
+ expect(scope.form.alias.$error.max).toBeFalsy();
+
+ helper.changeInputValueTo('140');
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('140');
+ expect(scope.value).toBe(140);
+ expect(scope.form.alias.$error.max).toBeFalsy();
+
+ scope.$apply('max = null');
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('140');
+ expect(scope.value).toBe(140);
+ expect(scope.form.alias.$error.max).toBeFalsy();
+
+ scope.$apply('max = "asdf"');
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('140');
+ expect(scope.value).toBe(140);
+ 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 = compileRangeInput('ng-model="value" name="alias" max="{{max}}"');
+
+ 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');
+ });
+ }
+ });
+
+ if (supportsRange) {
+
+ describe('min and max', function() {
+
+ it('should set the correct initial value when min and max are specified', function() {
+ scope.max = 80;
+ scope.min = 40;
+ var inputElm = compileRangeInput('ng-model="value" name="alias" max="{{max}}" min="{{min}}"');
+
+ expect(inputElm.val()).toBe('60');
+ expect(scope.value).toBe(60);
+ });
+
+ it('should set element and model value to min if max is less than min', function() {
+ scope.min = 40;
+ var inputElm = compileRangeInput('ng-model="value" name="alias" max="{{max}}" min="{{min}}"');
+
+ expect(inputElm.val()).toBe('70');
+ expect(scope.value).toBe(70);
+
+ scope.max = 20;
+ scope.$digest();
+
+ expect(inputElm.val()).toBe('40');
+ expect(scope.value).toBe(40);
+ });
+ });
+
+ }
+
+
+ describe('step', function() {
+
+ if (supportsRange) {
+ // Browsers that implement range will never allow you to set a value that doesn't match the step value
+ // However, currently only Firefox fully inplements the spec when setting the value after the step value changes.
+ // Other browsers fail in various edge cases, which is why they are not tested here.
+ it('should round the input value to the nearest step on user input', function() {
+ var inputElm = compileRangeInput('ng-model="value" name="alias" step="5"');
+
+ helper.changeInputValueTo('5');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(5);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+
+ helper.changeInputValueTo('10');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(10);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+
+ helper.changeInputValueTo('9');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(10);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+
+ helper.changeInputValueTo('7');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(5);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+
+ helper.changeInputValueTo('7.5');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(10);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+ });
+
+ it('should round the input value to the nearest step when setting the model', function() {
+ var inputElm = compileRangeInput('ng-model="value" name="alias" step="5"');
+
+ scope.$apply('value = 10');
+ expect(inputElm.val()).toBe('10');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(10);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+
+ scope.$apply('value = 5');
+ expect(inputElm.val()).toBe('5');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(5);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+
+ scope.$apply('value = 7.5');
+ expect(inputElm.val()).toBe('10');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(10);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+
+ scope.$apply('value = 7');
+ expect(inputElm.val()).toBe('5');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(5);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+
+ scope.$apply('value = 9');
+ expect(inputElm.val()).toBe('10');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(10);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+ });
+
+ } else {
+ it('should validate if "range" is not implemented', function() {
+ scope.step = 10;
+ scope.value = 20;
+ var inputElm = compileRangeInput('ng-model="value" name="alias" step="{{step}}"');
+
+ expect(inputElm.val()).toBe('20');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(20);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+
+ helper.changeInputValueTo('18');
+ expect(inputElm).toBeInvalid();
+ expect(inputElm.val()).toBe('18');
+ expect(scope.value).toBeUndefined();
+ expect(scope.form.alias.$error.step).toBeTruthy();
+
+ helper.changeInputValueTo('10');
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('10');
+ expect(scope.value).toBe(10);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+
+ scope.$apply('value = 12');
+ expect(inputElm).toBeInvalid();
+ expect(inputElm.val()).toBe('12');
+ expect(scope.value).toBe(12);
+ expect(scope.form.alias.$error.step).toBeTruthy();
+ });
+
+ it('should validate even if the step value changes on-the-fly', function() {
+ scope.step = 10;
+ var inputElm = compileRangeInput('ng-model="value" name="alias" step="{{step}}"');
+
+ helper.changeInputValueTo('10');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(10);
+
+ // Step changes, but value matches
+ scope.$apply('step = 5');
+ expect(inputElm.val()).toBe('10');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(10);
+ expect(scope.form.alias.$error.step).toBeFalsy();
+
+ // Step changes, value does not match
+ scope.$apply('step = 6');
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeUndefined();
+ expect(inputElm.val()).toBe('10');
+ expect(scope.form.alias.$error.step).toBeTruthy();
+
+ // null = valid
+ scope.$apply('step = null');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(10);
+ expect(inputElm.val()).toBe('10');
+ expect(scope.form.alias.$error.step).toBeFalsy();
+
+ // Step val as string
+ scope.$apply('step = "7"');
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeUndefined();
+ expect(inputElm.val()).toBe('10');
+ expect(scope.form.alias.$error.step).toBeTruthy();
+
+ // unparsable string is ignored
+ scope.$apply('step = "abc"');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(10);
+ expect(inputElm.val()).toBe('10');
+ expect(scope.form.alias.$error.step).toBeFalsy();
+ });
+ }
+ });
+
+ // Helpers
+ function compileRangeInput(attrs, opts) {
+ return helper.compileInput('', opts);
+ }
+ });
describe('email', function() {
diff --git a/test/ng/directive/validatorsSpec.js b/test/ng/directive/validatorsSpec.js
index 6cd2c14dd7b9..224b848a42cc 100644
--- a/test/ng/directive/validatorsSpec.js
+++ b/test/ng/directive/validatorsSpec.js
@@ -351,7 +351,7 @@ describe('validators', function() {
it('should accept values of any length when maxlength is non-numeric', function() {
- var inputElm = helper.compileInput('');
+ var inputElm = helper.compileInput('');
helper.changeInputValueTo('aaaaaaaaaa');
$rootScope.$apply('maxlength = "5"');