vee-validate icon indicating copy to clipboard operation
vee-validate copied to clipboard

Debounce validation on Field / useField

Open mkierdev opened this issue 2 years ago • 10 comments

It seems that v2 version had an option to debounce triggering validation on input. However I cannot find any way to debounce in vee-validate v4 (vue 3). It would be great if we could set time in miliseconds that would debounce validation. In this way, validation would wait till user finish writing (ex 500ms). Input blur solution does not satisfy my needs (we have to trigger validation while user has still focus in input, however it cannot be immediate).

mkierdev avatar Feb 23 '23 12:02 mkierdev

This was requested a few times and I think it is straightforward to add in vee-validate, so I will mark it as an enhancement and decide if it should be added or not.

Here is a quick snippet if you are using the composition API which is easiest to implement:

import { useField } from 'vee-validate';
import {  debounce } from 'lodash-es';
 
const { value, handleChange } = useField('some field', 'rules', {
  validateOnValueUpdate: false,
});

const debouncedValidation = debounce(validate, 400);
const onInput = (event) => {
  handleChange(event, false);
  debouncedValidation();
};
``

Then bind the `onInput` to your desired input event on your input, be it `@input` or `@update:modelValue` if it is a component.

logaretm avatar Mar 01 '23 22:03 logaretm

