logaretm / vee-validate

✅ Painless Vue forms
https://vee-validate.logaretm.com/v4
MIT License
10.8k stars 1.26k forks source link

Debounce validation on Field / useField #4161

Closed mkierdev closed 1 week ago

mkierdev commented 1 year ago

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).

logaretm commented 1 year ago

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.
xontik commented 1 year ago

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.

logaretm commented 1 year ago

@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?

xontik commented 1 year ago

@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 ?

logaretm commented 1 year ago

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

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.

reinaldoarrosi commented 1 year ago

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?

Stetzon commented 1 year ago

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)
}
moaoa commented 11 months ago

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

xontik commented 11 months ago

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

jakecodev commented 4 months ago

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()
  2. bind event

    <input :value=value @input="onInput"/>