Tipos de input con v-model
Vue permite utilizar v-model con todos los tipos de elementos de formulario HTML. Cada tipo de input tiene un comportamiento específico que conviene dominar para construir formularios completos y robustos.
Campos de texto, número y fecha
Los campos de texto, número y fecha son los más habituales en cualquier formulario:
<template>
<div>
<input v-model="nombre" type="text" placeholder="Nombre" />
<input v-model.number="edad" type="number" placeholder="Edad" />
<textarea v-model="bio" rows="4" placeholder="Biografía"></textarea>
<input v-model="fechaNacimiento" type="date" />
<p>Nombre: {{ nombre }}</p>
<p>Edad: {{ edad }} (tipo: {{ typeof edad }})</p>
<p>Bio: {{ bio }}</p>
<p>Fecha: {{ fechaNacimiento }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const nombre = ref('')
const edad = ref<number>(0)
const bio = ref('')
const fechaNacimiento = ref('')
</script>
Checkbox individual y múltiple
Un checkbox individual devuelve un valor booleano. Cuando se enlazan múltiples checkboxes al mismo array, Vue gestiona automáticamente la adición y eliminación de valores:
<template>
<div>
<label>
<input type="checkbox" v-model="aceptaTerminos" />
Acepto los terminos
</label>
<p>Terminos aceptados: {{ aceptaTerminos }}</p>
<h4>Intereses:</h4>
<label v-for="opcion in opcionesIntereses" :key="opcion">
<input type="checkbox" v-model="intereses" :value="opcion" />
{{ opcion }}
</label>
<p>Seleccionados: {{ intereses }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const aceptaTerminos = ref(false)
const opcionesIntereses = ['Tecnologia', 'Deportes', 'Musica', 'Viajes']
const intereses = ref<string[]>([])
</script>
Radio buttons
Los radio buttons enlazados con v-model al mismo ref permiten seleccionar una unica opcion dentro de un grupo:
<template>
<div>
<label v-for="color in colores" :key="color">
<input type="radio" v-model="colorFavorito" :value="color" />
{{ color }}
</label>
<p>Color favorito: {{ colorFavorito }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const colores = ['Rojo', 'Verde', 'Azul', 'Amarillo']
const colorFavorito = ref('')
</script>
Select simple y múltiple
El elemento select puede funcionar en modo individual o múltiple. En modo múltiple, v-model se enlaza a un array:
<template>
<div>
<select v-model="pais">
<option value="" disabled>Selecciona un pais</option>
<option value="es">Espana</option>
<option value="mx">Mexico</option>
<option value="ar">Argentina</option>
</select>
<p>Pais: {{ pais }}</p>
<select v-model="idiomas" multiple>
<option value="es">Espanol</option>
<option value="en">Ingles</option>
<option value="fr">Frances</option>
<option value="de">Aleman</option>
</select>
<p>Idiomas: {{ idiomas }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const pais = ref('')
const idiomas = ref<string[]>([])
</script>
Modificadores de v-model
Vue ofrece tres modificadores que alteran el comportamiento de v-model de forma declarativa.
.lazy
Por defecto, v-model sincroniza el valor en cada evento input. Con .lazy, la sincronizacion ocurre en el evento change, es decir, cuando el campo pierde el foco:
<input v-model.lazy="busqueda" type="text" placeholder="Se actualiza al perder el foco" />
.number
El modificador .number convierte automáticamente el valor a tipo numérico usando parseFloat. Si la conversión falla, devuelve el valor original:
<input v-model.number="precio" type="text" placeholder="Solo números" />
.trim
El modificador .trim elimina automáticamente los espacios en blanco al inicio y final del valor introducido:
<input v-model.trim="email" type="text" placeholder="Sin espacios extra" />
Se pueden combinar varios modificadores en el mismo input: v-model.lazy.trim="campo".
Generacion dinamica de formularios
Cuando los campos del formulario provienen de una configuración o de una API, se pueden generar de forma dinámica a partir de un esquema de datos:
<template>
<form @submit.prevent="enviar">
<div v-for="campo in esquema" :key="campo.nombre">
<label :for="campo.nombre">{{ campo.etiqueta }}</label>
<input
v-if="campo.tipo === 'text' || campo.tipo === 'email' || campo.tipo === 'number'"
:id="campo.nombre"
:type="campo.tipo"
v-model="formulario[campo.nombre]"
/>
<textarea
v-else-if="campo.tipo === 'textarea'"
:id="campo.nombre"
v-model="formulario[campo.nombre]"
></textarea>
<select
v-else-if="campo.tipo === 'select'"
:id="campo.nombre"
v-model="formulario[campo.nombre]"
>
<option v-for="op in campo.opciones" :key="op" :value="op">{{ op }}</option>
</select>
</div>
<button type="submit">Enviar</button>
</form>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
interface CampoEsquema {
nombre: string
etiqueta: string
tipo: 'text' | 'email' | 'number' | 'textarea' | 'select'
opciones?: string[]
}
const esquema: CampoEsquema[] = [
{ nombre: 'nombre', etiqueta: 'Nombre', tipo: 'text' },
{ nombre: 'email', etiqueta: 'Email', tipo: 'email' },
{ nombre: 'edad', etiqueta: 'Edad', tipo: 'number' },
{ nombre: 'mensaje', etiqueta: 'Mensaje', tipo: 'textarea' },
{ nombre: 'rol', etiqueta: 'Rol', tipo: 'select', opciones: ['Admin', 'Editor', 'Lector'] }
]
const formulario = reactive<Record<string, string | number>>({})
function enviar() {
console.log('Datos:', { ...formulario })
}
</script>
Componentes de input reutilizables con defineModel
A partir de Vue 3.4, defineModel simplifica la creación de componentes de formulario reutilizables. defineModel crea automáticamente una prop y el evento de actualización correspondiente, eliminando la necesidad de declarar props y emits manualmente:
<!-- FormInput.vue -->
<template>
<div class="form-input">
<label :for="id">{{ label }}</label>
<input :id="id" :type="type" v-model="model" :placeholder="placeholder" />
</div>
</template>
<script setup lang="ts">
const model = defineModel<string>({ required: true })
defineProps<{
label: string
id: string
type?: string
placeholder?: string
}>()
</script>
Uso desde el componente padre:
<template>
<FormInput v-model="nombre" label="Nombre" id="nombre" placeholder="Tu nombre" />
<FormInput v-model="email" label="Email" id="email" type="email" placeholder="tu@email.com" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import FormInput from './FormInput.vue'
const nombre = ref('')
const email = ref('')
</script>
Manejo de archivos: FileReader y FormData
Los campos de tipo file no soportan v-model directamente. Se manejan mediante el evento @change y las APIs nativas FileReader y FormData:
<template>
<div>
<input type="file" accept="image/*" @change="onFileChange" />
<img v-if="previewUrl" :src="previewUrl" alt="Preview" width="200" />
<button v-if="archivo" @click="enviarArchivo">Subir archivo</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const archivo = ref<File | null>(null)
const previewUrl = ref<string>('')
function onFileChange(event: Event) {
const input = event.target as HTMLInputElement
if (!input.files?.length) return
archivo.value = input.files[0]
const reader = new FileReader()
reader.onload = (e) => {
previewUrl.value = e.target?.result as string
}
reader.readAsDataURL(archivo.value)
}
async function enviarArchivo() {
if (!archivo.value) return
const formData = new FormData()
formData.append('imagen', archivo.value)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
})
const resultado = await response.json()
console.log('Subida completada:', resultado)
}
</script>
Formularios multi-paso (wizard)
Los formularios multipaso dividen un formulario largo en secciones más manejables. Se controla la navegación con un contador de paso reactivo:
<template>
<div class="wizard">
<div class="progress-bar">
<div
v-for="n in totalPasos"
:key="n"
class="step"
:class="{ active: n <= pasoActual, completed: n < pasoActual }"
>
{{ n }}
</div>
</div>
<form @submit.prevent="enviar">
<div v-if="pasoActual === 1">
<h3>Datos personales</h3>
<input v-model="datos.nombre" type="text" placeholder="Nombre" />
<input v-model="datos.email" type="email" placeholder="Email" />
</div>
<div v-if="pasoActual === 2">
<h3>Preferencias</h3>
<label v-for="tema in temas" :key="tema">
<input type="checkbox" v-model="datos.temas" :value="tema" /> {{ tema }}
</label>
</div>
<div v-if="pasoActual === 3">
<h3>Revision</h3>
<p><strong>Nombre:</strong> {{ datos.nombre }}</p>
<p><strong>Email:</strong> {{ datos.email }}</p>
<p><strong>Temas:</strong> {{ datos.temas.join(', ') }}</p>
</div>
<div class="botones">
<button type="button" v-if="pasoActual > 1" @click="pasoActual--">Anterior</button>
<button type="button" v-if="pasoActual < totalPasos" @click="pasoActual++">Siguiente</button>
<button type="submit" v-if="pasoActual === totalPasos">Enviar</button>
</div>
</form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
const pasoActual = ref(1)
const totalPasos = 3
const temas = ['Frontend', 'Backend', 'DevOps', 'Data Science']
const datos = reactive({
nombre: '',
email: '',
temas: [] as string[]
})
function enviar() {
console.log('Formulario enviado:', { ...datos })
}
</script>
Gestion del estado del formulario
En formularios complejos conviene rastrear el estado del formulario: si se ha modificado (dirty), si se esta enviando (loading) o si ya se envio (submitted):
<template>
<form @submit.prevent="enviar">
<input v-model="nombre" type="text" @input="marcarModificado" placeholder="Nombre" />
<input v-model="email" type="email" @input="marcarModificado" placeholder="Email" />
<p v-if="isDirty" class="aviso">Tienes cambios sin guardar</p>
<button type="submit" :disabled="isLoading || isSubmitted">
<span v-if="isLoading">Enviando...</span>
<span v-else-if="isSubmitted">Enviado correctamente</span>
<span v-else>Enviar</span>
</button>
<button type="button" v-if="isDirty && !isLoading" @click="resetear">Resetear</button>
</form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const nombre = ref('')
const email = ref('')
const isDirty = ref(false)
const isLoading = ref(false)
const isSubmitted = ref(false)
function marcarModificado() {
isDirty.value = true
isSubmitted.value = false
}
function resetear() {
nombre.value = ''
email.value = ''
isDirty.value = false
isSubmitted.value = false
}
async function enviar() {
isLoading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 1500))
isSubmitted.value = true
isDirty.value = false
} finally {
isLoading.value = false
}
}
</script>
Este patron permite informar al usuario del estado exacto del formulario en cada momento y evitar envios duplicados o perdida accidental de datos no guardados.
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