-
-
Notifications
You must be signed in to change notification settings - Fork 7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(VNumberInput): avoid showing NaN #19913
Conversation
While writing tests I have noticed many edge cases related to Explanation of scenarios (I am not convinced E2E tests are self-explanatory)
The issue is related to precision. I think we would benefit from strict Along the way I was forced to rewrite |
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.
Did an initial testing, when typing "-" the first time, it's ignored, but when keep typing the second time, it shows up.
Also, could you provide a simplified example that demonstrate the reason to justify introducing the "text" model for VTextField? I'm confused what's the problem it's trying to resolve, so far, transforming number input's v-model based on VTextField's onUpdate:modelValue
still feels enough.
Fix should be in onModelUpdate
where you could properly transform what's being typed from VTextField to the correct number
Could you share the code you've used? I cannot reproduce this behavior with the simplest case I can think of:
Input behaves correctly on Chrome and Firefox (on Linux). And there are test cases in the
There are only 2 ways to solve it that I see:
|
There is one more argument for the internal model split - we could support grouping and precision enforcement. I have already implemented this on top of this PR, so it is blocking me from posting PR for #19898. I hope we could deliver these fixes and functionalities before this component gets out of Labs :) |
Hey @yuwu9145, how can I help push this PR forward? This component will soon be out of labs and I think it would not look good if we let it show "NaN" or ship suboptimal UX (i.e. interruptions while typing). |
I'll be available to check it again tomorrow |
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.
+/- buttons become useless when displaying exponent number.
Instead, numberInput should only support certain max/min/fractionDigits ranges without converting to exponent format or loosing precision. If users want to use number in advanced level (e.g. bigInt), they should use string with v-text-field where advanced numbers can be processed by third-party libraries (e.g. decimal.js). This can be shown in the doc once we figure out what are the exact ranges it supports.
NumberInput should be kept simple and focus on BASIC NUMERICAL INPUTS:
- Only allow "numbers", "-" , "."
- "-", "." are only allowed once, AND "-" only allowed at the start
@johnleider could you confirm if this was the right direction for numberInput?
I think solution to resolve NaN issue should be around onModelUpdate
Internal model split opens the door for more advanced usages which I personally don't think suitable in number input
2c10687
to
5e0eb0d
Compare
I am trying to follow... we need to suppress Edit: previously listed downsides are not relevant All the relevant E2E tests still pass. |
f1b5f08
to
7069595
Compare
@yuwu9145, I have polished the PR a little after following your guide. I don't necessarily like the fact we are closing the doors for grouping separators and formatting locales, but maybe it's better to ship a scalper than swiss army knife (for the first version out of labs). |
{ text: '0.000010', expected: '0.00001' }, | ||
{ text: '0.012340', expected: '0.01234' }, | ||
{ text: '099999990.000000010', expected: '99999990.00000001' }, | ||
{ text: '99999999999999999999999999999', expected: '999999999999999' }, |
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 99999999999999999999999999999
becomes 999999999999999
?
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.
JS number has a quirk:
Precision loss when it's beyond MAX_SAFE_INTEGER
Perhaps our number input should only support between Number.MIN_SAFE_INTEGER
and Number.MAX_SAFE_INTEGER
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 the end goal is the same. We want to respect whatever user typed and avoid replacing already typed characters because of JS quirks. Using integer limit/range with inputs (that can be float values) seems like something that will leave tons of scenarios uncovered.
Suggested implementation shortens input string (removing characters from the end) until condition is satisfied
cleanText !== Number(cleanText).toFixed(decimalDigits)
Original question was about a very long number. You can paste or type to any example from documentation. Pasted it becomes 1e+29
, typed... well breaks in more spectacular way and can lead to NaN
, hence test case should be a great fit.
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.
Using integer limit/range with inputs (that can be float values) seems like something that will leave tons of scenarios uncovered.
The primary purpose of number-input is BASIC NUMERICAL INPUTS, for example, money/weight etc. It is designed to AVOID JS number quirks rather than RESOLVE all precision & unsafe JS quirks.
As per my understanding, there are three number challenges:
- Unsafe integer (
9007199254740993
->9007199254740992
) - useMAX_SAFE_INTEGER
&MIN_SAFE_INTEGER
range - JS floating error (
0.1 + 0.2 === 0.30000000000000004
) - almost impossible to resolve perfectly, so we make it clear in doc that we usetoFixed(Math.max(modelDecimals, stepDecimals))
to cope with it internally - Exponent format - not supported because they invalidate +/- buttons
These would cover the most of the use cases.
If users want to deal with crazy numbers (bigInt or long decimals), they should not use number-input but operate on STRING in text-field. These cases do not require +/- buttons anyway
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 always considered my implementation as a way to avoid JS quirks rather than resolving them :)
Scenario: For whatever reason user types (or pastes) 555555...
(very long string of digits). What would we like to show:
NaN
--- exposing JS quirk9007199254740991
(MAX_SAFE_INTEGER) --- exposing JS quirk5555555555555555
(digits from user, but only up until they can be displayed)
I am leaning towards the last and I still did not hear any argument against it. In my opinion it is not contrary to any of your points.
Imagine a developer that just placed v-number-input
in the form and never expected end-user to have a "heavy-finger" or a slightly broken keyboard (I am used to both). But in some of the scenarios, he might unnecessarily surprise end-user. I think the unsurprising result is the best outcome and it it's implementation is quite unsophisticated.
I also don't want to fight over it. It is much more important to close this PR and proceed to strict precision implementation. If you feel like Math.max(Math.min(Number(cleanText), Number.MAX_SAFE_INTEGER), Number.MIN_SAFE_INTEGER)
should replace the while-loop, I won't have anything against adjusting the PR.
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.
MAX_SAFE_INTEGER will be used as the max prop.max
, so any input bigger than it will be restricted/changed to MAX_SAFE_INTEGER.
describe('typing values', () => { | ||
it('should ignore invalid and duplicated characters', () => { | ||
const scenarios = [ | ||
{ text: '+=1234... abc e12', expected: '1234.12' }, |
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.
Invalid input should be enforced to empty undefined
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 wanted a test case for number extraction, but this example does not convey that we wish to accept numbers copy/pasted with any format (as long as decimal separator is dot). I will update this part.
Confirmed. |
Thanks for your time, but it isn't heading to the right direction: JS built-in numbers do have quirks, but what we should do is clearly figure out a safe range they can work within and document floating limitations to prevent confusing users. Therefore, there is no value in having extractNumber that performs custom string-to-number conversion. Additionally, a new text model isn't necessary as the beforeinput event works well for preprocessing and pre-validating input. I'm going to push forward #20211 for your reference |
After playing a bit with
I think the docs could include something like "this field supports numbers up to 14 digits long. Use explicit min/max and precision to avoid JS-related issues". |
Description
In order to avoid
NaN
we need to decouple "text" model for VTextField and the "exposed" model representing value for VNumberInput's consumer. I don't see it any other way and this approach served me many years in my custom wrapper arount VTextField back in Vuetify 2, so I am pretty sure it's the only way forward.Each time we sync between these two
typeof model.value === 'number' && !isNaN(...)
extractNumber(...)
Note: there are many ways to break this implementation, so we will need a bunch of test cases as a safety net for future changes. I plan on introducing them in the following days.
fixes #19798
Markup: