Form inputs don't get validated while typing (@input), only @blur works

1.2k views Asked by At

I'm using a combobox with input data validation (Vuelidate):

<template>
    <v-combobox
        clearable
        v-model="surname"
        :items="commonSurnames"
        label="Surname"
        placeholder="Type in the surname"
        class="pt-5 pb-5"
        :error-messages="surnameErrors"
        @input="$v.surname.$touch()"
        @blur="$v.surname.$touch()">
    </v-combobox>
</template>

<script>
import { validationMixin } from 'vuelidate'
import { required, maxLength } from 'vuelidate/lib/validators'

export default {
    mixins: [validationMixin],
    validations: {
        surname: {
            required,
            maxLength: maxLength(30),
            validSurname(surname) {
                return (
                    /^[a-zA-Z]-?*.+$/.test(surname)
                )
            }
        },
    name: 'Surnames',

    data() {
        return {
            surname: '',
            [...]
    },
    methods: {
        [...]
    },
    computed: {
        surnameErrors() {
            const errors = []
            if (!this.$v.surname.$dirty) return errors
            !this.$v.surname.validSurname && errors.push('Format must be like: Smith or Smith-Wesson')
            !this.$v.surname.maxLength && errors.push('Surname must be at most 30 characters long.')
            !this.$v.surname.required && errors.push('Surname is required.')
            return errors
    }
}
</script>

Versions of components:

  "dependencies": {
    "@vue/compiler-sfc": "^3.0.0",
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "vuelidate": "^0.7.5",
    "vuetify": "^2.2.11"
  },

I suppose I did everything as in the Vuetify Documentation, but my form gets validated a bit differently than what is there, in the docs: I can exceed the limit of 30 characters without being notified about it while typing. I only know it when the input loses focus. The same situation with RegEx validation: any value is accepted without error notification. If the value is not valid, I get notified when leaving the input field.

Did I miss something when copying the example from the docs, or the @input listener works incorrectly? Or is it that v-combobox can't be validated this way?

2

There are 2 answers

1
tony19 On

v-combobox emits change or input events only when one of its valid values from the dropdown are selected. The text entered in the combobox is actually search input, used to lookup a valid value; and that entry is bound to v-combobox's search-input prop.

The v-combobox API lists update:search-input as an event that is fired when the search input changes, so you could use that to bind the validation handler:

<!-- BEFORE: -->
<v-combobox @input="$v.surname.$touch()">

<!-- AFTER: -->
<v-combobox @update:search-input="$v.surname.$touch()">
0
Johnny Cage On

I used to have a component wrapper around v-combobox, v-autocomplete and v-select (yes, single multi-purpose wrapper). And I would recommend using the code below in a wrapper to avoid copy pasting the fix/workaround.

In general workaround required lookup into Vuetify sources. This led to manipulating errorBucket and valid in @update:search-input listener.

Additionally you might want to emit 'input'. It requires one small tweak to suppress propagation into v-combobox value, since it breaks autocomplete behavior otherwise. Hence if (this.search) { return; }

Full example: Codepen

Vue.component(
  'my-combobox', {
  template: `
      <v-combobox
         ref="combobox"
         outlined dense
         v-model="selection"
         v-bind="{ ...$attrs, items, rules: validationRules }"
         v-on="$listeners"
         @update:search-input="onSearch"
         @focus="touched = true"></v-combobox>
      `,
  props: {
    value: String,
    items: Array,
    required: Boolean,
    rules: Array,
  },
  data() {
    return {
      selection: null,
      search: null,
      touched: false,
    };
  },
  computed: {
    validationRules() {
      return [
        ...(this.rules || []),
        (v) => !this.required || (v?.length ?? 0) > 0 || 'Value is required',
      ];
    },
  },
  methods: {
    onSearch(v) {
      if (!this.touched) return;
      this.search = v;
      const $self = this.$refs.combobox;
      $self.errorBucket = this.validationRules.filter(f => f(v) !== true);
      $self.valid = $self.errorBucket.length === 0;
      this.$emit('input', v);
    },
  },
  watch: {
    selection: {
      handler(v) {
        this.$emit('input', v);
      },
    },
    value: {
      immediate: true,
      handler(v) {
        if (this.search) { return; }
        this.selection = v;
      },
    },
  },
});