Skip to main content

Vue.js Vuelidate

Introduction

Form validation is a critical aspect of any interactive web application. When dealing with user input, validating data on the client-side before sending it to a server provides instant feedback to users and helps prevent unnecessary server requests with invalid data.

Vuelidate is a lightweight model-based validation library for Vue.js that allows you to add validation rules to your form inputs without polluting your templates with complex validation logic. It's designed to be simple, flexible, and powerful, making it an excellent choice for form validation in Vue.js applications.

In this guide, we'll explore how to use Vuelidate to validate forms in Vue.js applications, covering:

  • Basic setup and installation
  • Simple form validations
  • Complex validation scenarios
  • Custom validators
  • Error handling and user feedback
  • Best practices

Getting Started with Vuelidate

Installation

First, let's install Vuelidate in your Vue.js project:

bash
# npm
npm install @vuelidate/core @vuelidate/validators

# or yarn
yarn add @vuelidate/core @vuelidate/validators

Vuelidate v2.x (the current major version) is split into two packages:

  • @vuelidate/core: The core functionality
  • @vuelidate/validators: A collection of common validators

Basic Setup

Let's create a simple form with validation:

html
<template>
<form @submit.prevent="submitForm">
<div>
<label>Username</label>
<input type="text" v-model="formData.username" />
<div v-if="v$.username.$error" class="error">
{{ v$.username.$errors[0].$message }}
</div>
</div>

<div>
<label>Email</label>
<input type="email" v-model="formData.email" />
<div v-if="v$.email.$error" class="error">
{{ v$.email.$errors[0].$message }}
</div>
</div>

<div>
<label>Password</label>
<input type="password" v-model="formData.password" />
<div v-if="v$.password.$error" class="error">
{{ v$.password.$errors[0].$message }}
</div>
</div>

<button type="submit" :disabled="v$.$invalid">Submit</button>
</form>
</template>

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

export default {
setup() {
const formData = reactive({
username: '',
email: '',
password: ''
})

const rules = {
username: { required },
email: { required, email },
password: { required, minLength: minLength(8) }
}

const v$ = useVuelidate(rules, formData)

const submitForm = async () => {
const result = await v$.value.$validate()
if (result) {
// Form is valid, proceed with submission
console.log('Form submitted:', formData)
} else {
console.log('Form validation failed')
}
}

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

<style>
.error {
color: red;
font-size: 0.8em;
margin-top: 5px;
}
</style>

In this example:

  1. We create a reactive form data object
  2. Define validation rules for each field
  3. Use useVuelidate to connect the rules with the form data
  4. Display validation errors when users interact with the form
  5. Prevent form submission when validation fails

Understanding Vuelidate Core Concepts

Validation Rules

Vuelidate comes with many built-in validators in the @vuelidate/validators package:

ValidatorDescription
requiredChecks if a value exists
emailValidates email format
minLengthChecks minimum string length
maxLengthChecks maximum string length
numericEnsures value is numeric
alphaValidates alphabetic characters
alphaNumValidates alphanumeric characters
sameAsValidates if a value is the same as another
urlValidates URL format
notNegates another validator

Validation State

Vuelidate provides numerous properties to check validation state:

  • $invalid: Boolean indicating if validation has failed
  • $dirty: Boolean indicating if the field has been interacted with
  • $error: Combines $invalid and $dirty to show errors only after interaction
  • $errors: Array of error messages for the field
  • $touch(): Method to manually mark a field as touched/dirty

Advanced Validation Examples

Conditional Validation

Sometimes you need validation rules that depend on other fields or conditions:

html
<script>
import { reactive, computed } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import { required, email, minLength, helpers } from '@vuelidate/validators'

export default {
setup() {
const formData = reactive({
accountType: 'personal',
email: '',
companyName: '',
website: ''
})

const rules = computed(() => ({
accountType: { required },
email: { required, email },
companyName: {
required: helpers.withMessage(
'Company name is required for business accounts',
() => formData.accountType === 'business'
)
},
website: {
required: helpers.withMessage(
'Website is required for business accounts',
() => formData.accountType === 'business'
)
}
}))

const v$ = useVuelidate(rules, formData)

return { formData, v$ }
}
}
</script>

In this example, companyName and website are only required when accountType is 'business'.

Custom Validators

Vuelidate allows you to create custom validators for specific requirements:

html
<script>
import { reactive } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import { required, helpers } from '@vuelidate/validators'

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

// Custom validator to check if username is available
const isUsernameAvailable = (value) => {
// Simulate API check (would be async in real application)
const takenUsernames = ['admin', 'root', 'superuser']
return !takenUsernames.includes(value.toLowerCase())
}

const rules = {
username: {
required,
available: helpers.withMessage(
'This username is already taken',
isUsernameAvailable
)
}
}

const v$ = useVuelidate(rules, formData)

return { formData, v$ }
}
}
</script>

