How can I clear validation-on-blur errors when I use an onBlur trigger?

Viewed 3750

I am developing a v-text-field that will allow simple math expressions like 1+1 to result in 2 when you either press enter or blur (lose focus) from the text field. This is my code so far.

<template>
  <v-text-field
    :rules="rules"
    @blur="compute"
    @keydown.enter="compute"
    v-model="localValue"
    validate-on-blur
  />
</template>

<script>
import { create, all } from 'mathjs'
const math = create(all)

export default {
    name: "NumericTextField",
    props: ["value"],
    data () {
        return {
            localValue: this.value,
        }
    },
    watch: {
        value (v) {
            this.localValue = v
        },
    },
    methods: {
        compute () {
            try {
                const result = math.evaluate(this.localValue)
                this.$emit("input", result)
            } catch (e) {
                console.log("compute error", e)
            }
        },
    },
    computed: {
        rules () {
                return [v => /^([0-9]*[.])?[0-9]+$/.test(v) || "Must be numeric."]
        },
    },
}
</script>

There is one odd behavior in that when I blur the text field, by let's say tabbing away to the next text field, validate-on-blur takes effect and so does the @blur to compute the result. Validation wins this race I am left with a violation error showing up while the result quickly changes to a numeric answer which should be valid. Concretely, I type 1+1 (which is not numeric since there's a plus sign in there). When I press tab, the text box is changed to 2 (which is great) and also a validation error appears complaining about the fact that 1+1 is not numeric. That's not desired. If the result is numeric I'd like the validation to take that into account. But I don't know how to control this race condtion.

I have tried, and had success latching on to the form of the page, and explicitly revalidating all forms on the field by giving my form a ref and then calling this.$refs.myForm.validate() at the end of compute. But that makes my component not really very reusable since it needs to know about the parent form. Furthermore, it's reevaluating all form validations on blur which I think could get expensive if there are a lot of these one one page.

Is there a way to control the race condition? Or a work around?

1 Answers

I don't think there is any way to prevent Vuetify's validate-on-blur from running first. So here is how you can do a sneaky workaround without it. Your template will look like this:

<template>
  <v-text-field
    :rules="rules"
    @blur="validate"
    @keydown.enter="validate"
    @focus="clearRules"
    @input="clearRules"
    v-model="localValue"
  />
</template>

Since you won't use validate-on-blur, you need to manually code the setting and clearing of rules, otherwise they'll be applied immediately on input. Define a data property with an empty array:

data() {
  return {
    rules: []
  }
},

The rules will be cleared every time the field is focused or input occurs. All that's needed now is to run a validate method on every blur/keydown which will: 1) set the rules, and 2) compute the result:

validate() {
  this.setRules();
  this.compute();
},
setRules() {
  this.rules = [v => /^([0-9]*[.])?[0-9]+$/.test(v) || "Must be numeric."];
},
compute() {
  try {
    const result = math.evaluate(this.localValue)
    this.$emit("input", result)
  } catch(e) {
    console.log("compute error", e)
  }
},

Here is the working demo (I used eval instead of your mathjs library):

new Vue({
  el: '#app',
  vuetify: new Vuetify(),
  data() {
    return {
      // ...
      rules: []
    }
  },
  methods: {
    validate() {
      this.setRules();
      this.compute();
    },
    setRules() {
      this.rules = [v => /^([0-9]*[.])?[0-9]+$/.test(v) || "Must be numeric."];
    },
    compute() {
      try {
        this.localValue = eval(this.localValue);
      } catch(e) {
        // console.log("compute error", e)
      }
    },
    clearRules() {
      this.rules = [];
    }
  }
})
<div id="app">
  <v-app>
    <v-text-field
      :rules="rules"
      @blur="validate"
      @keydown.enter="validate"
      @focus="clearRules"
      @input="clearRules"
      v-model="localValue"
      ></v-text-field>
  </v-app>
</div>



<!-- LIBRARIES -->
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>

Idea taken from here and tailored to work with blur.

Related