Chapter 14 - Mastering Form Validation in Vue: Vuelidate vs VeeValidate Showdown

Form validation in Vue.js ensures accurate user input. Vuelidate and VeeValidate are popular libraries offering model-based and declarative approaches, respectively. Both support custom validators, async validation, and complex form scenarios with nested fields and arrays.

Chapter 14 - Mastering Form Validation in Vue: Vuelidate vs VeeValidate Showdown

Form validation is a crucial aspect of web development that ensures user input is accurate and complete before submitting data to the server. In the Vue.js ecosystem, two popular libraries for form validation are Vuelidate and VeeValidate. Let’s dive into how to implement form validation using these libraries.

First, let’s start with Vuelidate. It’s a simple, lightweight, and model-based validation library for Vue.js that allows you to validate your forms without much hassle. To get started, you’ll need to install Vuelidate in your project:

npm install @vuelidate/core @vuelidate/validators

Once installed, you can import and use Vuelidate in your Vue component. Here’s a basic example of how to set up form validation with Vuelidate:

<template>
  <form @submit.prevent="submitForm">
    <input v-model="form.name" placeholder="Name" />
    <span v-if="v$.form.name.$error">Name is required</span>
    
    <input v-model="form.email" placeholder="Email" />
    <span v-if="v$.form.email.$error">Invalid email</span>
    
    <button type="submit">Submit</button>
  </form>
</template>

<script>
import { useVuelidate } from '@vuelidate/core'
import { required, email } from '@vuelidate/validators'

export default {
  setup() {
    const form = {
      name: '',
      email: ''
    }

    const rules = {
      form: {
        name: { required },
        email: { required, email }
      }
    }

    const v$ = useVuelidate(rules, { form })

    const submitForm = async () => {
      const isFormCorrect = await v$.value.$validate()
      if (isFormCorrect) {
        // Form is valid, proceed with submission
        console.log('Form submitted:', form)
      } else {
        // Form is invalid
        console.log('Form errors:', v$.value.$errors)
      }
    }

    return { form, v$, submitForm }
  }
}
</script>

In this example, we’re using the useVuelidate composition function to set up our validation rules. We define our form data and validation rules, then use v$ to access validation state in our template.

Now, let’s explore VeeValidate, another popular form validation library for Vue.js. VeeValidate offers a more declarative approach to form validation. Here’s how you can use it:

First, install VeeValidate:

npm install vee-validate @vee-validate/rules

Now, let’s create a form with validation using VeeValidate:

<template>
  <Form @submit="onSubmit">
    <Field name="name" v-slot="{ field, errors }">
      <input v-bind="field" placeholder="Name" />
      <span>{{ errors[0] }}</span>
    </Field>
    
    <Field name="email" v-slot="{ field, errors }">
      <input v-bind="field" placeholder="Email" />
      <span>{{ errors[0] }}</span>
    </Field>
    
    <button type="submit">Submit</button>
  </Form>
</template>

<script>
import { Form, Field, defineRule } from 'vee-validate'
import { required, email } from '@vee-validate/rules'

defineRule('required', required)
defineRule('email', email)

export default {
  components: {
    Form,
    Field
  },
  setup() {
    const onSubmit = values => {
      // Form is valid, proceed with submission
      console.log('Form submitted:', values)
    }

    return { onSubmit }
  }
}
</script>

In this VeeValidate example, we’re using the Form and Field components provided by the library. We define our validation rules using defineRule, and the Field component handles the validation state for each input.

Both Vuelidate and VeeValidate offer powerful features for form validation, but they have different approaches. Vuelidate is more flexible and allows for fine-grained control over validation logic, while VeeValidate provides a more declarative API that some developers find easier to use.

Let’s dive deeper into some advanced features of these libraries. With Vuelidate, you can create custom validators easily. For example, let’s say we want to validate that a username is unique:

<template>
  <form @submit.prevent="submitForm">
    <input v-model="form.username" placeholder="Username" />
    <span v-if="v$.form.username.$error">{{ v$.form.username.$errors[0].$message }}</span>
    
    <button type="submit">Submit</button>
  </form>
</template>

<script>
import { useVuelidate } from '@vuelidate/core'
import { required } from '@vuelidate/validators'

export default {
  setup() {
    const form = {
      username: ''
    }

    const isUnique = (value) => {
      // Simulating an API call to check uniqueness
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(value !== 'admin')
        }, 1000)
      })
    }

    const rules = {
      form: {
        username: { 
          required,
          isUnique: {
            $asyncValidator: isUnique,
            $message: 'This username is already taken'
          }
        }
      }
    }

    const v$ = useVuelidate(rules, { form })

    const submitForm = async () => {
      const isFormCorrect = await v$.value.$validate()
      if (isFormCorrect) {
        console.log('Form submitted:', form)
      }
    }

    return { form, v$, submitForm }
  }
}
</script>