Async Validators

For validations that require server calls (like checking if a username is available in a database):

html
<script>
import { reactive } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import { required, helpers } from '@vuelidate/validators'

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

// Simulated API call
const checkUsernameAvailability = async (username) => {
// In real app, this would be an API call
await new Promise(resolve => setTimeout(resolve, 500))
const takenUsernames = ['admin', 'root', 'superuser']
return !takenUsernames.includes(username.toLowerCase())
}

const isUsernameAvailable = helpers.withAsync(async (value) => {
if (!value) return true // Skip API call if empty
return await checkUsernameAvailability(value)
})

const rules = {
username: {
required,
isAvailable: helpers.withMessage(
'This username is already taken',
isUsernameAvailable
)
}
}

const v$ = useVuelidate(rules, formData)

return { formData, v$ }
}
}
</script>

Practical Example: Registration Form

Let's build a complete registration form with various validation types:

html
<template>
<div class="registration-form">
<h2>Create an Account</h2>

<form @submit.prevent="submitForm">
<div class="form-group">
<label>Full Name</label>
<input
type="text"
v-model="formData.fullName"
@blur="v$.fullName.$touch()"
/>
<div class="error" v-if="v$.fullName.$error">
{{ v$.fullName.$errors[0].$message }}
</div>
</div>

<div class="form-group">
<label>Email</label>
<input
type="email"
v-model="formData.email"
@blur="v$.email.$touch()"
/>
<div class="error" v-if="v$.email.$error">
{{ v$.email.$errors[0].$message }}
</div>
</div>

<div class="form-group">
<label>Username</label>
<input
type="text"
v-model="formData.username"
@blur="v$.username.$touch()"
/>
<div class="error" v-if="v$.username.$error">
<div v-for="(error, index) in v$.username.$errors" :key="index">
{{ error.$message }}
</div>
</div>
<div v-if="v$.username.isAvailable.$pending" class="pending">
Checking username availability...
</div>
</div>

<div class="form-group">
<label>Password</label>
<input
type="password"
v-model="formData.password"
@blur="v$.password.$touch()"
/>
<div class="error" v-if="v$.password.$error">
<div v-for="(error, index) in v$.password.$errors" :key="index">
{{ error.$message }}
</div>
</div>
</div>

<div class="form-group">
<label>Confirm Password</label>
<input
type="password"
v-model="formData.confirmPassword"
@blur="v$.confirmPassword.$touch()"
/>
<div class="error" v-if="v$.confirmPassword.$error">
{{ v$.confirmPassword.$errors[0].$message }}
</div>
</div>

<div class="form-group checkbox">
<input
type="checkbox"
id="terms"
v-model="formData.acceptTerms"
@change="v$.acceptTerms.$touch()"
/>
<label for="terms">I accept the Terms of Service</label>
<div class="error" v-if="v$.acceptTerms.$error">
{{ v$.acceptTerms.$errors[0].$message }}
</div>
</div>

<div class="form-actions">
<button
type="submit"
:disabled="v$.$invalid || v$.$pending"
class="submit-button"
>
Register
</button>
</div>

<div v-if="formSubmitted" class="success-message">
Registration successful!
</div>
</form>
</div>
</template>

<script>
import { reactive, ref } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import {
required,
email,
minLength,
maxLength,
alpha,
alphaNum,
sameAs,
helpers
} from '@vuelidate/validators'

