Simple, lightweight model-based validation library for Vue.js applications
—
Complete validation state management with reactive properties and control methods for tracking field state, errors, and programmatic validation control.
Reactive properties available on all validation objects for tracking validation status.
interface ValidationState {
/**
* True if any validation rule is failing
*/
readonly $invalid: boolean;
/**
* True if all validation rules are passing (opposite of $invalid)
*/
readonly $valid: boolean;
/**
* True if any async validation is currently pending
*/
readonly $pending: boolean;
/**
* True if the field has been touched/modified by user interaction
*/
readonly $dirty: boolean;
/**
* True if this field or any nested field is dirty
*/
readonly $anyDirty: boolean;
/**
* True if field is invalid AND dirty (commonly used for error display)
*/
readonly $error: boolean;
/**
* True if this field or any nested field has an error
*/
readonly $anyError: boolean;
/**
* Parameter object from validators (contains metadata)
*/
readonly $params: object | null;
}Usage:
// Template usage
export default {
template: `
<div>
<input
v-model="email"
:class="{ 'error': $v.email.$error }"
@blur="$v.email.$touch()"
/>
<!-- Show error only when field is dirty and invalid -->
<div v-if="$v.email.$error" class="error-message">
<span v-if="!$v.email.required">Email is required</span>
<span v-if="!$v.email.email">Email format is invalid</span>
</div>
<!-- Show loading indicator for async validation -->
<div v-if="$v.email.$pending" class="loading">
Validating email...
</div>
<!-- Overall form state -->
<button
:disabled="$v.$invalid || $v.$pending"
@click="submitForm"
>
Submit
</button>
</div>
`,
computed: {
formIsReady() {
return !this.$v.$invalid && !this.$v.$pending
},
hasAnyErrors() {
return this.$v.$anyError
}
}
}Methods for programmatically controlling validation state.
interface ValidationControl {
/**
* Marks the field as dirty (touched by user)
* Triggers error display if field is invalid
*/
$touch(): void;
/**
* Resets the field to pristine state (not dirty)
* Hides error display even if field is invalid
*/
$reset(): void;
/**
* Flattens nested validation parameters into a flat array structure
* Useful for programmatic access to all validation metadata
*/
$flattenParams(): Array<{path: string[], name: string, params: object}>;
/**
* Getter/setter for the validated model value
* Setting triggers validation and marks field as dirty
*/
$model: any;
}Usage:
export default {
methods: {
// Touch all fields to show validation errors
validateAll() {
this.$v.$touch()
if (!this.$v.$invalid) {
this.submitForm()
}
},
// Reset specific field
resetEmail() {
this.$v.email.$reset()
this.email = ''
},
// Reset entire form
resetForm() {
this.$v.$reset()
this.email = ''
this.name = ''
this.password = ''
},
// Programmatically set and validate value
setEmailFromAPI(newEmail) {
// Setting $model triggers validation and marks as dirty
this.$v.email.$model = newEmail
},
// Get flattened parameter structure for debugging or error reporting
debugValidation() {
const flatParams = this.$v.$flattenParams()
console.log('All validation parameters:', flatParams)
// Example output:
// [
// { path: ['email'], name: 'required', params: { type: 'required' } },
// { path: ['email'], name: 'email', params: { type: 'email' } },
// { path: ['password'], name: 'minLength', params: { type: 'minLength', min: 8 } }
// ]
},
// Handle form submission
submitForm() {
// Touch all fields first
this.$v.$touch()
// Check if form is valid
if (this.$v.$invalid) {
console.log('Form has validation errors')
return
}
// Check for pending async validations
if (this.$v.$pending) {
console.log('Validation still in progress')
return
}
// Form is valid, proceed with submission
console.log('Submitting form...')
}
}
}Each validation rule exposes its own state for fine-grained control.
interface ValidatorState {
/**
* True if this specific validator is passing
*/
readonly [validatorName]: boolean;
/**
* True if this specific validator is pending (async)
*/
readonly $pending: boolean;
/**
* Parameters for this specific validator
*/
readonly $params: object | null;
}Usage:
export default {
validations: {
password: {
required,
minLength: minLength(8),
strongPassword: customStrongPasswordValidator
}
},
computed: {
passwordErrors() {
const pw = this.$v.password
if (!pw.$dirty) return []
const errors = []
// Check each individual validator
if (!pw.required) {
errors.push('Password is required')
}
if (!pw.minLength) {
errors.push(`Password must be at least ${pw.minLength.$params.min} characters`)
}
if (!pw.strongPassword) {
errors.push('Password must include uppercase, lowercase, number, and special character')
}
return errors
},
passwordStrength() {
const pw = this.$v.password
if (!pw.$dirty || !this.password) return 0
let strength = 0
if (pw.required) strength += 1
if (pw.minLength) strength += 2
if (pw.strongPassword) strength += 2
return strength
}
}
}Common patterns for form validation and user experience.
Usage:
export default {
data() {
return {
formSubmitted: false,
showValidationSummary: false
}
},
computed: {
// Show errors after form submission or when field is dirty
shouldShowErrors() {
return (field) => {
return this.formSubmitted || field.$dirty
}
},
// Collect all validation errors
allErrors() {
const errors = []
const collectErrors = (validation, path = '') => {
for (const key in validation) {
if (key.startsWith('$')) continue
const field = validation[key]
const fieldPath = path ? `${path}.${key}` : key
if (typeof field === 'object' && field.$error) {
// Individual field error
for (const rule in field) {
if (rule.startsWith('$') || field[rule]) continue
errors.push({
field: fieldPath,
rule: rule,
message: this.getErrorMessage(fieldPath, rule, field[rule].$params)
})
}
} else if (typeof field === 'object') {
// Nested validation
collectErrors(field, fieldPath)
}
}
}
collectErrors(this.$v)
return errors
}
},
methods: {
async handleSubmit() {
this.formSubmitted = true
this.$v.$touch()
// Wait for any pending async validations
if (this.$v.$pending) {
await this.waitForValidation()
}
if (this.$v.$invalid) {
this.showValidationSummary = true
return
}
try {
await this.submitToAPI()
this.resetFormAfterSubmit()
} catch (error) {
console.error('Submission failed:', error)
}
},
waitForValidation() {
return new Promise((resolve) => {
const checkPending = () => {
if (!this.$v.$pending) {
resolve()
} else {
this.$nextTick(checkPending)
}
}
checkPending()
})
},
resetFormAfterSubmit() {
this.formSubmitted = false
this.showValidationSummary = false
this.$v.$reset()
// Reset form data
Object.assign(this.$data, this.$options.data())
},
getErrorMessage(field, rule, params) {
const messages = {
required: `${field} is required`,
email: `${field} must be a valid email`,
minLength: `${field} must be at least ${params?.min} characters`,
// ... add more message mappings
}
return messages[rule] || `${field} is invalid`
}
}
}Advanced patterns for reacting to validation state changes.
Usage:
export default {
watch: {
// Watch overall form validity
'$v.$invalid': {
handler(isInvalid) {
this.$emit('validity-changed', !isInvalid)
},
immediate: true
},
// Watch specific field for real-time feedback
'$v.email.$error': {
handler(hasError) {
if (hasError && this.$v.email.$dirty) {
this.showEmailHelp = true
}
}
},
// Watch for pending state changes
'$v.$pending'(isPending) {
this.isValidating = isPending
if (isPending) {
this.validationStartTime = Date.now()
} else {
console.log(`Validation completed in ${Date.now() - this.validationStartTime}ms`)
}
},
// Deep watch for nested validation changes
'$v': {
handler(newVal, oldVal) {
// Custom logic for validation state changes
this.saveValidationStateToLocalStorage()
},
deep: true
}
},
methods: {
saveValidationStateToLocalStorage() {
const state = {
dirty: this.$v.$dirty,
invalid: this.$v.$invalid,
errors: this.allErrors
}
localStorage.setItem('formValidationState', JSON.stringify(state))
}
}
}Install with Tessl CLI
npx tessl i tessl/npm-vuelidate