Skip to content

Commit d7d172c

Browse files
committed
fix(input[range]): correctly initialize with interpolated min / max values
The interpolation directive only sets the actual element attribute value after a digest passed. That means previously, the min/max values on input range were not set when the first $render happened, so the browser would not adjust the input value according to min/max. This meant the range input and model would not be initialzed as expected. With this change, input range will set the actual element attribute value during its own linking phase, as it is already available on the attrs argument passed to the link fn. Fixes angular#14982
1 parent 21aa003 commit d7d172c

File tree

2 files changed

+159
-51
lines changed

2 files changed

+159
-51
lines changed

src/ng/directive/input.js

+43-38
Original file line numberDiff line numberDiff line change
@@ -1547,10 +1547,12 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15471547
numberFormatterParser(ctrl);
15481548
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
15491549

1550-
var minVal = 0,
1551-
maxVal = 100,
1552-
supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range',
1553-
validity = element[0].validity;
1550+
var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range',
1551+
minVal = supportsRange ? 0 : undefined,
1552+
maxVal = supportsRange ? 100 : undefined,
1553+
validity = element[0].validity,
1554+
minAttrType = isDefined(attr.ngMin) ? 'ngMin' : isDefined(attr.min) ? 'min' : false,
1555+
maxAttrType = isDefined(attr.ngMax) ? 'ngMax' : isDefined(attr.max) ? 'max' : false;
15541556

15551557
var originalRender = ctrl.$render;
15561558

@@ -1563,6 +1565,42 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15631565
} :
15641566
originalRender;
15651567

1568+
if (minAttrType) {
1569+
ctrl.$validators.min = minAttrType === 'min' && supportsRange ?
1570+
// Since all browsers set the input to a valid value, we don't need to check validity
1571+
function noopMinValidator() { return true; } :
1572+
// ngMin doesn't set the min attr, so the browser doesn't adjust the input value as setting min would
1573+
function minValidator(modelValue, viewValue) {
1574+
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
1575+
};
1576+
1577+
setInitialValueAndObserver(minAttrType, 'min', minChange);
1578+
}
1579+
1580+
if (maxAttrType) {
1581+
ctrl.$validators.max = maxAttrType === 'max' && supportsRange ?
1582+
// Since all browsers set the input to a valid value, we don't need to check validity
1583+
function noopMaxValidator() { return true; } :
1584+
// ngMax doesn't set the max attr, so the browser doesn't adjust the input value as setting max would
1585+
function maxValidator(modelValue, viewValue) {
1586+
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
1587+
};
1588+
1589+
setInitialValueAndObserver(maxAttrType, 'max', maxChange);
1590+
}
1591+
1592+
function setInitialValueAndObserver(actualAttrName, htmlAttrName, changeFn) {
1593+
// e.g. max === max
1594+
if (actualAttrName === htmlAttrName) {
1595+
// interpolated attributes set the attribute value only after a digest, but we need the
1596+
// attribute value when the input is first rendered, so that the browser can adjust the
1597+
// input value based on the min/max value
1598+
element.attr(htmlAttrName, attr[htmlAttrName]);
1599+
}
1600+
1601+
attr.$observe(htmlAttrName, changeFn);
1602+
}
1603+
15661604
function minChange(val) {
15671605
if (isDefined(val) && !isNumber(val)) {
15681606
val = parseFloat(val);
@@ -1577,8 +1615,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15771615
var elVal = element.val();
15781616
// IE11 doesn't set the el val correctly if the minVal is greater than the element value
15791617
if (minVal > elVal) {
1580-
element.val(minVal);
15811618
elVal = minVal;
1619+
element.val(elVal);
15821620
}
15831621
ctrl.$setViewValue(elVal);
15841622
} else {
@@ -1587,23 +1625,6 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15871625
}
15881626
}
15891627

1590-
var minAttrType = isDefined(attr.ngMin) ? 'ngMin' : isDefined(attr.min) ? 'min' : false;
1591-
if (minAttrType) {
1592-
ctrl.$validators.min = isDefined(attr.min) && supportsRange ?
1593-
function noopMinValidator(value) {
1594-
// Since all browsers set the input to a valid value, we don't need to check validity
1595-
return true;
1596-
} :
1597-
// ngMin doesn't set the min attr, so the browser doesn't adjust the input value as setting min would
1598-
function minValidator(modelValue, viewValue) {
1599-
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
1600-
};
1601-
1602-
// Assign minVal when the directive is linked. This won't run the validators as the model isn't ready yet
1603-
minChange(attr.min);
1604-
attr.$observe('min', minChange);
1605-
}
1606-
16071628
function maxChange(val) {
16081629
if (isDefined(val) && !isNumber(val)) {
16091630
val = parseFloat(val);
@@ -1627,22 +1648,6 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
16271648
ctrl.$validate();
16281649
}
16291650
}
1630-
var maxAttrType = isDefined(attr.max) ? 'max' : attr.ngMax ? 'ngMax' : false;
1631-
if (maxAttrType) {
1632-
ctrl.$validators.max = isDefined(attr.max) && supportsRange ?
1633-
function noopMaxValidator() {
1634-
// Since all browsers set the input to a valid value, we don't need to check validity
1635-
return true;
1636-
} :
1637-
// ngMax doesn't set the max attr, so the browser doesn't adjust the input value as setting max would
1638-
function maxValidator(modelValue, viewValue) {
1639-
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
1640-
};
1641-
1642-
// Assign maxVal when the directive is linked. This won't run the validators as the model isn't ready yet
1643-
maxChange(attr.max);
1644-
attr.$observe('max', maxChange);
1645-
}
16461651

