Intro to VeeValidate

January 24, 2025

1,379 words

Post contents

Forms are a critical part of many web apps; Vue apps included. While Vue has a large home-grown ecosystem of tools, Vue does not have an official complex form library. Luckily for us, vee-validate aims to be a good fit for any form requirements our Vue apps may have.

Here's a simple form using vee-validate:

<!-- FormComp.vue --><script setup>import { Form as VForm, Field as VField } from 'vee-validate'function onSubmit(values) {  console.log(values)}</script><template>  <VForm @submit="onSubmit">    <div>      <label>        Name        <VField name="name" value="" />      </label>    </div>    <div>      <label>        Email        <VField name="email" value="" />      </label>    </div>    <button type="submit">Submit</button>  </VForm></template>

Here, we'll use the import {Something as SomethingElse} syntax in order to avoid namespace collision (where two things are named the same, and the compiler has challenges figuring out which is which) with HTML's default form element.

Input States

VForm and VField also have the ability to access the component's inner state using v-slot. This enables us to access if a form or field is:

  • Touched: if the user has clicked or tabbed into a field, regardless of if the user has put any value in.
  • Dirty: if data has been placed into a field
<!-- FormComp.vue --><script setup>import { ref } from 'vue'import { Form as VForm, Field as VField } from 'vee-validate'const pending = ref(false)const submitted = ref(false)function onSubmit(values) {  submitted.value = true  pending.value = true  sendToServer(values).then(() => {    pending.value = false  })}// Pretend this is calling to a serverfunction sendToServer(formData) {  // Wait 4 seconds, then resolve promise  return new Promise((resolve) => setTimeout(() => resolve(0), 4000))}</script><template>  <VForm @submit="onSubmit" v-slot="{ meta }">    <div>      <label>        Name        <VField name="name" value="" v-slot="{ field, meta }">          <input v-bind="field" />          <p v-if="meta.dirty">Field is dirty</p>          <p v-if="meta.touched">Field has been touched</p>        </VField>      </label>    </div>    <div>      <label>        Disabled field        <VField disabled name="email" value="" />      </label>    </div>    <p v-if="meta.dirty">Form is dirty</p>    <p v-if="meta.touched">Form has been touched</p>    <p v-if="submitted">Form submitted</p>    <p v-if="pending">Form is pending</p>    <button type="submit">Submit</button>  </VForm></template>

Form Arrays

In many forms, we need to track a list of fields.

An example of this could be keeping track of a list of users you'd want to share a file with.

To do this, you're able to use v-for to iterate through each user index and use said index to alias the name property of v-field to access a user's information:

<!-- FormComp.vue --><script setup>import { ref } from 'vue'import { Form as VForm, Field as VField, FieldArray } from 'vee-validate'const initialValues = { users: [{ name: '', id: 0 }] }const id = ref(1)function onSubmit(values) {  console.log(values)}</script><template>  <div>    <h1>Friend List</h1>    <VForm @submit="onSubmit" :initial-values="initialValues">      <FieldArray name="users" key-path="id" v-slot="{ fields, push, remove }">        <div v-for="(field, idx) in fields" :key="field.key">          <label>            Name            <VField :name="'users[' + idx + '].name'" />          </label>          <button type="button" @click="remove(idx)">Remove User</button>        </div>        <button type="button" @click="push({ name: '', id: ++id })">Add User</button>      </FieldArray>      <button type="submit">Submit</button>    </VForm>  </div></template>

Our usage of key-path with the id to track which user is which is worth highlighting here. Because we're now using an array, we need a unique ID for each user, otherwise we might run into issues with Vue's ability to figure out which user is which when adding a new field.

Form Validation

While handling forms is great and all - it's form validation where things get tricky in most apps. The ability to prevent users from submitting and showing error messages is a critical component of most all forms.

Luckily, vee-validate allows you to pass a rules parameter that's a function. If the function returns anything other than true, it will display the value as the error in the ErrorMessage component.

<!-- FormComp.vue --><script setup>import { ref } from 'vue'import { Form as VForm, Field as VField, ErrorMessage } from 'vee-validate'function onSubmit(values) {  console.log(values)}function required(value) {  // Validation failed!  if (!value) return 'This field is required'  // Validation passed!  return true}</script><template>  <VForm @submit="onSubmit">    <div>      <label>        Name        <VField name="name" value="" :rules="required" />      </label>    </div>    <div>      <ErrorMessage name="name" />    </div>    <button type="submit">Submit</button>  </VForm></template>

Complex Data Schema

Instead of writing our own functions to validate user input, let's instead use a library that can do that validation for us.

yup is a library that allows us to do "schema" based validation. A schema is simply another way of saying "a set of rules that should be followed". In this case, we want Yup to make sure that the user's inputs match the rules we set up in Yup's validation.

We can then pass that Yup schema into vee-validate's VForm validationSchema property.

<!-- FormComp.vue --><script setup>import { Form as VForm, Field as VField, ErrorMessage } from 'vee-validate'import * as yup from 'yup'const formSchema = yup.object().shape({  name: yup.string().required(),})function onSubmit(values) {  console.log(values)}</script><template>  <VForm @submit="onSubmit" :validationSchema="formSchema">    <div>      <label>        Name        <VField name="name" value="" />      </label>    </div>    <div>      <ErrorMessage name="name" />    </div>    <button type="submit">Submit</button>  </VForm></template>

By default, Yup will attempt to figure out the error message it should show based on the schema and the user's input. However, we're able to change the error message displayed by Yup with the following:

const formSchema = yup.object().shape({  name: yup.string().required("You must input a name of the user to share with."),});

Validation Types

Marking a field as required is far from the only type of form validation. While there are any number of items, there

  • Minimum string length
  • Maximum string length
  • Two inputs match each other
  • Match a regex

Here's a playground where we demonstrate a form that validates all of those examples:

<!-- FormComp.vue --><script setup>import { Form as VForm, Field as VField, ErrorMessage } from 'vee-validate'import * as yup from 'yup'const formSchema = yup.object().shape({  minLenStr: yup.string().min(3),  maxLenStr: yup.string().max(3),  regex: yup.string().matches(/hello|hi/i),  pass: yup.string(),  confirm: yup.string().oneOf([yup.ref('pass'), null], 'Must match "password" field value'),})function onSubmit(values) {  console.log(values)}</script><template>  <VForm @submit="onSubmit" :validationSchema="formSchema">    <div>      <label>        Minimum Length String (3)        <VField name="minLenStr" value="" />      </label>    </div>    <div>      <ErrorMessage name="minLenStr" />    </div>    <div>      <label>        Maximum Length String (3)        <VField name="maxLenStr" value="" />      </label>    </div>    <div>      <ErrorMessage name="maxLenStr" />    </div>    <div>      <label>        Regex        <VField name="regex" value="" />      </label>    </div>    <div>      <ErrorMessage name="regex" />    </div>    <div>      <label>        Password        <VField name="pass" type="password" value="" />      </label>    </div>    <div>      <label>        Password Confirm        <VField name="confirm" type="password" value="" />      </label>    </div>    <div>      <ErrorMessage name="confirm" />    </div>    <button type="submit">Submit</button>  </VForm></template>

Non-Text Form Fields

Not all fields in a form are going to be text inputs, however. You might want to introduce a checkbox to the user to make sure they've accepted terms and conditions, have a dropdown of time zones, or have a date picker for the user to input a time.

Just like text inputs, you can combine these input types with validation!

While there are many other types of user input elements, let's focus on just one: Checkboxes.

vee-validate supports casting a VField to a different input type, just like React's Formik. Luckily for us, usage with yup is for checkboxes as simple as adding the required validator to Yup's schema shape.

<!-- FormComp.vue --><script setup>import { Form as VForm, Field as VField, ErrorMessage } from 'vee-validate'import * as yup from 'yup'const formSchema = yup.object().shape({  termsAndConditions: yup.bool().required('You need to accept the terms and conditions'),})function onSubmit(values) {  console.log(values)}</script><template>  <VForm @submit="onSubmit" :validationSchema="formSchema">    <div>      <label>        Terms and Conditions        <VField name="termsAndConditions" type="checkbox" :value="true" />      </label>    </div>    <div>      <ErrorMessage name="termsAndConditions" />    </div>    <button type="submit">Submit</button>  </VForm></template>

Conclusion

Hopefully this has been helpful to see into VeeValidate's usage.

If you liked this and want to learn more Vue for free, you might want to check out my book series titled "The Framework Field Guide", which teaches React, Angular, and Vue all at once for free.

Take care!

Previous articleIntro to Formik

Subscribe to our newsletter!

Subscribe to our newsletter to get updates on new content we create, events we have coming up, and more! We'll make sure not to spam you and provide good insights to the content we have.