In this example, we’ve added a custom async validator isUnique that simulates checking if a username is already taken. Vuelidate handles async validation seamlessly, making it easy to incorporate API calls into your validation logic.

VeeValidate also supports custom validators and async validation. Here’s how you might implement the same username uniqueness check with VeeValidate:

<template>
  <Form @submit="onSubmit">
    <Field name="username" :rules="{ required: true, unique: unique }" v-slot="{ field, errors }">
      <input v-bind="field" placeholder="Username" />
      <span>{{ errors[0] }}</span>
    </Field>
    
    <button type="submit">Submit</button>
  </Form>
</template>

<script>
import { Form, Field, defineRule } from 'vee-validate'
import { required } from '@vee-validate/rules'

defineRule('required', required)
defineRule('unique', async (value) => {
  // Simulating an API call to check uniqueness
  await new Promise(resolve => setTimeout(resolve, 1000))
  if (value === 'admin') {
    return 'This username is already taken'
  }
  return true
})

export default {
  components: {
    Form,
    Field
  },
  setup() {
    const onSubmit = values => {
      console.log('Form submitted:', values)
    }

    return { onSubmit }
  }
}
</script>

In this VeeValidate example, we’ve defined a custom unique rule that performs the same async check as our Vuelidate example.

One of the great things about both these libraries is how they handle complex form scenarios. Let’s say we have a form with nested fields and array inputs. Here’s how we might handle that with Vuelidate:

<template>
  <form @submit.prevent="submitForm">
    <input v-model="form.name" placeholder="Name" />
    <span v-if="v$.form.name.$error">Name is required</span>
    
    <input v-model="form.email" placeholder="Email" />
    <span v-if="v$.form.email.$error">Invalid email</span>
    
    <div v-for="(phone, index) in form.phones" :key="index">
      <input v-model="form.phones[index]" :placeholder="`Phone ${index + 1}`" />
      <span v-if="v$.form.phones.$each.$response.$errors[index]">Invalid phone number</span>
    </div>
    
    <button @click="addPhone">Add Phone</button>
    
    <input v-model="form.address.street" placeholder="Street" />
    <span v-if="v$.form.address.street.$error">Street is required</span>
    
    <input v-model="form.address.city" placeholder="City" />
    <span v-if="v$.form.address.city.$error">City is required</span>
    
    <button type="submit">Submit</button>
  </form>
</template>

<script>
import { useVuelidate } from '@vuelidate/core'
import { required, email, minLength } from '@vuelidate/validators'

export default {
  setup() {
    const form = reactive({
      name: '',
      email: '',
      phones: [''],
      address: {
        street: '',
        city: ''
      }
    })

    const rules = {
      form: {
        name: { required },
        email: { required, email },
        phones: {
          $each: minLength(10)
        },
        address: {
          street: { required },
          city: { required }
        }
      }
    }

    const v$ = useVuelidate(rules, { form })

    const addPhone = () => {
      form.phones.push('')
    }

    const submitForm = async () => {
      const isFormCorrect = await v$.value.$validate()
      if (isFormCorrect) {
        console.log('Form submitted:', form)
      }
    }

    return { form, v$, addPhone, submitForm }
  }
}
</script>

This example demonstrates how Vuelidate can handle nested objects (form.address) and arrays (form.phones) in your form data. The $each validator is used to apply the minLength validation to each phone number in the array.

VeeValidate can handle similar complex scenarios, but it approaches them slightly differently:

<template>
  <Form @submit="onSubmit">
    <Field name="name" rules="required" v-slot="{ field, errors }">
      <input v-bind="field" placeholder="Name" />
      <span>{{ errors[0] }}</span>
    </Field>
    
    <Field name="email" rules="required|email" v-slot="{ field, errors }">
      <input v-bind="field" placeholder="Email" />
      <span>{{ errors[0] }}</span>
    </Field>
    
    <div v-for="(_, index) in phones" :key="index">
      <Field :name="`phones[${index}]`" :rules="{ required: true, min: 10 }" v-slot="{ field, errors }">
        <input v-bind="field" :placeholder="`Phone ${index + 1}`" />
        <span>{{ errors[0] }}</span>
      </Field>
    </div>
    
    <button @click="addPhone">Add Phone</button>
    
    <Field name="address.street" rules="required" v-slot="{ field, errors }">
      <input v-bind="field" placeholder="Street" />
      <span>{{ errors[0] }}</span>
    </Field>
    
    <Field name="address.city" rules="required" v-slot="{ field, errors }">
      <input v-bind="field" placeholder="City" />
      <span>{{ errors[0] }}</span>
    </Field>
    
    <button type="submit">Submit</button>
  </Form>
</template>

<script>
import { ref } from 'vue'
import