16471652
}
16481653

test/ng/directive/inputSpec.js

+116-13
Original file line numberDiff line numberDiff line change
@@ -2877,7 +2877,7 @@ describe('input', function() {
28772877
expect(inputElm.val()).toEqual('50');
28782878
});
28792879

2880-
it('should set model to 50 when no value specified', function() {
2880+
it('should set model to 50 when no value specified and default min/max', function() {
28812881
var inputElm = helper.compileInput('<input type="range" ng-model="age" />');
28822882

28832883
expect(inputElm.val()).toBe('50');
@@ -2887,7 +2887,7 @@ describe('input', function() {
28872887
expect(scope.age).toBe(50);
28882888
});
28892889

2890-
it('should parse non-number values to 50', function() {
2890+
it('should parse non-number values to 50 when default min/max', function() {
28912891
var inputElm = helper.compileInput('<input type="range" ng-model="age" />');
28922892

28932893
scope.$apply('age = 10');
@@ -2949,8 +2949,20 @@ describe('input', function() {
29492949
describe('min', function() {
29502950

29512951
if (supportsRange) {
2952+
2953+
it('should initialize correctly with non-default model and min value', function() {
2954+
scope.value = -3;
2955+
scope.min = -5;
2956+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" min="{{min}}" />');
2957+
2958+
expect(inputElm).toBeValid();
2959+
expect(inputElm.val()).toBe('-3');
2960+
expect(scope.value).toBe(-3);
2961+
expect(scope.form.alias.$error.min).toBeFalsy();
2962+
});
2963+
29522964
// Browsers that implement range will never allow you to set the value < min values
2953-
it('should validate', function() {
2965+
it('should adjust invalid input values', function() {
29542966
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" min="10" />');
29552967

29562968
helper.changeInputValueTo('5');
@@ -2964,6 +2976,22 @@ describe('input', function() {
29642976
expect(scope.form.alias.$error.min).toBeFalsy();
29652977
});
29662978

2979+
it('should set the model to the min val if it is less than the min val', function() {
2980+
scope.value = -10;
2981+
// Default min is 0
2982+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" min="{{min}}" />');
2983+
2984+
expect(inputElm).toBeValid();
2985+
expect(inputElm.val()).toBe('0');
2986+
expect(scope.value).toBe(0);
2987+
2988+
scope.$apply('value = 5; min = 10');
2989+
2990+
expect(inputElm).toBeValid();
2991+
expect(inputElm.val()).toBe('10');
2992+
expect(scope.value).toBe(10);
2993+
});
2994+
29672995
it('should adjust the element and model value when the min value changes on-the-fly', function() {
29682996
scope.min = 10;
29692997
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" min="{{min}}" />');
@@ -2997,8 +3025,9 @@ describe('input', function() {
29973025
});
29983026

29993027
} else {
3028+
// input[type=range] will become type=text in browsers that don't support it
3029+
30003030
it('should validate if "range" is not implemented', function() {
3001-
// This will become type=text in browsers that don't support it
30023031
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" min="10" />');
30033032

30043033
helper.changeInputValueTo('5');
@@ -3012,6 +3041,34 @@ describe('input', function() {
30123041
expect(scope.form.alias.$error.min).toBeFalsy();
30133042
});
30143043

3044+
it('should not assume a min val of 0 if the min interpolates to a non-number', function() {
3045+
scope.value = -10;
3046+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" min="{{min}}" />');
3047+
3048+
expect(inputElm).toBeValid();
3049+
expect(inputElm.val()).toBe('-10');
3050+
expect(scope.value).toBe(-10);
3051+
expect(scope.form.alias.$error.min).toBeFalsy();
3052+
3053+
helper.changeInputValueTo('-5');
3054+
expect(inputElm).toBeValid();
3055+
expect(inputElm.val()).toBe('-5');
3056+
expect(scope.value).toBe(-5);
3057+
expect(scope.form.alias.$error.min).toBeFalsy();
3058+
3059+
scope.$apply('max = "null"');
3060+
expect(inputElm).toBeValid();
3061+
expect(inputElm.val()).toBe('-5');
3062+
expect(scope.value).toBe(-5);
3063+
expect(scope.form.alias.$error.max).toBeFalsy();
3064+
3065+
scope.$apply('max = "asdf"');
3066+
expect(inputElm).toBeValid();
3067+
expect(inputElm.val()).toBe('-5');
3068+
expect(scope.value).toBe(-5);
3069+
expect(scope.form.alias.$error.max).toBeFalsy();
3070+
});
3071+
30153072
it('should validate even if the min value changes on-the-fly', function() {
30163073
scope.min = 10;
30173074
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" min="{{min}}" />');
@@ -3074,6 +3131,7 @@ describe('input', function() {
30743131
scope.min = 20;
30753132
scope.$digest();
30763133
expect(inputElm).toBeInvalid();
3134+
expect(inputElm.val()).toBe('15');
30773135

30783136
scope.min = null;
30793137
scope.$digest();
@@ -3094,6 +3152,17 @@ describe('input', function() {
30943152

30953153
if (supportsRange) {
30963154
// Browsers that implement range will never allow you to set the value > max value
3155+
it('should initialize correctly with non-default model and max value', function() {
3156+
scope.value = 130;
3157+
scope.max = 150;
3158+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="{{max}}" />');
3159+
3160+
expect(inputElm).toBeValid();
3161+
expect(inputElm.val()).toBe('130');
3162+
expect(scope.value).toBe(130);
3163+
expect(scope.form.alias.$error.max).toBeFalsy();
3164+
});
3165+
30973166
it('should validate', function() {
30983167
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="10" />');
30993168

@@ -3108,9 +3177,16 @@ describe('input', function() {
31083177
expect(scope.form.alias.$error.max).toBeFalsy();
31093178
});
31103179

3111-
it('should set the model to the max val if it is more than the max val', function() {
3112-
scope.value = 90;
3113-
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="10" />');
3180+
it('should set the model to the max val if it is greater than the max val', function() {
3181+
scope.value = 110;
3182+
// Default max is 100
3183+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="{{max}}" />');
3184+
3185+
expect(inputElm).toBeValid();
3186+
expect(inputElm.val()).toBe('100');
3187+
expect(scope.value).toBe(100);
3188+
3189+
scope.$apply('value = 90; max = 10');
31143190

31153191
expect(inputElm).toBeValid();
31163192
expect(inputElm.val()).toBe('10');
@@ -3164,6 +3240,34 @@ describe('input', function() {
31643240
expect(scope.form.alias.$error.max).toBeFalsy();
31653241
});
31663242

3243+
it('should not assume a max val of 100 if the max attribute interpolates to a non-number', function() {
3244+
scope.value = 120;
3245+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="{{max}}" />');
3246+
3247+
expect(inputElm).toBeValid();
3248+
expect(inputElm.val()).toBe('120');
3249+
expect(scope.value).toBe(120);
3250+
expect(scope.form.alias.$error.max).toBeFalsy();
3251+
3252+
helper.changeInputValueTo('140');
3253+
expect(inputElm).toBeValid();
3254+
expect(inputElm.val()).toBe('140');
3255+
expect(scope.value).toBe(140);
3256+
expect(scope.form.alias.$error.max).toBeFalsy();
3257+
3258+
scope.$apply('max = null');
3259+
expect(inputElm).toBeValid();
3260+
expect(inputElm.val()).toBe('140');
3261+
expect(scope.value).toBe(140);
3262+
expect(scope.form.alias.$error.max).toBeFalsy();
3263+
3264+
scope.$apply('max = "asdf"');
3265+
expect(inputElm).toBeValid();
3266+
expect(inputElm.val()).toBe('140');
3267+
expect(scope.value).toBe(140);
3268+
expect(scope.form.alias.$error.max).toBeFalsy();
3269+
});
3270+
31673271
it('should validate even if the max value changes on-the-fly', function() {
31683272
scope.max = 10;
31693273
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="{{max}}" />');
@@ -3245,22 +3349,21 @@ describe('input', function() {
32453349

32463350
describe('min and max', function() {
32473351

3248-
it('should keep the initial default value when min and max are specified', function() {
3352+
it('should set the correct initial value when min and max are specified', function() {
32493353
scope.max = 80;
32503354
scope.min = 40;
32513355
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="{{max}}" min="{{min}}" />');
32523356

3253-
expect(inputElm.val()).toBe('50');
3254-
expect(scope.value).toBe(50);
3357+
expect(inputElm.val()).toBe('60');
3358+
expect(scope.value).toBe(60);
32553359
});
32563360

3257-
32583361
it('should set element and model value to min if max is less than min', function() {
32593362
scope.min = 40;
32603363
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="{{max}}" min="{{min}}" />');
32613364

3262-
expect(inputElm.val()).toBe('50');
3263-
expect(scope.value).toBe(50);
3365+
expect(inputElm.val()).toBe('70');
3366+
expect(scope.value).toBe(70);
32643367

32653368
scope.max = 20;
32663369
scope.$digest();

0 commit comments

Comments
 (0)