HI !
In 4.9.3, after the fix on handleChange (https://github.com/logaretm/vee-validate/issues/4251)not validating by default, the validation rules are called before the debounced handleChange method even with validateOnValueUpdate: false It means my field meta flag and errors get update immediatly and then, after the debounce delay get updated again.

Could you look into that ?

Here the sample code I use :

const {
  value: inputValue,
  errors,
  handleChange,
  handleBlur,
  meta,
} = useField<string>(name, rules, {
  initialValue: props.modelValue,
  label: labelInError.value ? labelInError : label,
  validateOnValueUpdate: false,
});

const debouncedHandleChange = debounce(v => {
  // Here it's called after 3sec but the validation already occured
  return handleChange(v);
}, 3000);
const localValue = computed<string>({
  get() {
    return inputValue.value;
  },
  set(value: string) {
    emit('update:modelValue', value);
    // validation occured here before the call to handle change
    debouncedHandleChange(value);
  },
});

Edit: checking with debugger my rule is called twice, before beeing called by handleChange

Edit2: rolling back to 4.7.4 fix the issues and i do not event need the validateOnValueUpdate part.

xontik avatar May 11 '23 09:05 xontik

@xontik sure, I will look into it later today. validateOnValueUpdate was always there, it worked differently.

Another possible reason is useField does listen for modelValue on your component so can you try setting syncVModel to false and see if it works for you?

logaretm avatar May 11 '23 11:05 logaretm

@logaretm yes searching in the code that's what i concluded, with both validateOnValueUpdate false and syncVModel false it works. Is it the recommended way to do it in 4.9 ?

xontik avatar May 11 '23 11:05 xontik

Depending on what your component is doing, each option controls a different aspect:

  • if you are syncing the input value directly with v-model=inputValue then you have to turn off validateOnValueUpdate.
  • if you are managing the v-model updates manually (and you are in the example by debouncing it) then yes, you should turn syncVModel off.

So in your case, maybe the combo is required but I'm wondering why it worked before without setting those options. Possibly if you added v-model support recently then it caused this, because useField detects if modelValue is defined as a prop or not to turn syncVModel on or off automatically if not specified.

logaretm avatar May 11 '23 16:05 logaretm

I'm also trying to add debounce feature to useField but I can't make it work because validation rules are always executed, regardless of validateOnValueUpdate or shouldValidate parameter from handleChange method.

I tried setting syncVModel=false just to see if it would work, but it didn't in my case. I also tried rolling back to 4.7.4 and I still can't make it work. Here's what I could figure out so far.

To be able to make debouncing feature available to anyone using my components I wrapped useField method with my own:

export function useField<TValue = unknown>(
  name: MaybeRef<string>,
  rules?: ValidationRules,
  opts?: Partial<ValidationFieldOptions<TValue>>): ValidationField<TValue> {

  const debounceOption = unref(opts?.debounce);
  let debounceMilliseconds = 0;

  if (debounceOption === true) {
    // Default debouncing is 250ms (when debounceOption = true)
    debounceMilliseconds = 250;
  } else if (Number.isFinite(debounceOption)) {
    // If debounceOption is a number, then we use that as the debouncing delay
    debounceMilliseconds = Number(debounceOption);
  }

  // If debounce was not configured, return the original useField from vee-validate
  if (debounceMilliseconds <= 0) {
    return veeUseField(name, rules, opts);
  }

  // If debounce was configured, then we force `validateOnValueUpdate = false`
  // since we'll be handling the call to validate ourselves.
  opts = opts ?? {};
  opts.validateOnValueUpdate = false;
  const field = veeUseField(name, rules, opts);

  // Setup the debouncedValidation callback
  const debouncedValidation = debounce(field.validate, debounceMilliseconds);

  // Create our custom handleChange method because we want to:
  // 1. Update the value without checking for validations rules (we use shouldValidate = false for that)
  // 2. Call debouncedValidation method that will take care of respecting the configure debouncing delay
  // and then checking validation rules and updating field.meta 
  const handleChange = field.handleChange;
  field.handleChange = (function(e: unknown, shouldValidate?: boolean) {
    handleChange(e, false);

    if (shouldValidate !== false) {
      debouncedValidation();
    }
  }).bind(field);

  return field;
}

The above does not work because, as soon as handleChange(e, false) is called, validation rules are executed and field.meta.valid is updated. This behavior happens even after I explicitly set validateOnValueUpdate = false and shouldValidate = false

I understand that #4251 had some issues with field.meta.valid not being updated when it should, but the current behavior is rather strange: we have 2 parameters (validateOnValueUpdate and shouldValidate) that say that validation shouldn't happen, but, regardless of what is set in those parameters, validation rules are executed anyway. I think its reasonable to expect that users will understand that field.meta won't be updated if they use shouldValidate = false.

As far as I could track this down, the issue is caused by this if statement. It seems to me that changing setValue method to:

function setValue(newValue: TValue, shouldValidate = true) {
  value.value = newValue;

  if (shouldValidate) {
    validateWithStateMutation();
  }
}

would make everything work as expected, without breaking the fix that was made for #4251. Am I missing something about this change? If I am, how can I add validation debouncing when validation is always executed real-time with no way to disable it?

reinaldoarrosi avatar Jun 04 '23 14:06 reinaldoarrosi

Also having trouble getting this working via the options API. validateName issues an async call and is fired on every input despite the validateOnModelUpdate and related props set to false. It does not matter if I have the onInput handler there or not

<Field
    name="name"
    v-model="form.name"
    :rules="validateName"
    :validateOnModelUpdate="false"
    :validateOnChange="false"
    :syncVModel="false"
    v-slot="{ field, handleChange, validate }"
>
    <input 
        v-bind="field" 
        type="text"
        id="input-name"
        required
        autocomplete="off"
        :label="$l('Name')"
        :placeholder="$l('Enter name')"
        @input="onInput($event, handleChange, validate)"
    >
</Field>

---

onInput(e, handleChange, validate) {
    handleChange(e, false)
    this.debounceValidate(validate)
},
debounceValidate(fn) {
    debounce(fn, 500)
}

Stetzon avatar Jul 12 '23 20:07 Stetzon

I have hit this issue i am using vee validate with element-plus and the input fields feels a bit laggy

I would love to open a PR for this

moaoa avatar Nov 14 '23 13:11 moaoa

You check the latest version ton's of things have changed.

xontik avatar Nov 15 '23 00:11 xontik

Implement debounce validation

  1. implement closure:
function debounceX() {
  let event = null
  const debounceValidation = debounce(() => handleChange(event), 2000)
  return ($event) => {
    event = $event
    debounceValidation()
  }
}
const onInput = debounceX()
  1. bind event
<input :value=value @input="onInput"/>

jakecodev avatar Jun 24 '24 05:06 jakecodev