-
Notifications
You must be signed in to change notification settings - Fork 27.4k
feat(input): add support for input[type=range] #14870
Conversation
We found a Contributor License Agreement for you (the sender of this pull request) and all commit authors, but as best as we can tell these commits were authored by someone else. If that's the case, please add them to this pull request and have them confirm that they're okay with these commits being contributed to Google. If we're mistaken and you did author these commits, just reply here to confirm. |
1 similar comment
We found a Contributor License Agreement for you (the sender of this pull request) and all commit authors, but as best as we can tell these commits were authored by someone else. If that's the case, please add them to this pull request and have them confirm that they're okay with these commits being contributed to Google. If we're mistaken and you did author these commits, just reply here to confirm. |
f104808
to
828f286
Compare
The biggest issue is that browser that implement range set the element value to a valid value if you try to set an invalid value. Two cases: you try to set a non-number: input value is set to 50. You try to set a value that exceeds max or is lower than min: value is set to max / min value. This is problematic for two reasons:
In the PR, I have changed it so that we update the model after the browser has changed the value. As an alternative, I though of creating a validator that adds a misMatch error the the control that is set whenever the browsers updates the value and it is now distinct from the model. |
BTW, it doesn't set it to 50, it sets it to |
|
||
ctrl.$render = function() { | ||
if (ctrl.$isEmpty(ctrl.$viewValue)) { | ||
ctrl.$viewValue = '50'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't be hard-coded to 50. It should be (max - min) / 2
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, i case it wasn't clear, 50
is the result of (max - min) / 2
(max + min) / 2
for the default min (0
) and max (100
) values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, the PR is currently missing the min max handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the correct formula is actually min + ((max - min) / 2)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I meant (max + min) / 2
(which is equivalent to your formula).
I will correct it to avoid confusion.
I think we need to better handle the situation where |
That's true. I noticed that but haven't included it yet. So what do you in general think about updating the model when the browser changes the values? |
Yeah, I think we should read the viewValue every time the |
When a step is set, the browser rounds non-matching values to the nearest step ... |
So, how does affect |
That's for us to decide. We already want to set undefined / null models to 50, so we could also propagate the step-adjusted value back to the model. But I'm not sure if this isn't unexpected behavior. For example, you are loading a legacy value from a database that doesn't match your step value. Now you want the user to correct this value. The first problem is that the input will be auto-set to a fitting value (and the model too if we implement it). Now the user does not know that "his" value has been changed, and the application doesn't know if the user actually wanted to set this value. The input is also not marked as invalid, so the user can submit the form etc. and basically send a value to the DB that he didn't even know changed. Another option for us might be to introduce a "mismatch" error that is set on the model controller when the input value is changed after it is set from the model. |
It is a problematic situation either way. For example if we leave the original (off-step) modelValue, the field will be invalid, but a valid value will be shown to the user. And in order to set the modelValue to the corrected, nearest step value, they will have to move the knob to a different value and back, so we can detect a viewValue change and parse that. Things like that make me think that supporting range is going to cause more headaches than it will solve problems. Users are probably better of using a non-native slider/range input - e.g. as an independant module or from a UI framework (such as ngMaterial). 😞 |
But for simple cases, binding to range is almost as simple as binding to number. So I'm not sure we should bail out of this simply because there are some edge cases. |
So from reading the HTML5 specs, I think it is fair to say that range is “special" In the sense that it can _never_ hold an invalid value That means that we must always update the ngModel $viewValue to a valid value however it is updated (via user interaction or programmatically). If we agree that this is the case, and document it clearly, then I think we could go ahead with this directive... I think that we should also, when reacting to model changes, simply compute the appropriate valid value that would work for the control and update the model explicitly ourselves So in the case of a model that is out of “step”, if we have this range
and we try to set |
@@ -1530,6 +1530,11 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) { | |||
|
|||
// TODO(matsko): implement validateLater to reduce number of validations | |||
if (minAttrType === 'min') { | |||
var elVal = element.val(); | |||
// IE11 doesn't set the el val correctly if the maxVal is less than the element value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment doesn’t match the surrounding code. Bad copy-paste from line 1572?
} : | ||
// 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why calling $isEmpty
with the viewValue
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because we are validating the $viewValue. I think dates use the modelValue, but this is something we should change, imo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we almost always call $isEmpty
with the modelValue
(regardless of what value we are actually validating). Imo it makes sense to (a) be consistent what we call $isEmpty
with and (b) always call it with modelValue
(which can be $modelValue
or $$rawModelValue
), in order to account for custom parsers.
For example, an input[range]
's viewValue can never be empty, but I might want to have a custom parser that converts '0'
to null
and I want this to be considered empty.
It is debatable whether we should be validating the $viewValue
or the $modelValue
. I think there are valid usecases for both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, there are definitely both approaches in the code base. But for example the most recent change that touches this, are the 'ng-empty', and 'ng-not-empty' classes, which are set based on the result of $ctrl.isEmpty(viewValue)
. https://github.com/angular/angular.js/blob/master/src/ng/directive/ngModel.js#L323
I'd rather make all built-in validators use isEmpty(viewValue), because technically all validators validate what the user has entered, even if we parse it a Date or Number.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't thought that through tbh, but I see merit in both cases. Fwiw, the initial intent seems to be to call it against the $modelValue
, as indicated by the default implementation (which checks for undefined
, null
, NaN
- values that can hardly appeat as $viewValue
s).
Imo, the way someone decides to "interpret" the value - i.e. through custom parsers - should be taken into account if possible. But, I understand there are "technical" difficulties with this approach, so checking against the $viewValue
might be preferrable.
Most importantly, we should be consistent (if we find ourselves needing to call $isEmpty
with both values, it is probably an indication that we need to rethink the API - maybe have two separate methods) and communicate it well through docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, the required validator calls $isEmpty with the viewValue:
angular.js/src/ng/directive/validators.js
Line 70 in cd2f6d9
return !attr.required || !ctrl.$isEmpty(viewValue); |
Though I agree that it's a problem in general that validators sometimes validate the viewValue and sometimes the modelValue.
I just found out that you can make an I think we need some tests with |
I left step out, because I wanted to get it to work with min/max first. I'd like to work on step for number/range after this is merged |
I have mixed feelings about this 😃 I would rather not release Btw, here is a browser inconsistency (probably a Chrome bug) I found out: According to (my understanding of) the spec, browsers should automatically adjust the <input type="range" step="10" /> console.log(input.step) // --> '10'
console.log(input.value) // --> '50'
console.log(input.validity.valid) // --> true
input.step = '15';
console.log(input.step) // --> '15'
console.log(input.value) // --> '45'
console.log(input.validity.valid) // --> true
input.step = 20;
console.log(input.step) // --> '20'
console.log(input.value) // --> Firefox: '40' | Chrome: '45'
console.log(input.validity.valid) // --> Firefox: true | Chrome: false (stepMismatch) |
Okay, step support should be in the same release as the basic support. But it doesn't have to be in the same commit. |
} else { | ||
// TODO(matsko): implement validateLater to reduce number of validations | ||
ctrl.$validate(); | ||
} | ||
} | ||
|
||
var minAttrType = isDefined(attr.min) ? 'min' : attr.ngMin ? 'ngMin' : false; | ||
var minAttrType = isDefined(attr.ngMin) ? 'ngMin' : isDefined(attr.min) ? 'min' : false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same change is necessary for max
below.
LGTM (the travis failures are not related to this PR and have been fixed on master). |
4c05f61
to
1001d76
Compare
Thanks to @cironunes for the initial implementation in angular#9715 Adds support for binding to input[range] with the following behavior / features: - Like input[number], it requires the model to be a Number, and will set the model to a Number - it supports setting the min/max values via the min/max and ngMin/ngMax attributes - it follows the browser behavior of never allowing an invalid value. That means, when the browser converts an invalid value (empty: null, undefined, false ..., out of bounds: greater than max, less than min) to a valid value, the input will in turn set the model to this new valid value via $setViewValue. -- this means a range input will never be required and never have a non-Number model value, once the ngModel directive is initialized. -- this behavior is supported when the model changes and when the min/max attributes change in a way that prompts the browser to update the input value. -- ngMin / ngMax do not prompt the browser to update the values, as they don't set the attribute values. Instead, they will set the min / max errors when appropriate - browsers that do not support input[range] (IE9) handle the input like a number input (with validation etc.) Closes angular#5892 Closes angular#9715 Close angular#14870
Thanks to @cironunes for the initial implementation in angular#9715 Adds support for binding to input[range] with the following behavior / features: - Like input[number], it requires the model to be a Number, and will set the model to a Number - it supports setting the min/max values via the min/max and ngMin/ngMax attributes - it follows the browser behavior of never allowing an invalid value. That means, when the browser converts an invalid value (empty: null, undefined, false ..., out of bounds: greater than max, less than min) to a valid value, the input will in turn set the model to this new valid value via $setViewValue. -- this means a range input will never be required and never have a non-Number model value, once the ngModel directive is initialized. -- this behavior is supported when the model changes and when the min/max attributes change in a way that prompts the browser to update the input value. -- ngMin / ngMax do not prompt the browser to update the values, as they don't set the attribute values. Instead, they will set the min / max errors when appropriate - browsers that do not support input[range] (IE9) handle the input like a number input (with validation etc.) Closes angular#5892 Closes angular#9715 Close angular#14870
1001d76
to
807622e
Compare
CLAs look good, thanks! |
1 similar comment
CLAs look good, thanks! |
Thanks for the review, @gkalpak ! And thanks again @cironunes for the initial implementation. |
Thanks to @cironunes for the initial implementation in #9715 Adds support for binding to input[range] with the following behavior / features: - Like input[number], it requires the model to be a Number, and will set the model to a Number - it supports setting the min/max values via the min/max and ngMin/ngMax attributes - it follows the browser behavior of never allowing an invalid value. That means, when the browser converts an invalid value (empty: null, undefined, false ..., out of bounds: greater than max, less than min) to a valid value, the input will in turn set the model to this new valid value via $setViewValue. -- this means a range input will never be required and never have a non-Number model value, once the ngModel directive is initialized. -- this behavior is supported when the model changes and when the min/max attributes change in a way that prompts the browser to update the input value. -- ngMin / ngMax do not prompt the browser to update the values, as they don't set the attribute values. Instead, they will set the min / max errors when appropriate - browsers that do not support input[range] (IE9) handle the input like a number input (with validation etc.) Closes #5892 Closes #9715 Close #14870
Maybe I misunderstand, but even if it changes the behaviour, this PR doesnot fix the issue #6726.
Before the commit 9130166 it gives :
After it gives :
The 2 views of value are now consistent, but the value and the slider should be at 150 as it is in the range [50,150]. I tried to change order of min, max, value, but it seems to have no effects. |
@mpromonet Or it's a bug ... (it's a bug). Thanks for testing and reporting. |
What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
feature
Please check if the PR fulfills these requirements
Other information:
the input[type=range] behavior is the same of an input[type=number]
with min=0, max=100 and step=1 as defaults
Closes #5892
Closes #9715