Skip to content

08. Formularios y Modelos

En Vue 3 con Composition API, v-model sigue siendo la directiva principal para crear enlaces bidireccionales entre formularios y datos reactivos. A diferencia de la Options API, los datos reactivos se definen usando ref() o reactive().

<script setup>
import { ref } from 'vue'
// Valores individuales
const username = ref('')
const password = ref('')
const rememberMe = ref(false)
// Objeto reactivo para agrupar campos relacionados
const form = reactive({
username: '',
password: '',
rememberMe: false
})
function handleSubmit() {
console.log('Formulario enviado:', {
username: username.value,
password: password.value,
rememberMe: rememberMe.value
})
// O si usas el objeto reactivo
console.log('Formulario enviado:', form)
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<!-- Usando refs individuales -->
<div>
<label for="username">Usuario:</label>
<input id="username" v-model="username" type="text" />
</div>
<div>
<label for="password">Contraseña:</label>
<input id="password" v-model="password" type="password" />
</div>
<div>
<label>
<input type="checkbox" v-model="rememberMe" />
Recordarme
</label>
</div>
<!-- O usando un objeto reactivo -->
<!--
<div>
<label for="username">Usuario:</label>
<input id="username" v-model="form.username" type="text" />
</div>
<div>
<label for="password">Contraseña:</label>
<input id="password" v-model="form.password" type="password" />
</div>
<div>
<label>
<input type="checkbox" v-model="form.rememberMe" />
Recordarme
</label>
</div>
-->
<button type="submit">Iniciar sesión</button>
</form>
</template>

Vue proporciona varios modificadores para v-model que facilitan la manipulación de la entrada del usuario:

<script setup>
import { ref } from 'vue'
const message = ref('')
const age = ref(0)
const trimmedInput = ref('')
const lazyInput = ref('')
</script>
<template>
<div>
<!-- .number: convierte la entrada a número -->
<input v-model.number="age" type="text" />
<p>Edad + 1 = {{ age + 1 }}</p>
<!-- .trim: elimina espacios en blanco al inicio y final -->
<input v-model.trim="trimmedInput" placeholder="Se eliminarán espacios" />
<!-- .lazy: actualiza después de cambiar el foco (no en cada pulsación) -->
<input v-model.lazy="lazyInput" placeholder="Actualiza al perder foco" />
</div>
</template>

En Vue 3, puedes crear componentes con su propio v-model personalizado. Esto permite crear componentes de formulario reutilizables que mantienen la misma experiencia de enlace bidireccional que los elementos nativos.

Implementación básica de v-model personalizado

Section titled “Implementación básica de v-model personalizado”
CustomInput.vue
<script setup>
// Define las props y eventos
const props = defineProps({
modelValue: String // Nombre especial para v-model
})
const emit = defineEmits(['update:modelValue'])
// Función para emitir el evento de actualización
function updateValue(event) {
emit('update:modelValue', event.target.value)
}
</script>
<template>
<input
:value="modelValue"
@input="updateValue"
class="custom-input"
/>
</template>

Una forma más elegante de implementar un v-model personalizado es usando una propiedad computada con getter y setter:

CustomInput.vue
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: String
})
const emit = defineEmits(['update:modelValue'])
// Propiedad computada con getter y setter
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
<template>
<input v-model="value" class="custom-input" />
</template>

En Vue 3, puedes tener múltiples v-model en un mismo componente, especificando nombres personalizados:

UserForm.vue
<script setup>
const props = defineProps({
firstName: String,
lastName: String
})
const emit = defineEmits(['update:firstName', 'update:lastName'])
// Computadas para cada v-model
const firstNameModel = computed({
get: () => props.firstName,
set: (value) => emit('update:firstName', value)
})
const lastNameModel = computed({
get: () => props.lastName,
set: (value) => emit('update:lastName', value)
})
</script>
<template>
<div>
<input v-model="firstNameModel" placeholder="Nombre" />
<input v-model="lastNameModel" placeholder="Apellido" />
</div>
</template>

La validación de formularios es una parte esencial de cualquier aplicación. Con la Composition API, puedes implementar validaciones de forma limpia y reutilizable.

