Skip to content
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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open

Conversation

J-Sek
Copy link
Contributor

@J-Sek J-Sek commented May 27, 2024

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

  • "text" model becomes valid number or an empty string – thanks to typeof model.value === 'number' && !isNaN(...)
  • "exposed" model becomes valid number – thanks to custom aggressive parsing in 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:

<template>
  <v-app theme="dark">
    <v-container>
      <v-card class="mx-auto pa-6 my-4" style="max-width: 1200px">
        <v-row>
          <v-col cols="4">
            <small class="d-block mb-2">(clearable)</small>
            <v-number-input v-model="emptyValue" clearable />
            <code>value: {{ emptyValue }}</code>
          </v-col>
          <v-col cols="4">
            <small class="d-block mb-2">(:step=".25")</small>
            <v-number-input v-model="value1" :step=".25" />
            <code>value: {{ value1 }}</code>
          </v-col>
          <v-col cols="4">
            <small class="d-block mb-2">(:max="20" :min="-20")</small>
            <v-number-input v-model="value2" :max="20" :min="-20" />
            <div class="d-flex align-center justify-space-between">
              <code>value: {{ value2 }}</code>
              <div class="text-right">
                <v-btn @click="value2 = 5.125">Set to 5.125</v-btn>
                <v-btn @click="value2 = NaN">Set to NaN</v-btn>
              </div>
            </div>
          </v-col>
        </v-row>
      </v-card>
    </v-container>
  </v-app>
</template>

<script setup lang="ts">
  import { ref } from 'vue'

  const emptyValue = ref<number>(null)
  const value1 = ref<number>(15)
  const value2 = ref<number>(-10.25)
</script>

@MajesticPotatoe MajesticPotatoe added T: bug Functionality that does not work as intended/expected labs C: VNumberInput labels May 28, 2024
@J-Sek
Copy link
Contributor Author

J-Sek commented Jun 1, 2024

While writing tests I have noticed many edge cases related to .toString() producing totally different text that was used to parse the number. Initially I just wanted to avoid situation when component "misbehaves" in a quite critical manner - by interrupting user when he is in the middle of typing. But I learned about handful of other scenarios, most of which apply to pasting text from clipboard and raw typing.

Explanation of scenarios (I am not convinced E2E tests are self-explanatory)

  • typing - is immediately replaced with NaN (but it later works when you clear text and try again)
  • typing -0 is immediately replaced with 0 (I wanted to type -0.2 and I am basically unable to do it)
  • typing 0.0000001 is immediately replaced with 1e-7 and it only gets worse the higher the precision
  • once the mentioned problems were fixed I had to be very careful and disallow typing when one more digit would result in rounding the whole value (e.g. from 3.99... to 4) as I think it is not the intention of the user. He can still move cursor and type digits earlier, however effective digits are cut off as we prioritize digits from the left. I am OK with this tradeoff although it would be the best to display validation error stating that the field gets more than it can handle.
    • the core of this functionality is a little buried inside extractNumber(...). This function now removes digits until the condition cleanText !== Number(cleanText).toFixed(decimalDigits) is satisfied.

reasoning: accepting all digits them would affect user's input due to JS number format limitation. I would rather cut the text instead of replacing whole input with something unexpected.

The issue is related to precision. I think we would benefit from strict precision prop (i.e. fractional digits limit) that would be 0 by default. Feature request is already open #19898.

Along the way I was forced to rewrite getDecimals(...). As mentioned .toString() produces exponential notation after some point. New implementation just counts digits after the dot (if any are present) and includes value after e. I don't love the internal naming and lack of comments inside, but opted for brevity until someone complaints.

@J-Sek J-Sek marked this pull request as ready for review June 1, 2024 05:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C: VNumberInput labs T: bug Functionality that does not work as intended/expected
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug Report][3.6.5] (VNumberInput): interpretes negative numbers as NaN
2 participants