Capítulo 10 de 11

Tutorial Vue 3: formulários e two-way binding com v-model

verificado a 2 Setembro 2026 · 5 min

Resposta rápida

v-model cria uma ligação de dois sentidos entre um campo de formulário e um dado. O modificador .number converte o que é escrito em número, .trim retira os espaços e @submit.prevent impede que a página recarregue ao enviar.

No fim deste capítulo, saberás construir um formulário completo, validá-lo e entregar o seu resultado ao componente pai.

v-bind liga o dado ao template, num só sentido. Um formulário precisa também do sentido inverso: o que o utilizador escreve tem de subir até ao estado. É o papel de v-model.

O CSS do formulário

Acrescenta estas regras 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; }

O componente de formulário

Cria src/components/ReviewForm.vue. Cada campo está ligado a uma referência por 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>

Escreve no campo do nome e observa name nas ferramentas de desenvolvimento do Vue: o valor acompanha cada tecla.

O que faz o v-model

v-model não é magia, é um atalho. Num <input>, estas duas linhas são equivalentes:

html
<input v-model="name">

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

Uma ligação que desce, um listener que sobe. O Vue adapta o atributo e o evento ao elemento: value e input para um campo de texto, checked e change para uma caixa de seleção, value e change para uma lista pendente.

Os modificadores

Três sufixos evitam código repetitivo:

  • .number converte o que é escrito em número. Sem ele, um <select> e um <input type="number"> devolvem uma string: rating valia "5", e um teste === 5 falhava.
  • .trim retira os espaços no início e no fim do que é escrito.
  • .lazy sincroniza no evento change em vez de a cada tecla.
html
<input id="name" v-model.trim="name">
<select id="rating" v-model.number="rating">

Submeter

Um formulário HTML recarrega a página ao enviar. O modificador .prevent visto no capítulo 5 impede-o:

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

A função valida, emite a avaliação para o pai e depois limpa os 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>

Mostra a mensagem de erro no template, mesmo antes do botão:

src/components/ReviewForm.vue
<p v-if="error" class="review-error" role="alert">{{ error }}</p>
Porquê não alert()?

Uma caixa alert() bloqueia a página, não é lida por um leitor de ecrã no momento certo e não se testa. Um parágrafo com role="alert" é anunciado pelas tecnologias de apoio assim que aparece, e mantém-se visível durante a correção.

Mostrar as avaliações

Cria src/components/ReviewList.vue, que recebe a lista numa 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>

Montar tudo

ProductDisplay.vue passa a ser o pai dos dois novos componentes: guarda as avaliações e escuta o formulário, com a emissão de eventos do 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>

O v-if="reviews.length" evita mostrar uma caixa vazia enquanto ninguém deixar uma avaliação.

Envia o formulário vazio: aparece a mensagem de erro. Preenche-o: a avaliação junta-se à lista e os campos ficam limpos.

As avaliações desaparecem ao recarregar, já que só vivem em memória. Para as guardar é precisa uma base de dados: vê Firebase com Vue.

v-model num componente

v-model também funciona nos teus próprios componentes. Desde o Vue 3.4, a macro defineModel torna-o imediato:

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

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

O pai usa-o então como um campo nativo:

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

Terminaste a aplicação

Mostra um produto e as suas variantes, reage ao hover, trata da disponibilidade, enche um carrinho e aceita avaliações validadas. O capítulo seguinte acrescenta o último toque: as animações de entrada e de saída.

Na Options API

v-model e os seus modificadores escrevem-se da mesma maneira. Os campos declaram-se em data(), a validação em methods e a emissão com this.$emit.

ExercícioAcrescenta uma caixa de seleção «Recomendo este produto» ligada por v-model a um booleano, passa-a na avaliação emitida e mostra-a na lista.

Erros frequentes

Esquecer o .number numa nota Um <select> e um <input type="number"> devolvem uma string. Sem .number, um teste === 5 falha e um cálculo concatena em vez de somar.
Esquecer o .prevent no formulário A página recarrega ao enviar e o estado perde-se. Escreve @submit.prevent="onSubmit".
Validar com alert() A caixa bloqueia a página e não é anunciada corretamente pelos leitores de ecrã. Mostra uma mensagem com role="alert".
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.