Fundamentos de formularios en Vue

Intermedio
Vuejs
Vuejs
Actualizado: 04/09/2025

v-model

La directiva v-model es uno de los mecanismos más elegantes de Vue.js para crear un enlace bidireccional entre los elementos de formulario y el estado de la aplicación. Esta directiva simplifica enormemente el manejo de datos en formularios, eliminando la necesidad de escribir código repetitivo para sincronizar valores entre la vista y el modelo de datos.

v-model combina internamente el enlace de propiedades :value y el manejo de eventos @input, creando lo que conocemos como two-way data binding. Cuando el usuario modifica un campo del formulario, el estado se actualiza automáticamente, y cuando el estado cambia programáticamente, la interfaz se actualiza para reflejarlo.

Sintaxis básica con campos de texto

El uso más común de v-model es con campos de entrada de texto. Observa cómo funciona en este ejemplo práctico:

<template>
  <div>
    <input v-model="mensaje" placeholder="Escribe algo...">
    <p>Has escrito: {{ mensaje }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const mensaje = ref('')
</script>

En este ejemplo, cualquier cambio en el campo de entrada se refleja inmediatamente en el párrafo inferior. La reactividad de Vue.js se encarga de mantener sincronizados ambos elementos sin necesidad de código adicional.

Funcionamiento con diferentes tipos de input

La versatilidad de v-model se hace evidente al trabajar con distintos tipos de elementos de formulario. Cada tipo maneja los datos de manera específica según su naturaleza:

Campos numéricos:

<template>
  <div>
    <input type="number" v-model.number="edad">
    <p>Edad: {{ edad }} (tipo: {{ typeof edad }})</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const edad = ref(0)
</script>

Áreas de texto:

<template>
  <div>
    <textarea v-model="descripcion" rows="4" cols="50"></textarea>
    <p>Descripción: {{ descripcion }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const descripcion = ref('')
</script>

Trabajando con checkboxes y radio buttons

Los elementos de selección como checkboxes y radio buttons tienen comportamientos específicos con v-model que es importante comprender:

Checkbox individual (valor booleano):

<template>
  <div>
    <input type="checkbox" id="acepta" v-model="aceptaTerminos">
    <label for="acepta">Acepto los términos y condiciones</label>
    <p>Estado: {{ aceptaTerminos ? 'Aceptado' : 'No aceptado' }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const aceptaTerminos = ref(false)
</script>

Múltiples checkboxes (array de valores):

<template>
  <div>
    <h3>Selecciona tus tecnologías favoritas:</h3>
    
    <input type="checkbox" id="vue" value="Vue.js" v-model="tecnologias">
    <label for="vue">Vue.js</label>
    
    <input type="checkbox" id="react" value="React" v-model="tecnologias">
    <label for="react">React</label>
    
    <input type="checkbox" id="angular" value="Angular" v-model="tecnologias">
    <label for="angular">Angular</label>
    
    <p>Tecnologías seleccionadas: {{ tecnologias }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const tecnologias = ref([])
</script>

Radio buttons (valor único):

<template>
  <div>
    <h3>Selecciona tu nivel de experiencia:</h3>
    
    <input type="radio" id="principiante" value="principiante" v-model="nivelExperiencia">
    <label for="principiante">Principiante</label>
    
    <input type="radio" id="intermedio" value="intermedio" v-model="nivelExperiencia">
    <label for="intermedio">Intermedio</label>
    
    <input type="radio" id="avanzado" value="avanzado" v-model="nivelExperiencia">
    <label for="avanzado">Avanzado</label>
    
    <p>Nivel seleccionado: {{ nivelExperiencia }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const nivelExperiencia = ref('')
</script>

Elementos select

Los elementos select ofrecen flexibilidad para crear listas desplegables tanto de selección única como múltiple:

Select de selección única:

<template>
  <div>
    <select v-model="paisSeleccionado">
      <option disabled value="">Selecciona un país</option>
      <option value="es">España</option>
      <option value="mx">México</option>
      <option value="ar">Argentina</option>
      <option value="co">Colombia</option>
    </select>
    
    <p>País seleccionado: {{ paisSeleccionado }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const paisSeleccionado = ref('')
</script>

Select múltiple:

<template>
  <div>
    <select v-model="coloresFavoritos" multiple>
      <option value="rojo">Rojo</option>
      <option value="azul">Azul</option>
      <option value="verde">Verde</option>
      <option value="amarillo">Amarillo</option>
    </select>
    
    <p>Colores seleccionados: {{ coloresFavoritos }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const coloresFavoritos = ref([])
</script>

Modificadores útiles de v-model

Vue.js proporciona modificadores que extienden la funcionalidad básica de v-model para casos específicos:

  • .lazy - Sincroniza después del evento 'change' en lugar de 'input'
  • .number - Convierte automáticamente el valor a número
  • .trim - Elimina espacios en blanco al inicio y final
<template>
  <div>
    <!-- Sincronización lazy (solo cuando pierde el foco) -->
    <input v-model.lazy="textoLazy" placeholder="Sincronización lazy">
    <p>Valor lazy: {{ textoLazy }}</p>
    
    <!-- Conversión automática a número -->
    <input v-model.number="precio" type="number" placeholder="Precio">
    <p>Precio: {{ precio }} ({{ typeof precio }})</p>
    
    <!-- Eliminación automática de espacios -->
    <input v-model.trim="nombreCompleto" placeholder="Nombre completo">
    <p>Nombre: "{{ nombreCompleto }}"</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const textoLazy = ref('')
const precio = ref(0)
const nombreCompleto = ref('')
</script>

El dominio de v-model es fundamental para crear formularios eficientes en Vue.js. Esta directiva no solo reduce la cantidad de código necesario, sino que también hace que el manejo de estados en formularios sea más predecible y mantenible, aprovechando al máximo el sistema de reactividad de Vue.js.

Formularios básicos y validaciones simples

Los formularios básicos en Vue.js requieren una estructura organizativa que combine la captura de datos con la validación de los mismos antes de procesarlos. La validación es un aspecto crítico que garantiza la integridad de los datos y mejora la experiencia del usuario proporcionando retroalimentación inmediata sobre errores o campos incompletos.

Estructura de un formulario completo

Un formulario bien estructurado debe manejar múltiples aspectos: captura de datos, validación, envío y manejo de errores. Veamos cómo construir un formulario de registro de usuario que integre estos elementos:

<template>
  <form @submit.prevent="enviarFormulario" class="formulario">
    <h2>Registro de Usuario</h2>
    
    <div class="campo">
      <label for="nombre">Nombre completo:</label>
      <input 
        id="nombre"
        v-model="formulario.nombre" 
        type="text" 
        :class="{ error: errores.nombre }"
        @blur="validarCampo('nombre')"
      >
      <span v-if="errores.nombre" class="mensaje-error">{{ errores.nombre }}</span>
    </div>
    
    <div class="campo">
      <label for="email">Email:</label>
      <input 
        id="email"
        v-model="formulario.email" 
        type="email" 
        :class="{ error: errores.email }"
        @blur="validarCampo('email')"
      >
      <span v-if="errores.email" class="mensaje-error">{{ errores.email }}</span>
    </div>
    
    <div class="campo">
      <label for="edad">Edad:</label>
      <input 
        id="edad"
        v-model.number="formulario.edad" 
        type="number" 
        :class="{ error: errores.edad }"
        @blur="validarCampo('edad')"
      >
      <span v-if="errores.edad" class="mensaje-error">{{ errores.edad }}</span>
    </div>
    
    <button type="submit" :disabled="!formularioValido">
      {{ enviando ? 'Enviando...' : 'Registrar' }}
    </button>
    
    <div v-if="mensajeExito" class="mensaje-exito">
      {{ mensajeExito }}
    </div>
  </form>
</template>

<script setup>
import { ref, computed, reactive } from 'vue'

const formulario = reactive({
  nombre: '',
  email: '',
  edad: null
})

const errores = ref({})
const enviando = ref(false)
const mensajeExito = ref('')
</script>

<style scoped>
.formulario {
  max-width: 400px;
  margin: 0 auto;
}

.campo {
  margin-bottom: 1rem;
}

.error {
  border-color: #e74c3c;
}

.mensaje-error {
  color: #e74c3c;
  font-size: 0.875rem;
}

.mensaje-exito {
  color: #27ae60;
  margin-top: 1rem;
}
</style>

Sistema de validación por campos

La validación por campos permite proporcionar retroalimentación inmediata al usuario sin esperar al envío del formulario. Este enfoque mejora significativamente la experiencia de usuario:

// Continuación del script setup anterior

const validarCampo = (campo) => {
  errores.value = { ...errores.value }
  
  switch (campo) {
    case 'nombre':
      if (!formulario.nombre.trim()) {
        errores.value.nombre = 'El nombre es obligatorio'
      } else if (formulario.nombre.trim().length < 2) {
        errores.value.nombre = 'El nombre debe tener al menos 2 caracteres'
      } else {
        delete errores.value.nombre
      }
      break
      
    case 'email':
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
      if (!formulario.email.trim()) {
        errores.value.email = 'El email es obligatorio'
      } else if (!emailRegex.test(formulario.email)) {
        errores.value.email = 'El formato del email no es válido'
      } else {
        delete errores.value.email
      }
      break
      
    case 'edad':
      if (!formulario.edad) {
        errores.value.edad = 'La edad es obligatoria'
      } else if (formulario.edad < 18) {
        errores.value.edad = 'Debes ser mayor de edad'
      } else if (formulario.edad > 120) {
        errores.value.edad = 'Ingresa una edad válida'
      } else {
        delete errores.value.edad
      }
      break
  }
}

const validarFormularioCompleto = () => {
  validarCampo('nombre')
  validarCampo('email')
  validarCampo('edad')
}

const formularioValido = computed(() => {
  return Object.keys(errores.value).length === 0 &&
         formulario.nombre.trim() !== '' &&
         formulario.email.trim() !== '' &&
         formulario.edad !== null
})

Manejo del envío de formularios

El manejo del envío debe incluir validación final, prevención de envíos duplicados y retroalimentación al usuario sobre el estado de la operación:

// Continuación del script setup

const enviarFormulario = async () => {
  // Validar todo el formulario antes de enviar
  validarFormularioCompleto()
  
  // Si hay errores, no enviar
  if (!formularioValido.value) {
    return
  }
  
  enviando.value = true
  mensajeExito.value = ''
  
  try {
    // Simular envío a servidor
    await new Promise(resolve => setTimeout(resolve, 2000))
    
    // Limpiar formulario tras éxito
    Object.assign(formulario, {
      nombre: '',
      email: '',
      edad: null
    })
    
    errores.value = {}
    mensajeExito.value = '¡Usuario registrado correctamente!'
    
  } catch (error) {
    // Manejo de errores del servidor
    errores.value.servidor = 'Error al registrar usuario. Inténtalo de nuevo.'
  } finally {
    enviando.value = false
  }
}

Validaciones en tiempo real

Las validaciones en tiempo real proporcionan una experiencia más fluida al usuario. Podemos implementar validaciones que se ejecuten mientras el usuario escribe:

<template>
  <div class="campo">
    <label for="password">Contraseña:</label>
    <input 
      id="password"
      v-model="formulario.password" 
      type="password" 
      :class="{ 
        error: errores.password,
        success: formulario.password && !errores.password 
      }"
      @input="validarPasswordEnTiempoReal"
    >
    
    <!-- Indicador de fortaleza de contraseña -->
    <div v-if="formulario.password" class="indicador-password">
      <div class="criterio" :class="{ cumplido: criteriosPassword.longitud }">
        ✓ Al menos 8 caracteres
      </div>
      <div class="criterio" :class="{ cumplido: criteriosPassword.mayuscula }">
        ✓ Una letra mayúscula
      </div>
      <div class="criterio" :class="{ cumplido: criteriosPassword.numero }">
        ✓ Un número
      </div>
    </div>
    
    <span v-if="errores.password" class="mensaje-error">{{ errores.password }}</span>
  </div>
</template>

<script setup>
// Añadir al reactive del formulario
const formulario = reactive({
  nombre: '',
  email: '',
  edad: null,
  password: ''
})

const criteriosPassword = ref({
  longitud: false,
  mayuscula: false,
  numero: false
})

const validarPasswordEnTiempoReal = () => {
  const password = formulario.password
  
  // Actualizar criterios
  criteriosPassword.value = {
    longitud: password.length >= 8,
    mayuscula: /[A-Z]/.test(password),
    numero: /\d/.test(password)
  }
  
  // Validar y mostrar errores solo si el campo no está vacío
  if (password.trim()) {
    if (!criteriosPassword.value.longitud) {
      errores.value.password = 'La contraseña debe tener al menos 8 caracteres'
    } else if (!criteriosPassword.value.mayuscula) {
      errores.value.password = 'La contraseña debe tener al menos una mayúscula'
    } else if (!criteriosPassword.value.numero) {
      errores.value.password = 'La contraseña debe tener al menos un número'
    } else {
      delete errores.value.password
    }
  } else {
    delete errores.value.password
  }
}
</script>

<style scoped>
.success {
  border-color: #27ae60;
}

.indicador-password {
  margin-top: 0.5rem;
  font-size: 0.875rem;
}

.criterio {
  color: #e74c3c;
}

.criterio.cumplido {
  color: #27ae60;
}
</style>

Validaciones condicionales y dependientes

En ocasiones necesitamos validaciones que dependan de otros campos del formulario. Este patrón es común en formularios de confirmación:

<template>
  <div class="campo">
    <label for="confirmar-password">Confirmar contraseña:</label>
    <input 
      id="confirmar-password"
      v-model="formulario.confirmarPassword" 
      type="password" 
      :class="{ error: errores.confirmarPassword }"
      @blur="validarConfirmacionPassword"
    >
    <span v-if="errores.confirmarPassword" class="mensaje-error">
      {{ errores.confirmarPassword }}
    </span>
  </div>
</template>

<script setup>
// Añadir al reactive del formulario
const formulario = reactive({
  nombre: '',
  email: '',
  edad: null,
  password: '',
  confirmarPassword: ''
})

const validarConfirmacionPassword = () => {
  if (!formulario.confirmarPassword.trim()) {
    errores.value.confirmarPassword = 'Debes confirmar la contraseña'
  } else if (formulario.password !== formulario.confirmarPassword) {
    errores.value.confirmarPassword = 'Las contraseñas no coinciden'
  } else {
    delete errores.value.confirmarPassword
  }
}

// Actualizar la validación del formulario completo
const validarFormularioCompleto = () => {
  validarCampo('nombre')
  validarCampo('email')
  validarCampo('edad')
  validarPasswordEnTiempoReal()
  validarConfirmacionPassword()
}
</script>

Este enfoque de validaciones simples con JavaScript nativo proporciona un control total sobre el comportamiento del formulario sin depender de librerías externas. La combinación de validaciones en tiempo real, validaciones por campo y validaciones al envío crea una experiencia de usuario robusta y profesional, estableciendo las bases para formularios más complejos en aplicaciones Vue.js.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en Vuejs

Documentación oficial de Vuejs
Alan Sastre - Autor del tutorial

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

  • Comprender el funcionamiento y sintaxis básica de la directiva v-model en Vue.js.
  • Aprender a manejar diferentes tipos de inputs (texto, número, checkbox, radio, select) con v-model.
  • Implementar validaciones simples y en tiempo real en formularios Vue.
  • Gestionar el envío de formularios con validación previa y feedback al usuario.
  • Aplicar validaciones condicionales y dependientes entre campos del formulario.