export default {
setup() {
const formData = reactive({
fullName: '',
email: '',
username: '',
password: '',
confirmPassword: '',
acceptTerms: false
})

const formSubmitted = ref(false)

// Custom password validator - requires one uppercase, lowercase, number
const strongPassword = helpers.withMessage(
'Password must contain at least 1 uppercase, 1 lowercase, and 1 number',
(value) => {
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/.test(value)
}
)

// Async username validator
const isUsernameAvailable = helpers.withAsync(async (value) => {
if (!value) return true
await new Promise(resolve => setTimeout(resolve, 1000))
const takenUsernames = ['admin', 'user', 'root']
return !takenUsernames.includes(value.toLowerCase())
})

const rules = {
fullName: {
required: helpers.withMessage('Full name is required', required),
minLength: helpers.withMessage('Name must be at least 3 characters', minLength(3))
},
email: {
required: helpers.withMessage('Email is required', required),
email: helpers.withMessage('Please enter a valid email address', email)
},
username: {
required: helpers.withMessage('Username is required', required),
minLength: helpers.withMessage('Username must be at least 4 characters', minLength(4)),
maxLength: helpers.withMessage('Username cannot exceed 20 characters', maxLength(20)),
alphaNum: helpers.withMessage('Username can only contain letters and numbers', alphaNum),
isAvailable: helpers.withMessage('This username is already taken', isUsernameAvailable)
},
password: {
required: helpers.withMessage('Password is required', required),
minLength: helpers.withMessage('Password must be at least 8 characters', minLength(8)),
strong: strongPassword
},
confirmPassword: {
required: helpers.withMessage('Please confirm your password', required),
sameAsPassword: helpers.withMessage(
'Passwords must match',
sameAs(formData.password)
)
},
acceptTerms: {
sameAs: helpers.withMessage(
'You must accept the terms and conditions',
sameAs(true)
)
}
}

const v$ = useVuelidate(rules, formData)

const submitForm = async () => {
const result = await v$.value.$validate()
if (result) {
// Form submission logic here
console.log('Form submitted successfully', formData)
formSubmitted.value = true

// In real app, you'd send this data to your server
// await api.register(formData)
}
}

return {
formData,
v$,
submitForm,
formSubmitted
}
}
}
</script>

<style scoped>
.registration-form {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}

.form-group {
margin-bottom: 15px;
}

label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}

input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}

.checkbox {
display: flex;
align-items: center;
}

.checkbox input {
margin-right: 10px;
}

.error {
color: #f56c6c;
font-size: 0.8em;
margin-top: 5px;
}

.pending {
color: #e6a23c;
font-size: 0.8em;
margin-top: 5px;
}

.submit-button {
background: #409eff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}

.submit-button:disabled {
background: #a0cfff;
cursor: not-allowed;
}

.success-message {
color: #67c23a;
margin-top: 15px;
padding: 10px;
background: #f0f9eb;
border: 1px solid #c2e7b0;
border-radius: 4px;
}
</style>

This comprehensive example demonstrates:

  1. Input validation for various field types
  2. Custom validation messages
  3. Async validation for username availability
  4. Password strength validation
  5. Password confirmation checking
  6. Terms acceptance validation
  7. Form submission handling
  8. Visual feedback for validation states

Best Practices

1. Validate at the Right Time

Instead of showing all validation errors immediately, show them after the user has interacted with a field:

html
<input 
type="text"
v-model="formData.username"
@blur="v$.username.$touch()"
/>
<div v-if="v$.username.$error">
{{ v$.username.$errors[0].$message }}
</div>

2. Use Custom Messages

Make error messages user-friendly and specific:

javascript
const rules = {
email: {
required: helpers.withMessage('Please enter your email address', required),
email: helpers.withMessage('Please enter a valid email address', email)
}
}

For complex forms, organize validations logically:

javascript
const addressRules = {
street: { required },
city: { required },
zipCode: { required, numeric }
}

const rules = {
name: { required },
email: { required, email },
address: addressRules
}

4. Combine Multiple Validators

Use multiple validators to create comprehensive validation rules:

javascript
const rules = {
password: {
required,
minLength: minLength(8),
containsUppercase: (value) => /[A-Z]/.test(value),
containsNumber: (value) => /\d/.test(value)
}
}

5. Use Computed Validators for Dynamic Rules

When validation depends on other fields:

javascript
const rules = computed(() => ({
discountCode: {
required: helpers.withMessage(
'Discount code is required when "useDiscount" is checked',
() => formData.useDiscount
)
}
}))

Summary

Vuelidate provides a powerful yet simple way to handle form validation in Vue.js applications. By separating validation logic from your templates, it helps you create clean, maintainable code that provides a great user experience.

In this guide, we've covered:

  • Basic setup and usage of Vuelidate
  • Built-in validators and how to use them
  • Creating custom validators for complex scenarios
  • Handling asynchronous validations
  • Best practices for form validation
  • A comprehensive practical example

With these tools and techniques, you can implement robust form validation in your Vue.js applications, ensuring data quality and providing immediate feedback to your users.

Additional Resources

Practice Exercises

  1. Create a login form with email and password validation
  2. Build a multi-step registration form with different validation rules for each step
  3. Implement a form with dynamic field validation based on user selections
  4. Create a custom validator for checking password strength with visual feedback
  5. Build a form with async validations that call a mock API endpoint

By practicing these exercises, you'll become proficient in implementing form validation with Vuelidate in your Vue.js applications.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)