Menu
Documentation

Form validation

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.

First Name
Last Name
Email Address
Message
Show code
vue
<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>
Optional dependencies

Note that you can totally remove those dependencies, Tairo and Shuriken UI components won't be affected.