Form validation is a way to check if the data entered by the user is valid. Tairo provides a simple way to validate forms using the vee-validate
and zod
libraries.
Vee-validate provides vue components and composable to control fields and form whereas zod is a library to define schemas. In the example below, we show how both work with Tairo with a simple user profile form.
<script setup lang="ts">
import { Field, useForm } from 'vee-validate'
import * as z from 'zod'
// This is the object that will contain the validation messages
const VALIDATION_TEXT = {
FIRSTNAME_REQUIRED: 'First name can\'t be empty',
LASTNAME_REQUIRED: 'Last name can\'t be empty',
EMAIL_REQUIRED: 'Enter a valid email address',
MESSAGE_REQUIRED: 'Message can\'t be empty',
}
// This is the Zod schema for the form input
// It's used to define the shape that the form data will have
const validationSchema = z.object({
firstName: z.string().min(1, VALIDATION_TEXT.FIRSTNAME_REQUIRED),
lastName: z.string().min(1, VALIDATION_TEXT.LASTNAME_REQUIRED),
email: z.email(VALIDATION_TEXT.EMAIL_REQUIRED),
message: z.string().min(1, VALIDATION_TEXT.MESSAGE_REQUIRED),
})
// Zod has a great infer method that will
// infer the shape of the schema into a TypeScript type
type FormInput = z.infer<typeof validationSchema>
const initialValues: FormInput = {
firstName: '',
lastName: '',
email: '',
message: '',
}
const {
handleSubmit,
isSubmitting,
setFieldError,
meta,
values,
resetForm,
} = useForm({
validationSchema,
initialValues,
})
const success = ref(false)
// Ask the user for confirmation before leaving the page if the form has unsaved changes
onBeforeRouteLeave(() => {
if (meta.value.dirty) {
// eslint-disable-next-line no-alert
return confirm('You have unsaved changes. Are you sure you want to leave?')
}
})
const toaster = useNuiToasts()
// This is where you would send the form data to the server
const onSubmit = handleSubmit(
async (_values) => {
success.value = false
// here you have access to the validated form values
// console.log('message-send-success', _values)
try {
// fake delay, this will make isSubmitting value to be true
await new Promise((resolve, reject) => {
if (values.firstName === 'Hanzo') {
// simulate a backend error
setTimeout(() => reject(new Error('Fake backend validation error')), 2000)
}
setTimeout(resolve, 4000)
})
toaster.add({
title: 'Message has been sent!',
icon: 'ph:check',
progress: true,
})
}
catch (error: any) {
// this will set the error on the form
if (error.message === 'Fake backend validation error') {
setFieldError('firstName', 'This name is not allowed')
document.documentElement.scrollTo({
top: 0,
behavior: 'smooth',
})
toaster.add({
title: 'Please review the errors in the form',
icon: 'lucide:alert-triangle',
})
}
return
}
resetForm()
document.documentElement.scrollTo({
top: 0,
behavior: 'smooth',
})
success.value = true
setTimeout(() => {
success.value = false
}, 3000)
},
(_error) => {
// this callback is optional and called only if the form has errors
success.value = false
// here you have access to the error
// console.log('message-send-error', _error)
// you can use it to scroll to the first error
document.documentElement.scrollTo({
top: 0,
behavior: 'smooth',
})
},
)
</script>
<template>
<div class="max-w-full">
<BaseCard class="p-6">
<form
action=""
method="POST"
novalidate
@submit.prevent="onSubmit"
>
<div class="grid grid-cols-12 gap-4">
<Field v-slot="{ field, errorMessage, handleChange, handleBlur }" name="firstName">
<BaseField
v-slot="{ inputAttrs, inputRef }"
label="First Name"
:state="errorMessage ? 'error' : isSubmitting ? 'loading' : 'idle'"
:error="errorMessage"
:disabled="isSubmitting"
class="col-span-12 sm:col-span-6"
required
>
<BaseInput
:ref="inputRef"
v-bind="inputAttrs"
placeholder="ex: John"
:model-value="field.value"
:error="errorMessage"
:disabled="isSubmitting"
type="text"
@update:model-value="handleChange"
@blur="handleBlur"
/>
</BaseField>
</Field>
<Field v-slot="{ field, errorMessage, handleChange, handleBlur }" name="lastName">
<BaseField
v-slot="{ inputAttrs, inputRef }"
label="Last Name"
:state="errorMessage ? 'error' : isSubmitting ? 'loading' : 'idle'"
:error="errorMessage"
:disabled="isSubmitting"
class="col-span-12 sm:col-span-6"
required
>
<BaseInput
:ref="inputRef"
v-bind="inputAttrs"
placeholder="ex: Doe"
:model-value="field.value"
:error="errorMessage"
:disabled="isSubmitting"
type="text"
@update:model-value="handleChange"
@blur="handleBlur"
/>
</BaseField>
</Field>
<Field v-slot="{ field, errorMessage, handleChange, handleBlur }" name="email">
<BaseField
v-slot="{ inputAttrs, inputRef }"
label="Email Address"
:state="errorMessage ? 'error' : isSubmitting ? 'loading' : 'idle'"
:error="errorMessage"
:disabled="isSubmitting"
class="col-span-12"
required
>
<BaseInput
:ref="inputRef"
v-bind="inputAttrs"
type="email"
placeholder="ex: johndoe@gmail.com"
:model-value="field.value"
:error="errorMessage"
:disabled="isSubmitting"
@update:model-value="handleChange"
@blur="handleBlur"
/>
</BaseField>
</Field>
<Field v-slot="{ field, errorMessage, handleChange, handleBlur }" name="message">
<BaseField
v-slot="{ inputAttrs, inputRef }"
label="Message"
:state="errorMessage ? 'error' : isSubmitting ? 'loading' : 'idle'"
:error="errorMessage"
:disabled="isSubmitting"
class="col-span-12"
required
>
<BaseTextarea
:ref="inputRef"
v-bind="inputAttrs"
placeholder="write your message..."
:model-value="field.value"
:error="errorMessage"
:disabled="isSubmitting"
@update:model-value="handleChange"
@blur="handleBlur"
/>
</BaseField>
</Field>
<div class="col-span-12">
<BaseButton
type="submit"
class="w-full"
color="primary"
:disabled="isSubmitting"
:loading="isSubmitting"
>
Send Message
</BaseButton>
</div>
</div>
</form>
</BaseCard>
</div>
</template>
Note that you can totally remove those dependencies, Tairo and Shuriken UI components won't be affected.