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

Commit 9d30cea

Browse files
committed
fix all edge cases
- when interpolated min / max change, call $setViewValue - when ngMin / ngMax change, call $validate . This is because they don't set the min / max attribute values in the DOM, and the browser doesn't adjust the values in this case - browsers that don't support range (IE9) don't manipulate the input values HOWEVER they validate them according to the number, min and max rules - browsers that support range use the native validity state as the validators. Due to the nature of range they should never be invalid, though
1 parent 556f305 commit 9d30cea

File tree

2 files changed

+169
-114
lines changed

2 files changed

+169
-114
lines changed

src/ng/directive/input.js

+42-37
Original file line numberDiff line numberDiff line change
@@ -1487,7 +1487,7 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
14871487

14881488
var minVal = 0,
14891489
maxVal = 100,
1490-
hasNativeValidation = ctrl.$$hasNativeValidators && element[0].validity,
1490+
supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range',
14911491
validity = element[0].validity;
14921492

14931493
ctrl.$$parserName = 'number';
@@ -1507,22 +1507,43 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15071507
return value;
15081508
});
15091509

1510+
var originalRender = ctrl.$render;
1511+
1512+
ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ?
1513+
//Browsers that implement range will set these values automatically, but reading the adjusted values after
1514+
//$render would cause the min / max validators to be applied with the wrong value
1515+
function rangeRender() {
1516+
originalRender();
1517+
ctrl.$setViewValue(element.val());
1518+
} :
1519+
originalRender;
1520+
15101521
function minChange(val) {
15111522
if (isDefined(val) && !isNumber(val)) {
15121523
val = parseFloat(val);
15131524
}
15141525
minVal = isNumber(val) && !isNaN(val) ? val : undefined;
1526+
// ignore changes before model is initialized
1527+
if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
1528+
return;
1529+
}
1530+
15151531
// TODO(matsko): implement validateLater to reduce number of validations
1516-
ctrl.$validate();
1532+
if (minAttrType === 'min') {
1533+
ctrl.$setViewValue(element.val());
1534+
} else {
1535+
ctrl.$validate();
1536+
}
15171537
}
15181538

1519-
if (isDefined(attr.min) || attr.ngMin) {
1520-
ctrl.$validators.min = hasNativeValidation && isDefined(validity.rangeUnderflow) ?
1521-
function nativeMaxValidator(value) {
1539+
var minAttrType = isDefined(attr.min) ? 'min' : attr.ngMin ? 'ngMin' : false;
1540+
if (minAttrType) {
1541+
ctrl.$validators.min = isDefined(attr.min) && supportsRange && isDefined(validity.rangeUnderflow) ?
1542+
function nativeMinValidator(value) {
15221543
return !element[0].validity.rangeOverflow;
15231544
} :
1524-
function maxValidator(modelValue, viewValue) {
1525-
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue >= minVal;
1545+
function minValidator(modelValue, viewValue) {
1546+
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
15261547
};
15271548

15281549
// Assign minVal when the directive is linked. This won't $validate as the model isn't ready yet
@@ -1536,11 +1557,22 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15361557
}
15371558
maxVal = isNumber(val) && !isNaN(val) ? val : undefined;
15381559
// TODO(matsko): implement validateLater to reduce number of validations
1539-
ctrl.$validate();
1560+
// ignore changes before model is initialized
1561+
if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
1562+
return;
1563+
}
1564+
1565+
if (maxAttrType === 'max') {
1566+
ctrl.$setViewValue(element.val());
1567+
} else {
1568+
ctrl.$validate();
1569+
}
15401570
}
15411571

1542-
if (isDefined(attr.max) || attr.ngMax) {
1543-
ctrl.$validators.max = hasNativeValidation && isDefined(validity.rangeOverflow)?
1572+
var maxAttrType = isDefined(attr.max) ? 'max' : attr.ngMax ? 'ngMax' : false;
1573+
if (maxAttrType) {
1574+
// Since ngMax doesn't set the max attr, the browser doesn't adjust the input value as setting max would
1575+
ctrl.$validators.max = isDefined(attr.max) && supportsRange && isDefined(validity.rangeOverflow) ?
15441576
function nativeMaxValidator(value) {
15451577
return !element[0].validity.rangeOverflow;
15461578
} :
@@ -1554,33 +1586,6 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
15541586
attr.$observe('max', maxChange);
15551587
}
15561588

1557-
var originalRender = ctrl.$render;
1558-
1559-
ctrl.$render = hasNativeValidation && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ?
1560-
//Browsers that implement range will set these values automatically, but reading the adjusted values after
1561-
//$render would cause the min / max validators to be applied with the wrong value
1562-
function rangeRender() {
1563-
console.log('rangeRendr');
1564-
if (ctrl.$isEmpty(ctrl.$viewValue)) {
1565-
var viewValue = minVal + (maxVal - minVal) / 2;
1566-
ctrl.$viewValue = viewValue;
1567-
originalRender();
1568-
ctrl.$setViewValue(viewValue);
1569-
console.log('elval', element.val());
1570-
} else if (ctrl.$viewValue > maxVal) {
1571-
ctrl.$viewValue = maxVal;
1572-
originalRender();
1573-
ctrl.$setViewValue(ctrl.$viewValue);
1574-
} else if (ctrl.$viewValue < minVal) {
1575-
ctrl.$viewValue = minVal;
1576-
originalRender();
1577-
ctrl.$setViewValue(ctrl.$viewValue);
1578-
} else {
1579-
originalRender();
1580-
}
1581-
} :
1582-
originalRender;
1583-
15841589
}
15851590

15861591
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {

test/ng/directive/inputSpec.js

+127-77
Original file line numberDiff line numberDiff line change
@@ -2827,6 +2827,9 @@ describe('input', function() {
28272827
scope = $rootScope;
28282828
});
28292829

2830+
var rangeTestEl = angular.element('<input type="range">');
2831+
var supportsRange = rangeTestEl[0].type === 'range';
2832+
28302833
it('should reset the model if view is invalid', function() {
28312834
var inputElm = helper.compileInput('<input type="range" ng-model="age"/>');
28322835

@@ -2865,7 +2868,7 @@ describe('input', function() {
28652868
expect(scope.age).toBe(50);
28662869
});
28672870

2868-
if (msie !== 9) {
2871+
if (supportsRange) {
28692872
// This behavior only applies to browsers that implement the range input, which do not
28702873
// allow to set a non-number value and will set the value of the input to 50 even when you
28712874
// change it directly on the element.
@@ -2920,7 +2923,7 @@ describe('input', function() {
29202923

29212924
describe('min', function() {
29222925

2923-
if (msie !== 9) {
2926+
if (supportsRange) {
29242927
// Browsers that implement range will never allow you to set the value < min values
29252928
it('should validate', function() {
29262929
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" min="10" />');
@@ -2935,49 +2938,55 @@ describe('input', function() {
29352938
expect(scope.value).toBe(100);
29362939
expect(scope.form.alias.$error.min).toBeFalsy();
29372940
});
2938-
}
29392941

2940-
it('should validate if "range" is not implemented', function() {
2941-
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" min="10" />');
2942-
2943-
// To test how browsers that don't implement range behave, we have to change the type, so
2944-
// that special layout and validation rules don't apply
2945-
inputElm[0].setAttribute('type', 'text');
2942+
it('should adjust the element and model value when the min value changes on-the-fly', function() {
2943+
scope.min = 10;
2944+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" min="{{min}}" />');
29462945

2947-
helper.changeInputValueTo('5');
2948-
expect(inputElm).toBeInvalid();
2949-
expect(scope.value).toBeUndefined();
2950-
expect(scope.form.alias.$error.min).toBeTruthy();
2946+
helper.changeInputValueTo('15');
2947+
expect(inputElm).toBeValid();
29512948

2952-
helper.changeInputValueTo('100');
2953-
expect(inputElm).toBeValid();
2954-
expect(scope.value).toBe(100);
2955-
expect(scope.form.alias.$error.min).toBeFalsy();
2956-
});
2949+
scope.min = 20;
2950+
scope.$digest();
2951+
expect(inputElm).toBeValid();
2952+
expect(scope.value).toBe(20);
2953+
expect(inputElm.val()).toBe('20');
29572954

2958-
it('should validate even if min value changes on-the-fly', function() {
2959-
scope.min = 10;
2960-
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" min="{{min}}" />');
2955+
scope.min = null;
2956+
scope.$digest();
2957+
expect(inputElm).toBeValid();
2958+
expect(scope.value).toBe(20);
2959+
expect(inputElm.val()).toBe('20');
29612960

2962-
helper.changeInputValueTo('15');
2963-
expect(inputElm).toBeValid();
2961+
scope.min = '15';
2962+
scope.$digest();
2963+
expect(inputElm).toBeValid();
2964+
expect(scope.value).toBe(20);
2965+
expect(inputElm.val()).toBe('20');
29642966

2965-
scope.min = 20;
2966-
scope.$digest();
2967-
expect(inputElm).toBeInvalid();
2967+
scope.min = 'abc';
2968+
scope.$digest();
2969+
expect(inputElm).toBeValid();
2970+
expect(scope.value).toBe(20);
2971+
expect(inputElm.val()).toBe('20');
2972+
});
29682973

2969-
scope.min = null;
2970-
scope.$digest();
2971-
expect(inputElm).toBeValid();
2974+
} else {
2975+
it('should validate if "range" is not implemented', function() {
2976+
// This will become type=text in browsers that don't support it
2977+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" min="10" />');
29722978

2973-
scope.min = '20';
2974-
scope.$digest();
2975-
expect(inputElm).toBeInvalid();
2979+
helper.changeInputValueTo('5');
2980+
expect(inputElm).toBeInvalid();
2981+
expect(scope.value).toBeUndefined();
2982+
expect(scope.form.alias.$error.min).toBeTruthy();
29762983

2977-
scope.min = 'abc';
2978-
scope.$digest();
2979-
expect(inputElm).toBeValid();
2980-
});
2984+
helper.changeInputValueTo('100');
2985+
expect(inputElm).toBeValid();
2986+
expect(scope.value).toBe(100);
2987+
expect(scope.form.alias.$error.min).toBeFalsy();
2988+
});
2989+
}
29812990
});
29822991

29832992
describe('ngMin', function() {
@@ -3024,7 +3033,7 @@ describe('input', function() {
30243033

30253034
describe('max', function() {
30263035

3027-
if (msie !== 9) {
3036+
if (supportsRange) {
30283037
// Browsers that implement range will never allow you to set the value > max value
30293038
it('should validate', function() {
30303039
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="10" />');
@@ -3048,49 +3057,87 @@ describe('input', function() {
30483057
expect(inputElm.val()).toBe('10');
30493058
expect(scope.value).toBe(10);
30503059
});
3051-
}
30523060

3053-
it('should validate if "range" is not implemented', function() {
3054-
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="10" />');
3061+
it('should adjust the element and model value if the max value changes on-the-fly', function() {
3062+
scope.max = 10;
3063+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="{{max}}" />');
30553064

3056-
// To test how browsers that don't implement range behave, we have to change the type, so
3057-
// that special layout and validation rules don't apply
3058-
inputElm[0].setAttribute('type', 'text');
3065+
helper.changeInputValueTo('5');
3066+
expect(inputElm).toBeValid();
30593067

3060-
helper.changeInputValueTo('20');
3061-
expect(inputElm).toBeInvalid();
3062-
expect(scope.value).toBeUndefined();
3063-
expect(scope.form.alias.$error.max).toBeTruthy();
3068+
scope.max = 0;
3069+
scope.$digest();
3070+
expect(inputElm).toBeValid();
3071+
expect(scope.value).toBe(0);
3072+
expect(inputElm.val()).toBe('0');
30643073

3065-
helper.changeInputValueTo('0');
3066-
expect(inputElm).toBeValid();
3067-
expect(scope.value).toBe(0);
3068-
expect(scope.form.alias.$error.max).toBeFalsy();
3069-
});
3074+
scope.max = null;
3075+
scope.$digest();
3076+
expect(inputElm).toBeValid();
3077+
expect(scope.value).toBe(0);
3078+
expect(inputElm.val()).toBe('0');
30703079

3071-
it('should validate even if max value changes on-the-fly', function() {
3072-
scope.max = 10;
3073-
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="{{max}}" />');
3080+
scope.max = '4';
3081+
scope.$digest();
3082+
expect(inputElm).toBeValid();
3083+
expect(scope.value).toBe(0);
3084+
expect(inputElm.val()).toBe('0');
30743085

3075-
helper.changeInputValueTo('5');
3076-
expect(inputElm).toBeValid();
3086+
scope.max = 'abc';
3087+
scope.$digest();
3088+
expect(inputElm).toBeValid();
3089+
expect(scope.value).toBe(0);
3090+
expect(inputElm.val()).toBe('0');
3091+
});
30773092

3078-
scope.max = 0;
3079-
scope.$digest();
3080-
expect(inputElm).toBeInvalid();
3093+
} else {
3094+
it('should validate if "range" is not implemented', function() {
3095+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="10" />');
30813096

3082-
scope.max = null;
3083-
scope.$digest();
3084-
expect(inputElm).toBeValid();
3097+
helper.changeInputValueTo('20');
3098+
expect(inputElm).toBeInvalid();
3099+
expect(scope.value).toBeUndefined();
3100+
expect(scope.form.alias.$error.max).toBeTruthy();
30853101

3086-
scope.max = '4';
3087-
scope.$digest();
3088-
expect(inputElm).toBeInvalid();
3102+
helper.changeInputValueTo('0');
3103+
expect(inputElm).toBeValid();
3104+
expect(scope.value).toBe(0);
3105+
expect(scope.form.alias.$error.max).toBeFalsy();
3106+
});
30893107

3090-
scope.max = 'abc';
3091-
scope.$digest();
3092-
expect(inputElm).toBeValid();
3093-
});
3108+
it('should validate even if the max value changes on-the-fly', function() {
3109+
scope.max = 10;
3110+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="{{max}}" />');
3111+
3112+
helper.changeInputValueTo('5');
3113+
expect(inputElm).toBeValid();
3114+
expect(scope.value).toBe(5);
3115+
3116+
scope.max = 0;
3117+
scope.$digest();
3118+
expect(inputElm).toBeInvalid();
3119+
expect(scope.value).toBe(5);
3120+
expect(inputElm.val()).toBe('5');
3121+
3122+
scope.max = null;
3123+
scope.$digest();
3124+
expect(inputElm).toBeValid();
3125+
expect(scope.value).toBe(5);
3126+
expect(inputElm.val()).toBe('5');
3127+
3128+
scope.max = '4';
3129+
scope.$digest();
3130+
expect(inputElm).toBeInvalid();
3131+
expect(scope.value).toBe(5);
3132+
expect(inputElm.val()).toBe('5');
3133+
3134+
scope.max = 'abc';
3135+
scope.$digest();
3136+
expect(inputElm).toBeValid();
3137+
expect(scope.value).toBe(5);
3138+
expect(inputElm.val()).toBe('5');
3139+
});
3140+
}
30943141
});
30953142

30963143
describe('ngMax', function() {
@@ -3132,16 +3179,19 @@ describe('input', function() {
31323179
scope.$digest();
31333180
expect(inputElm).toBeValid();
31343181
});
3182+
31353183
});
31363184

3137-
it('should set the correct initial view and model when min and max are specified', function() {
3138-
scope.max = 80;
3139-
scope.min = 40;
3140-
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="{{max}}" min="{{min}}" />');
3185+
if (supportsRange) {
3186+
it('should keep the initial default value when min and max are specified', function() {
3187+
scope.max = 80;
3188+
scope.min = 40;
3189+
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="{{max}}" min="{{min}}" />');
31413190

3142-
expect(inputElm.val()).toBe('60');
3143-
expect(scope.value).toBe(60);
3144-
});
3191+
expect(inputElm.val()).toBe('50');
3192+
expect(scope.value).toBe(50);
3193+
});
3194+
}
31453195

31463196
});
31473197

0 commit comments

Comments
 (0)