Simple, lightweight model-based validation library for Vue.js applications
—
Advanced validation features for arrays, nested objects, and dynamic collections with support for tracking, conditional validation, and complex data structures.
Validation of array elements using the special $each key for dynamic collections.
/**
* Collection validation configuration for arrays
*/
interface CollectionValidation {
$each: {
[validationKey: string]: ValidationRule;
$trackBy?: string | ((item: any) => string);
};
}
/**
* Validation state for collections includes $iter property
*/
interface CollectionValidationState extends ValidationState {
/**
* Iterator object providing access to individual element validations
*/
readonly $iter: {
[index: string]: ValidationState;
};
}Usage:
import { required, email, minLength } from 'vuelidate/lib/validators'
export default {
data() {
return {
users: [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' }
]
}
},
validations: {
users: {
$each: {
name: { required, minLength: minLength(2) },
email: { required, email }
}
}
},
computed: {
// Access validation for specific array elements
firstUserErrors() {
return this.$v.users.$iter[0]
},
// Check if any user has validation errors
hasUserErrors() {
return this.$v.users.$error
}
},
methods: {
// Add new user with automatic validation
addUser() {
this.users.push({ name: '', email: '' })
// Validation automatically extends to new items
},
// Remove user (validation automatically adjusts)
removeUser(index) {
this.users.splice(index, 1)
},
// Validate all users
validateAllUsers() {
this.$v.users.$touch()
return !this.$v.users.$invalid
}
}
}Stable tracking of array elements during reordering and modifications.
/**
* Track array elements by a specific property or function
*/
interface TrackingOptions {
$trackBy: string | ((item: any) => string);
}Usage:
export default {
data() {
return {
todos: [
{ id: 1, text: 'Learn Vuelidate', completed: false },
{ id: 2, text: 'Build awesome app', completed: false }
]
}
},
validations: {
todos: {
$each: {
// Track by ID to maintain validation state during reordering
$trackBy: 'id',
text: { required, minLength: minLength(3) }
}
}
},
methods: {
// Reorder todos - validation state follows the tracked items
moveTodoUp(index) {
if (index > 0) {
const item = this.todos.splice(index, 1)[0]
this.todos.splice(index - 1, 0, item)
// Validation state is preserved due to $trackBy
}
},
// Dynamic tracking based on content
validationsWithDynamicTracking() {
return {
items: {
$each: {
$trackBy: (item) => `${item.category}-${item.id}`,
name: { required }
}
}
}
}
}
}Validation of nested object structures with hierarchical validation state.
/**
* Nested validation configuration
*/
interface NestedValidation {
[propertyName: string]: {
[validationKey: string]: ValidationRule | NestedValidation;
};
}Usage:
export default {
data() {
return {
user: {
personal: {
firstName: '',
lastName: '',
birthDate: ''
},
contact: {
email: '',
phone: '',
address: {
street: '',
city: '',
postalCode: ''
}
}
}
}
},
validations: {
user: {
personal: {
firstName: { required, minLength: minLength(2) },
lastName: { required, minLength: minLength(2) },
birthDate: { required }
},
contact: {
email: { required, email },
phone: { required },
address: {
street: { required },
city: { required },
postalCode: { required, minLength: minLength(5) }
}
}
}
},
computed: {
// Access nested validation states
personalInfoValid() {
return !this.$v.user.personal.$invalid
},
addressValid() {
return !this.$v.user.contact.address.$invalid
},
contactSectionErrors() {
const contact = this.$v.user.contact
return {
hasEmailError: contact.email.$error,
hasPhoneError: contact.phone.$error,
hasAddressError: contact.address.$error
}
}
},
methods: {
// Validate specific sections
validatePersonalInfo() {
this.$v.user.personal.$touch()
return !this.$v.user.personal.$invalid
},
validateContact() {
this.$v.user.contact.$touch()
return !this.$v.user.contact.$invalid
}
}
}Reference-based validation for grouping related fields across different parts of the data structure.
/**
* Group validation using array references
*/
interface GroupValidation {
[groupKey: string]: string[] | string;
}Usage:
export default {
data() {
return {
billingAddress: {
street: '',
city: '',
country: ''
},
shippingAddress: {
street: '',
city: '',
country: ''
},
sameAsShipping: false
}
},
validations: {
// Individual field validations
billingAddress: {
street: { required },
city: { required },
country: { required }
},
shippingAddress: {
street: { required },
city: { required },
country: { required }
},
// Group validation - validates related fields together
addressGroup: [
'billingAddress.street',
'billingAddress.city',
'billingAddress.country',
// Conditionally include shipping address
...(this.sameAsShipping ? [] : [
'shippingAddress.street',
'shippingAddress.city',
'shippingAddress.country'
])
]
},
computed: {
allAddressFieldsValid() {
return !this.$v.addressGroup.$invalid
}
},
watch: {
sameAsShipping(useShipping) {
if (useShipping) {
// Copy billing to shipping
Object.assign(this.shippingAddress, this.billingAddress)
}
// Re-evaluate validations due to dynamic group composition
this.$v.$touch()
}
}
}Advanced patterns for dynamic validation with changing data structures.
Usage:
export default {
data() {
return {
formFields: [
{ type: 'text', name: 'username', value: '', required: true },
{ type: 'email', name: 'email', value: '', required: true },
{ type: 'number', name: 'age', value: '', required: false }
]
}
},
// Dynamic validations based on field configuration
validations() {
const validations = {
formFields: {
$each: {
$trackBy: 'name',
value: {}
}
}
}
// Build validation rules based on field types
this.formFields.forEach((field, index) => {
const fieldValidations = {}
if (field.required) {
fieldValidations.required = required
}
switch (field.type) {
case 'email':
fieldValidations.email = email
break
case 'number':
fieldValidations.numeric = numeric
break
case 'text':
if (field.minLength) {
fieldValidations.minLength = minLength(field.minLength)
}
break
}
validations.formFields.$each.value = fieldValidations
})
return validations
},
computed: {
dynamicFormErrors() {
const errors = []
this.formFields.forEach((field, index) => {
const validation = this.$v.formFields.$iter[index]
if (validation && validation.value.$error) {
errors.push({
field: field.name,
errors: this.getFieldErrors(validation.value, field)
})
}
})
return errors
}
},
methods: {
addField(fieldConfig) {
this.formFields.push({
type: fieldConfig.type,
name: fieldConfig.name,
value: '',
required: fieldConfig.required || false
})
},
removeField(index) {
this.formFields.splice(index, 1)
},
getFieldErrors(validation, fieldConfig) {
const errors = []
if (fieldConfig.required && !validation.required) {
errors.push(`${fieldConfig.name} is required`)
}
if (fieldConfig.type === 'email' && !validation.email) {
errors.push(`${fieldConfig.name} must be a valid email`)
}
if (fieldConfig.type === 'number' && !validation.numeric) {
errors.push(`${fieldConfig.name} must be a number`)
}
return errors
}
}
}Validation of arrays containing nested objects with their own collections.
Usage:
export default {
data() {
return {
departments: [
{
name: 'Engineering',
manager: { name: 'John Doe', email: 'john@company.com' },
employees: [
{ name: 'Alice', position: 'Senior Developer' },
{ name: 'Bob', position: 'Junior Developer' }
]
}
]
}
},
validations: {
departments: {
$each: {
$trackBy: 'name',
name: { required, minLength: minLength(2) },
manager: {
name: { required },
email: { required, email }
},
employees: {
$each: {
$trackBy: 'name',
name: { required, minLength: minLength(2) },
position: { required }
}
}
}
}
},
computed: {
departmentSummary() {
return this.departments.map((dept, deptIndex) => {
const deptValidation = this.$v.departments.$iter[deptIndex]
return {
name: dept.name,
isValid: !deptValidation.$error,
managerValid: !deptValidation.manager.$error,
employeeCount: dept.employees.length,
validEmployees: dept.employees.filter((emp, empIndex) =>
!deptValidation.employees.$iter[empIndex].$error
).length
}
})
}
},
methods: {
addDepartment() {
this.departments.push({
name: '',
manager: { name: '', email: '' },
employees: []
})
},
addEmployee(deptIndex) {
this.departments[deptIndex].employees.push({
name: '',
position: ''
})
},
validateDepartment(index) {
const deptValidation = this.$v.departments.$iter[index]
deptValidation.$touch()
return !deptValidation.$invalid
}
}
}Install with Tessl CLI
npx tessl i tessl/npm-vuelidate