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?
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:
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)
}
}
}
}
}
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.