<script setup>
import { ref, computed } from 'vue'
const email = ref('')
const password = ref('')
const submitted = ref(false)
// Validaciones computadas
const emailError = computed(() => {
if (!email.value) return submitted.value ? 'El email es obligatorio' : ''
if (!/^\S+@\S+\.\S+$/.test(email.value)) return 'Email inválido'
return ''
})
const passwordError = computed(() => {
if (!password.value) return submitted.value ? 'La contraseña es obligatoria' : ''
if (password.value.length < 6) return 'La contraseña debe tener al menos 6 caracteres'
return ''
})
const isFormValid = computed(() => {
return !emailError.value && !passwordError.value && email.value && password.value
})
function handleSubmit() {
submitted.value = true
if (isFormValid.value) {
console.log('Formulario válido, enviando datos...')
// Enviar datos al servidor
} else {
console.log('Formulario inválido')
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<div>
<label for="email">Email:</label>
<input
id="email"
v-model="email"
type="email"
:class="{ 'error': emailError && submitted }"
/>
<p v-if="emailError && submitted" class="error-message">{{ emailError }}</p>
</div>
<div>
<label for="password">Contraseña:</label>
<input
id="password"
v-model="password"
type="password"
:class="{ 'error': passwordError && submitted }"
/>
<p v-if="passwordError && submitted" class="error-message">{{ passwordError }}</p>
</div>
<button type="submit">Registrarse</button>
</form>
</template>
<style scoped>
.error {
border-color: red;
}
.error-message {
color: red;
font-size: 0.8em;
margin-top: 0.2em;
}
</style>

Para formularios más complejos, es recomendable extraer la lógica de validación a composables reutilizables:

useValidation.js
import { ref, computed } from 'vue'
export function useField(initialValue = '', validations) {
const value = ref(initialValue)
const errors = ref([])
const dirty = ref(false)
const validate = () => {
errors.value = []
for (const validation of validations) {
const error = validation(value.value)
if (error) {
errors.value.push(error)
}
}
return errors.value.length === 0
}
const onBlur = () => {
dirty.value = true
validate()
}
const isValid = computed(() => errors.value.length === 0)
return {
value,
errors,
dirty,
isValid,
validate,
onBlur
}
}
// Validadores comunes
export const required = (message = 'Este campo es obligatorio') => {
return (value) => {
return (value === undefined || value === null || value === '') ? message : null
}
}
export const minLength = (min, message) => {
return (value) => {
return value.length < min ? (message || `Debe tener al menos ${min} caracteres`) : null
}
}
export const email = (message = 'Email inválido') => {
return (value) => {
const re = /^\S+@\S+\.\S+$/
return !re.test(value) ? message : null
}
}

Integración con bibliotecas de validación

Section titled “Integración con bibliotecas de validación”

Para aplicaciones más complejas, puedes utilizar bibliotecas de validación como VeeValidate, Vuelidate o Formkit, que se integran perfectamente con la Composition API:

<script setup>
import { useForm, useField } from 'vee-validate'
import * as yup from 'yup'
// Definir esquema de validación
const schema = yup.object({
email: yup.string().required('El email es obligatorio').email('Email inválido'),
password: yup.string().required('La contraseña es obligatoria').min(6, 'Mínimo 6 caracteres')
})
// Usar el hook de formulario
const { handleSubmit, errors, values } = useForm({
validationSchema: schema
})
// Registrar campos
const { value: email } = useField('email')
const { value: password } = useField('password')
// Función de envío
const onSubmit = handleSubmit((values) => {
console.log('Formulario enviado:', values)
})
</script>
<template>
<form @submit="onSubmit">
<div>
<label for="email">Email</label>
<input id="email" v-model="email" type="email" />
<p v-if="errors.email" class="error-message">{{ errors.email }}</p>
</div>
<div>
<label for="password">Contraseña</label>
<input id="password" v-model="password" type="password" />
<p v-if="errors.password" class="error-message">{{ errors.password }}</p>
</div>
<button type="submit">Enviar</button>
</form>
</template>
🐝