Introducción a Vuelidate
Vuelidate es una librería de validación ligera y flexible para Vue.js que se integra de forma natural con la Composition API. A diferencia de otras soluciones, Vuelidate se basa en modelos y no en plantillas, lo que permite definir las reglas de validación directamente junto al estado reactivo del formulario.
Vuelidate proporciona validadores predefinidos, soporte para validaciones asíncronas, validación cruzada entre campos y un sistema de errores detallado que facilita mostrar mensajes específicos al usuario.
Instalación
Para utilizar Vuelidate es necesario instalar dos paquetes: el núcleo de la librería y la colección de validadores predefinidos:
npm install @vuelidate/core @vuelidate/validators
Configuración básica con useVuelidate
La función useVuelidate recibe dos argumentos: un objeto con las reglas de validación y un objeto con el estado reactivo a validar. Devuelve un objeto v$ que contiene toda la información de validación:
<template>
<form @submit.prevent="enviar">
<div>
<label for="nombre">Nombre</label>
<input id="nombre" v-model="formulario.nombre" type="text" />
<span v-if="v$.nombre.$error" class="error">
{{ v$.nombre.$errors[0].$message }}
</span>
</div>
<div>
<label for="email">Email</label>
<input id="email" v-model="formulario.email" type="email" />
<span v-if="v$.email.$error" class="error">
{{ v$.email.$errors[0].$message }}
</span>
</div>
<button type="submit">Enviar</button>
</form>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import { required, email, minLength } from '@vuelidate/validators'
const formulario = reactive({
nombre: '',
email: ''
})
const reglas = {
nombre: { required, minLength: minLength(3) },
email: { required, email }
}
const v$ = useVuelidate(reglas, formulario)
async function enviar() {
const esValido = await v$.value.$validate()
if (!esValido) return
console.log('Formulario válido:', { ...formulario })
}
</script>
Validadores predefinidos
Vuelidate incluye una amplia colección de validadores listos para usar. Los más habituales son:
| Validador | Descripción | Ejemplo |
|---|---|---|
| required | El campo no puede estar vacío | { required } |
| email | Formato de email válido | { email } |
| minLength(n) | Longitud mínima de caracteres | { minLength: minLength(5) } |
| maxLength(n) | Longitud máxima de caracteres | { maxLength: maxLength(100) } |
| numeric | Solo valores numéricos | { numeric } |
| between(min, max) | Valor entre un mínimo y un máximo | { between: between(18, 120) } |
| sameAs(ref) | Debe coincidir con otro campo | { sameAs: sameAs(password) } |
| url | Formato de URL válido | { url } |
| alpha | Solo caracteres alfabéticos | { alpha } |
Se pueden combinar múltiples validadores en un mismo campo para crear reglas más estrictas:
const reglas = {
username: { required, minLength: minLength(3), maxLength: maxLength(20) },
edad: { required, numeric, between: between(18, 120) },
sitioWeb: { url }
}
Estado de validación
El objeto v$ expone propiedades reactivas que describen el estado de cada campo y del formulario completo:
$invalid:truesi alguna regla no se cumple.$dirty:truesi el usuario ha interactuado con el campo (se ha hecho$touch).$error:truecuando el campo es$invalidy$dirtyal mismo tiempo. Es la propiedad más útil para mostrar errores.$errors: array de objetos de error con$message,$propertyy$validator.$pending:truesi hay validaciones asíncronas en curso.
<template>
<div>
<input v-model="formulario.nombre" @blur="v$.nombre.$touch()" />
<div v-if="v$.nombre.$error">
<p v-for="error in v$.nombre.$errors" :key="error.$uid" class="error">
{{ error.$message }}
</p>
</div>
<p>Dirty: {{ v$.nombre.$dirty }}</p>
<p>Invalid: {{ v$.nombre.$invalid }}</p>
</div>
</template>
Disparar y resetear la validación
Vuelidate ofrece tres métodos para controlar cuando se ejecuta la validación:
$touch()
Marca un campo o el formulario completo como dirty, activando la visualización de errores. Se suele usar en el evento @blur:
<input v-model="formulario.email" @blur="v$.email.$touch()" />
$validate()
Ejecuta la validación de todos los campos y devuelve una promesa con un booleano. Marca automáticamente todos los campos como dirty:
async function enviar() {
const esValido = await v$.value.$validate()
if (esValido) {
// procesar formulario
}
}
$reset()
Restablece el estado de validación, poniendo $dirty a false en todos los campos. Útil cuando se quiere limpiar el formulario:
function resetearFormulario() {
formulario.nombre = ''
formulario.email = ''
v$.value.$reset()
}
Mostrar mensajes de error
La forma más común de mostrar errores es iterar sobre el array $errors de cada campo. Cada error contiene un $message descriptivo:
<template>
<div class="campo">
<label for="password">Contraseña</label>
<input
id="password"
v-model="formulario.password"
type="password"
:class="{ 'input-error': v$.password.$error }"
@blur="v$.password.$touch()"
/>
<ul v-if="v$.password.$error" class="lista-errores">
<li v-for="error in v$.password.$errors" :key="error.$uid">
{{ error.$message }}
</li>
</ul>
</div>
</template>
Validadores personalizados
Validador síncrono
Un validador personalizado es simplemente una función que recibe el valor y devuelve true o false:
import { helpers } from '@vuelidate/validators'
const contieneNumero = helpers.withMessage(
'Debe contener al menos un número',
(value: string) => /\d/.test(value)
)
const contieneMayuscula = helpers.withMessage(
'Debe contener al menos una letra mayúscula',
(value: string) => /[A-Z]/.test(value)
)
const reglas = {
password: { required, minLength: minLength(8), contieneNumero, contieneMayuscula }
}
La función helpers.withMessage permite asociar un mensaje de error personalizado al validador.
Validador asíncrono
Los validadores asíncronos son funciones que devuelven una promesa. Son útiles para comprobar datos contra un servidor, como verificar si un nombre de usuario está disponible:
import { helpers } from '@vuelidate/validators'
const usernameDisponible = helpers.withMessage(
'Este nombre de usuario ya está en uso',
helpers.withAsync(async (value: string) => {
if (value === '') return true
const response = await fetch(`/api/check-username?username=${value}`)
const data = await response.json()
return data.disponible
})
)
const reglas = {
username: { required, minLength: minLength(3), usernameDisponible }
}
Mientras la validación asíncrona está en curso, v$.username.$pending será true, lo que permite mostrar un indicador de carga.
Validación cruzada con sameAs
La validación cruzada entre campos se realiza con el validador sameAs. El caso más común es la confirmación de contraseña:
<script setup lang="ts">
import { reactive, computed } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import { required, minLength, sameAs } from '@vuelidate/validators'
const formulario = reactive({
password: '',
confirmPassword: ''
})
const reglas = {
password: { required, minLength: minLength(8) },
confirmPassword: {
required,
sameAs: sameAs(computed(() => formulario.password))
}
}
const v$ = useVuelidate(reglas, formulario)
</script>
Es importante pasar un computed a sameAs para que la comparación sea reactiva y se actualice cuando el campo de referencia cambie.
Estado de validación a nivel de formulario
Antes de enviar un formulario, se comprueba si todo el formulario es válido accediendo al estado global de v$:
<template>
<form @submit.prevent="enviar">
<!-- campos del formulario -->
<button type="submit" :disabled="v$.$invalid">
Enviar
</button>
<p v-if="v$.$invalid && v$.$dirty" class="aviso">
Corrige los errores antes de enviar
</p>
</form>
</template>
<script setup lang="ts">
async function enviar() {
const esValido = await v$.value.$validate()
if (!esValido) {
console.log('Formulario inválido')
return
}
console.log('Enviando datos...')
}
</script>
Estilización de estados de validación
Se pueden añadir clases CSS dinámicas basadas en el estado de validación para dar feedback visual al usuario:
<template>
<div class="campo">
<input
v-model="formulario.email"
:class="{
'input-valido': v$.email.$dirty && !v$.email.$error,
'input-error': v$.email.$error
}"
@blur="v$.email.$touch()"
/>
</div>
</template>
<style scoped>
.input-error {
border-color: #dc3545;
background-color: #fff5f5;
}
.input-valido {
border-color: #28a745;
background-color: #f0fff0;
}
.error {
color: #dc3545;
font-size: 0.85rem;
margin-top: 0.25rem;
}
</style>
Ejemplo completo: formulario de registro
A continuación se presenta un ejemplo que combina todos los conceptos anteriores en un formulario de registro completo:
<template>
<form @submit.prevent="enviar" class="registro">
<h2>Crear cuenta</h2>
<div class="campo">
<label for="nombre">Nombre</label>
<input id="nombre" v-model="formulario.nombre" :class="{ 'input-error': v$.nombre.$error }" @blur="v$.nombre.$touch()" />
<ul v-if="v$.nombre.$error">
<li v-for="e in v$.nombre.$errors" :key="e.$uid">{{ e.$message }}</li>
</ul>
</div>
<div class="campo">
<label for="email">Email</label>
<input id="email" v-model="formulario.email" type="email" :class="{ 'input-error': v$.email.$error }" @blur="v$.email.$touch()" />
<ul v-if="v$.email.$error">
<li v-for="e in v$.email.$errors" :key="e.$uid">{{ e.$message }}</li>
</ul>
</div>
<div class="campo">
<label for="password">Contraseña</label>
<input id="password" v-model="formulario.password" type="password" :class="{ 'input-error': v$.password.$error }" @blur="v$.password.$touch()" />
<ul v-if="v$.password.$error">
<li v-for="e in v$.password.$errors" :key="e.$uid">{{ e.$message }}</li>
</ul>
</div>
<div class="campo">
<label for="confirm">Confirmar contraseña</label>
<input id="confirm" v-model="formulario.confirmPassword" type="password" :class="{ 'input-error': v$.confirmPassword.$error }" @blur="v$.confirmPassword.$touch()" />
<ul v-if="v$.confirmPassword.$error">
<li v-for="e in v$.confirmPassword.$errors" :key="e.$uid">{{ e.$message }}</li>
</ul>
</div>
<button type="submit" :disabled="v$.$invalid">Registrarse</button>
</form>
</template>
<script setup lang="ts">
import { reactive, computed } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import { required, email, minLength, sameAs, helpers } from '@vuelidate/validators'
const contieneNumero = helpers.withMessage(
'Debe contener al menos un número',
(value: string) => /\d/.test(value)
)
const contieneMayuscula = helpers.withMessage(
'Debe contener al menos una mayúscula',
(value: string) => /[A-Z]/.test(value)
)
const formulario = reactive({
nombre: '',
email: '',
password: '',
confirmPassword: ''
})
const reglas = {
nombre: { required, minLength: minLength(3) },
email: { required, email },
password: { required, minLength: minLength(8), contieneNumero, contieneMayuscula },
confirmPassword: { required, sameAs: sameAs(computed(() => formulario.password)) }
}
const v$ = useVuelidate(reglas, formulario)
async function enviar() {
const esValido = await v$.value.$validate()
if (!esValido) return
console.log('Registro completado:', { ...formulario })
}
</script>
Este patrón cubre la gran mayoría de los casos de uso de validación de formularios en aplicaciones Vue.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Vuejs
Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje