Vuelidate: async validation throttling/debouncing

Created on 24 Jan 2018  路  4Comments  路  Source: vuelidate/vuelidate

In your documentation regarding async validation you say:

If you need to throttle an async call, do it on your data change event, not on the validator itself.

What do you mean by that? I mean I can easily make throttled/debounced async calls upon @input event but how do I wire it back to vuelidate?

Did you possibly mean not wiring it back and using some additional computed value totally unrelated to vuelidate? But then I would loose those handy $v.$error and $v.$pending shortcuts :-/

Can you clarify that in docs maybe?

docs

Most helpful comment

@jacobg
Thanks for posting your solution, I used it as a guide and came up with the following in my project. I'm using Quasar and tried to use their "debounce" function but it returns another function and Vuelidate kept throwing an unable to determine async error - which turned out to be the clue for just returning the Promise itself and forgo Quasar's debounce. I had this doing a couple of other things but stripped them out for sake of brevity.

data () {
  chkUsernameAvailabilityTimer: null
},
validations: {
  login: {
    username: {
      required,
      isAvailable: function (value) {
        return new Promise((resolve) => {
          if (this.chkUsernameAvailabilityTimer) {
              clearTimeout(this.chkUsernameAvailabilityTimer)
              this.chkUsernameAvailabilityTimer = null
          }
          this.chkUsernameAvailabilityTimer = setTimeout(() => {
            this.$LoginService.chkUsernameAvailability(value)
              .then((response) => {
                if (response.data.available !== undefined) {
                  resolve(response.data.available)
                } else {
                  resolve(false)
                }
              })
              .catch(() => resolve(false))
          }, 500)
        }
      }
    }
  }
}

All 4 comments

The idea is that you should never use timers inside validators, simply treat them like computed values. What you have to defer is the input. The validation will run always when the value was changed. A good pattern is to use v-model.lazy.

You could potentially use a debounce inside validator too, but in that case it would have to be embedded inside the promise. In that case any data/state referenced from the component must be first accessed synchronously, not inside the timer callback. Otherwise the dependency will not be created and you risk that the validator will never run again.

Exactly the same principle holds for any computed value inside vue components.

I have a similar need, with the following specific requirements:

  • Any synchronous validations on the field occur immediately without debouncing
  • Even within an asynchronous validation, there must be a possibility to complete the validation synchronously immediately without debouncing. For instance, for a validation that makes a rest api call, there are several reasons why one wouldn't want to make the expensive API call: (a) the value is empty; (b) the value is the original saved and validated value; (c) the value fails the synchronous validations.

Here's the code I came up with:

// This is the wrapper function: validator is the actual validation function, and delay is in milliseconds.
// Note that the validator function accepts two parameters: the value and a debounce function to wrap
// the asynchronous validation.
function debounceAsyncValidator (validator, delay) {
  let currentTimer = null
  let currentPromiseReject = null

  function debounce () {
    return new Promise((resolve, reject) => {
      currentTimer = setTimeout(() => {
        currentTimer = null
        currentPromiseReject = null
        resolve()
      }, delay)
      currentPromiseReject = reject
    })
  }

  return function (value) {
    if (currentTimer) {
      currentPromiseReject(new Error('replaced'))
      clearTimeout(currentTimer)
      currentTimer = null
    }

    return validator.call(this, value, debounce)
  }
}
// Example component:
export default {
  props: ['id', 'username'],
  data: {
    editedUsername: null
  },
  validations () {
    return {
        editedUsername: {
          required,
          minLength: minLength(3),
          maxLength: maxLength(20),
          isUnique: debounceAsyncValidator(function (value, debounce) {
            // synchronous validations
            if (!value) return true
            if (value === this.username) return true
            if (!this.$v.editedUsername.minLength || !this.$v.editedUsername.maxLength) return true
            // capture all component state synchronously
            const userId = this.id
            return debounce()
              .then(() => userService.lookupByUsername(value))
              .then(user => {
                const isUnique = !user || user.id === userId
                return isUnique
              })
              .catch(() => {
                // could be caused by either rest api failure or by debounce
                return false
              })
          }, 500)
        }
    }
  }
}

What do you think about this solution?

Is this solution worth incorporating into the project, either as an enhancement to the asynchronous validation feature, or at least as a documentation example?

@jacobg
thx for this, been searching and trying for hours, any debounces i tried in the component works at first try but fails for preceding inputs. yours is the only one that debounced properly.

@jacobg
Thanks for posting your solution, I used it as a guide and came up with the following in my project. I'm using Quasar and tried to use their "debounce" function but it returns another function and Vuelidate kept throwing an unable to determine async error - which turned out to be the clue for just returning the Promise itself and forgo Quasar's debounce. I had this doing a couple of other things but stripped them out for sake of brevity.

data () {
  chkUsernameAvailabilityTimer: null
},
validations: {
  login: {
    username: {
      required,
      isAvailable: function (value) {
        return new Promise((resolve) => {
          if (this.chkUsernameAvailabilityTimer) {
              clearTimeout(this.chkUsernameAvailabilityTimer)
              this.chkUsernameAvailabilityTimer = null
          }
          this.chkUsernameAvailabilityTimer = setTimeout(() => {
            this.$LoginService.chkUsernameAvailability(value)
              .then((response) => {
                if (response.data.available !== undefined) {
                  resolve(response.data.available)
                } else {
                  resolve(false)
                }
              })
              .catch(() => resolve(false))
          }, 500)
        }
      }
    }
  }
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

ecmel picture ecmel  路  3Comments

hackuun picture hackuun  路  4Comments

jess8bit picture jess8bit  路  3Comments

jfis picture jfis  路  3Comments

kharysharpe picture kharysharpe  路  4Comments