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:
# 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:
<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:
- We create a reactive form data object
- Define validation rules for each field
- Use
useVuelidate
to connect the rules with the form data - Display validation errors when users interact with the form
- Prevent form submission when validation fails
Understanding Vuelidate Core Concepts
Validation Rules
Vuelidate comes with many built-in validators in the @vuelidate/validators
package:
Validator | Description |
---|---|
required | Checks if a value exists |
email | Validates email format |
minLength | Checks minimum string length |
maxLength | Checks maximum string length |
numeric | Ensures value is numeric |
alpha | Validates alphabetic characters |
alphaNum | Validates alphanumeric characters |
sameAs | Validates if a value is the same as another |
url | Validates URL format |
not | Negates 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:
<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:
<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):
<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:
<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:
- Input validation for various field types
- Custom validation messages
- Async validation for username availability
- Password strength validation
- Password confirmation checking
- Terms acceptance validation
- Form submission handling
- 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:
<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:
const rules = {
email: {
required: helpers.withMessage('Please enter your email address', required),
email: helpers.withMessage('Please enter a valid email address', email)
}
}
3. Group Related Validations
For complex forms, organize validations logically:
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:
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:
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
- Create a login form with email and password validation
- Build a multi-step registration form with different validation rules for each step
- Implement a form with dynamic field validation based on user selections
- Create a custom validator for checking password strength with visual feedback
- 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! :)