Capítulo 10 de 11

Tutorial VueJS 3: formularios, v-model y validación

verificado el 2 septiembre 2026 · 5 min

Respuesta rápida

v-model crea una vinculación en dos sentidos entre un campo de formulario y un dato. El modificador .number convierte lo introducido en número, .trim elimina los espacios y @submit.prevent impide que la página se recargue al enviar.

Al final de este capítulo, sabrás construir un formulario completo, validarlo y transmitir su resultado al componente padre.

v-bind vincula el dato a la plantilla, en un solo sentido. Un formulario necesita también el sentido contrario: lo que el usuario escribe tiene que subir al estado. De eso se encarga v-model.

El CSS del formulario

Añade estas reglas a src/assets/boutique.css:

src/assets/boutique.css
.review-form { display: flex; flex-direction: column; width: 425px; padding: 20px; border: 2px solid #d8d8d8; background: #fff; }
.review-container { width: 425px; padding: 20px; border: 2px solid #d8d8d8; background: #fff; }
.review-error { color: #b91c1c; }

El componente de formulario

Crea src/components/ReviewForm.vue. Cada campo se vincula a una referencia mediante v-model:

src/components/ReviewForm.vue
<script setup>
import { ref } from 'vue'

const name = ref('')
const review = ref('')
const rating = ref(null)
</script>

<template>
  <form class="review-form">
    <h3>Laisser un avis</h3>

    <label for="name">Nom</label>
    <input id="name" v-model="name">

    <label for="review">Avis</label>
    <textarea id="review" v-model="review"></textarea>

    <label for="rating">Note</label>
    <select id="rating" v-model.number="rating">
      <option disabled :value="null">Choisir une note</option>
      <option>5</option>
      <option>4</option>
      <option>3</option>
      <option>2</option>
      <option>1</option>
    </select>

    <button class="button" type="submit">Envoyer</button>
  </form>
</template>

Escribe en el campo del nombre y observa name en las herramientas de desarrollo de Vue: el valor sigue cada pulsación.

Qué hace v-model

v-model no es magia, es un atajo. Sobre un <input>, estas dos líneas son equivalentes:

html
<input v-model="name">

<input :value="name" @input="name = $event.target.value">

Una vinculación que baja y un escuchador que sube. Vue adapta el atributo y el evento según el elemento: value e input para un campo de texto, checked y change para una casilla, value y change para una lista desplegable.

Los modificadores

Tres sufijos te ahorran código repetitivo:

  • .number convierte lo introducido en número. Sin él, un <select> y un <input type="number"> devuelven una cadena: rating valdría "5", y una comprobación === 5 fallaría.
  • .trim elimina los espacios del principio y del final.
  • .lazy sincroniza en el evento change en lugar de en cada pulsación.
html
<input id="name" v-model.trim="name">
<select id="rating" v-model.number="rating">

Enviar

Un formulario HTML recarga la página al enviarse. El modificador .prevent, visto en el capítulo 5, lo impide:

src/components/ReviewForm.vue
<form class="review-form" @submit.prevent="onSubmit">

La función valida, emite la opinión hacia el padre y después vacía los campos:

src/components/ReviewForm.vue
<script setup>
import { ref } from 'vue'

const emit = defineEmits(['review-submitted'])

const name = ref('')
const review = ref('')
const rating = ref(null)
const error = ref('')

function onSubmit() {
  if (name.value === '' || review.value === '' || rating.value === null) {
    error.value = 'Avis incomplet. Merci de remplir tous les champs.'
    return
  }

  emit('review-submitted', {
    name: name.value,
    review: review.value,
    rating: rating.value
  })

  name.value = ''
  review.value = ''
  rating.value = null
  error.value = ''
}
</script>

Muestra el mensaje de error en la plantilla, justo antes del botón:

src/components/ReviewForm.vue
<p v-if="error" class="review-error" role="alert">{{ error }}</p>
¿Por qué no alert()?

Un cuadro alert() bloquea la página, no llega al lector de pantalla en el momento adecuado y no se puede testear. Un párrafo con role="alert" lo anuncian las tecnologías de asistencia en cuanto aparece, y se queda visible mientras el usuario corrige el formulario.

Mostrar las opiniones

Crea src/components/ReviewList.vue, que recibe la lista como prop:

src/components/ReviewList.vue
<script setup>
defineProps({
  reviews: { type: Array, required: true }
})
</script>

<template>
  <div class="review-container">
    <h3>Avis</h3>
    <ul>
      <li v-for="(review, index) in reviews" :key="index">
        {{ review.name }} a mis {{ review.rating }} étoiles
        <br>
        « {{ review.review }} »
      </li>
    </ul>
  </div>
</template>

Montarlo todo

ProductDisplay.vue pasa a ser el padre de los dos componentes nuevos: guarda las opiniones y escucha al formulario, con la emisión de eventos del capítulo anterior.

src/components/ProductDisplay.vue
<script setup>
import ReviewForm from './ReviewForm.vue'
import ReviewList from './ReviewList.vue'

const reviews = ref([])

function addReview(review) {
  reviews.value.push(review)
}
</script>

<template>
  <div class="product-display">
    <div class="product-container">
      <!-- la fiche produit, inchangée -->
    </div>

    <ReviewList v-if="reviews.length" :reviews="reviews" />
    <ReviewForm @review-submitted="addReview" />
  </div>
</template>

El v-if="reviews.length" evita mostrar un recuadro vacío mientras nadie haya dejado una opinión.

Envía el formulario vacío: aparece el mensaje de error. Rellénalo: la opinión se añade a la lista y los campos se vacían.

Las opiniones desaparecen al recargar, porque solo viven en memoria. Para conservarlas hace falta una base de datos: mira Firebase con Vue.

v-model en un componente

v-model también funciona en tus propios componentes. Desde Vue 3.4, la macro defineModel lo vuelve inmediato:

src/components/ChampTexte.vue
<script setup>
const valeur = defineModel()
</script>

<template>
  <input :value="valeur" @input="valeur = $event.target.value">
</template>

El padre lo usa entonces como un campo nativo:

html
<ChampTexte v-model="name" />

Has terminado la aplicación

Muestra un producto y sus variantes, reacciona al paso del ratón, gestiona la disponibilidad, llena un carrito y acepta opiniones validadas. El capítulo siguiente añade el último toque: las animaciones de entrada y de salida.

En Options API

v-model y sus modificadores se escriben igual. Los campos se declaran en data(), la validación en methods y la emisión con this.$emit.

EjercicioAñade una casilla «Recomiendo este producto» vinculada con v-model a un booleano, pásala en la opinión emitida y muéstrala en la lista.

Errores frecuentes

Olvidar .number en la valoración Un <select> y un <input type="number"> devuelven una cadena. Sin .number, una comprobación === 5 falla y un cálculo concatena en vez de sumar.
Olvidar .prevent en el formulario La página se recarga al enviar y el estado se pierde. Escribe @submit.prevent="onSubmit".
Validar con alert() El cuadro bloquea la página y los lectores de pantalla no lo anuncian correctamente. Muestra un mensaje con role="alert".
Newsletter

Las nuevas pruebas, tutoriales y proyectos, por correo.

Pruebas reproducibles, código versionado, resultados fechados. Nunca spam.