Skip to content

Instantly share code, notes, and snippets.

@tbl0605
Last active July 26, 2023 09:43
Show Gist options
  • Save tbl0605/7d18dcd28f80d29f5f1e9e659b64a42a to your computer and use it in GitHub Desktop.
Save tbl0605/7d18dcd28f80d29f5f1e9e659b64a42a to your computer and use it in GitHub Desktop.
Create VeeValidate rules that use asynchronous function calls for input validation (requires p-queue, Vue.js 2.x and VeeValidate 3.x)

Create VeeValidate rules that use asynchronous function calls for input validation (requires p-queue, Vue.js 2.x and VeeValidate 3.x)

VeeValidate is a great Validation Framework for Vue.js, but lacks examples and support for asynchronous function validation, even though it's a supported feature.

This example shows how a new "asyncCall" rule can use the result of an asynchronous function call to decide whether an input was successfully validated or not.

The asynchronous function (used as the "asyncFn" property of the "asyncCall" rule) must accept (as the only parameter) a value to be validated and must return an object that provides information on the validation status (see function "createAsyncQueueData").

The informations about the last validated value are cached in the "asyncQueue" property of the "asyncCall" rule.

Requires:

npm install vue@2 --save
npm install vee-validate@3 --save
npm install p-queue --save

Used as frontend toolkit:

npm install bootstrap@4 --save
<template>
<ValidationObserver v-slot="{ validate }">
<form class="needs-validation" novalidate @submit.stop.prevent="">
<div class="form-row align-items-start">
<div class="form-group col-8">
<ValidationProvider
:rules="asyncRule"
name="Album number"
:mode="blurMode"
v-slot="validatorData"
>
<!--<b-overlay :show="!!showOverlay" opacity="0.6" :no-fade="true">-->
<input
v-model.trim="chooseAlbumNumber"
type="text"
class="form-control mr-2"
:disabled="showOverlay"
placeholder="Choose your album number between 1 and 100"
/>
<!--</b-overlay>-->
<div
v-if="validatorData.errors.length > 0"
class="alert alert-danger m-0 mt-1 px-2 py-1"
>
{{ validatorData.errors[0] }}
</div>
</ValidationProvider>
</div>
<div class="form-group col">
<button
type="button"
class="btn btn-primary"
:disabled="showOverlay"
@click.prevent="handleSend(validate)"
>
Send
</button>
</div>
</div>
<div class="form-row">
<div v-if="albumTitle" class="alert alert-success">
Album title: {{ albumTitle }}
</div>
</div>
</form>
</ValidationObserver>
</template>
<script>
import { createAsyncQueue, createAsyncQueueData } from './rule.js';
const randomInt = max => Math.floor(Math.random() * max);
// Manually slow down a fetch request.
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
export default {
data: () => ({
asyncQueue: createAsyncQueue(),
// Component data:
chooseAlbumNumber: '',
albumTitle: '',
showOverlay: false
}),
computed: {
asyncRule() {
return {
required: true,
// The asyncCall rule needs 2 properties: an async function and an async queue cache.
asyncCall: {
asyncFn: this.getAsyncDataAndRefreshUI,
asyncQueue: this.asyncQueue
}
};
}
},
methods: {
blurMode() {
return {
on: ['blur']
};
},
async getAsyncData(asyncRuleValue) {
this.albumTitle = '';
return delay(randomInt(1000))
.then(() =>
fetch(`https://jsonplaceholder.typicode.com/albums/${asyncRuleValue}`)
)
.then(response =>
response.ok ? response.json() : Promise.reject('Invalid request')
)
.then(response => {
console.log('Successful fetch:', response);
this.albumTitle = response.title;
return createAsyncQueueData(asyncRuleValue);
})
.catch(
// NB: we don't really need to define an error function,
// the asyncCall rule would handle rejected promises gracefully anyway.
error => {
console.log('Failed fetch!');
// By storing the error associated with an album number here,
// we show the user the same error until he enters another album number.
return createAsyncQueueData(asyncRuleValue, false, error);
}
);
},
// We show and hide an overlay to make the asynchronous process visible to the user.
async getAsyncDataAndRefreshUI(asyncRuleValue) {
this.showOverlay = true;
return this.getAsyncData(asyncRuleValue).finally(() => {
this.showOverlay = false;
});
},
handleSend(validateFormAsyncFn) {
if (this.showOverlay) return;
validateFormAsyncFn().then(isFormValid => {
if (!isFormValid) return;
// Do here whatever you want to do after a successful validation...
console.log('Successful validation:', this.albumTitle);
});
}
}
};
</script>
import { extend as veeExtend } from 'vee-validate';
import PQueue from 'p-queue/dist';
import { markRaw } from 'vue';
veeExtend('asyncCall', {
params: ['asyncFn', 'asyncQueue', 'invalidMessage'],
async validate(
value,
{ asyncFn, asyncQueue, invalidMessage: defaultMessage }
) {
if (!asyncFn) {
return true;
}
if (value == null) {
if (defaultMessage) {
return String(defaultMessage);
}
return false;
}
var response = asyncQueue?.$lastResponse;
if (value !== response?.value) {
const wrapperFn = () =>
asyncQueue?.$queue
? asyncQueue.$queue.add(() => {
if (value === asyncQueue.$lastResponse?.value) {
return asyncQueue.$lastResponse;
}
return asyncFn(value).then(
_response =>
(asyncQueue.$lastResponse = markRaw({ ..._response })),
error =>
(asyncQueue.$lastResponse = markRaw({
value: undefined,
isValid: false,
invalidMessage:
error.response?.data || error.message || String(error)
}))
);
})
: asyncFn(value).catch(error => ({
value: undefined,
isValid: false,
invalidMessage:
error.response?.data || error.message || String(error)
}));
response = await wrapperFn();
}
const { isValid, invalidMessage: customMessage } = response;
if (isValid) {
return true;
}
if (customMessage) {
return String(customMessage);
}
if (defaultMessage) {
return String(defaultMessage);
}
return false;
},
message: 'The {_field_} field is not a valid value'
});
// Data to transmit to the asyncCall rule:
export const createAsyncQueue = () => ({
// Should not be reactive:
$lastResponse: markRaw({
value: undefined,
isValid: false,
invalidMessage: undefined
}),
// Should not be reactive:
$queue: markRaw(new PQueue({ concurrency: 1 }))
});
export const createAsyncQueueData = (
value,
isValid = true,
error = undefined
) => ({
value,
isValid,
invalidMessage: isValid
? undefined
: error.response?.data || error.message || String(error)
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment