Hoofdstuk 10 van 11

Vue 3-tutorial: formulieren binden en valideren met v-model

geverifieerd op 2 september 2026 · 5 min

Kort antwoord

v-model legt een tweerichtingsbinding tussen een formulierveld en een stuk data. De modifier .number zet de invoer om naar een getal, .trim haalt de spaties weg, en @submit.prevent voorkomt dat de pagina bij het versturen herlaadt.

Aan het einde van dit hoofdstuk, kun je een volledig formulier bouwen, het valideren en het resultaat aan de oudercomponent doorgeven.

v-bind koppelt de data aan de template, in één richting. Een formulier heeft ook de andere richting nodig: wat de gebruiker intikt, moet terug in de state komen. Dat is de taak van v-model.

De CSS van het formulier

Voeg deze regels toe aan 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; }

De formuliercomponent

Maak src/components/ReviewForm.vue aan. Elk veld hangt via v-model aan een ref:

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>

Tik iets in het veld “Nom” en houd name in de gaten in de Vue-devtools: de waarde volgt elke toetsaanslag.

Wat v-model doet

v-model is geen magie, het is een afkorting. Op een <input> zijn deze twee regels equivalent:

html
<input v-model="name">

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

Een binding die naar beneden gaat, een listener die naar boven gaat. Vue kiest attribuut en event op basis van het element: value en input voor een tekstveld, checked en change voor een checkbox, value en change voor een select.

De modifiers

Drie achtervoegsels besparen je herhaald werk:

  • .number zet de invoer om naar een getal. Zonder die modifier geven een <select> en een <input type="number"> een string terug: rating zou "5" zijn, en een test met === 5 zou mislukken.
  • .trim haalt de spaties aan het begin en het einde van de invoer weg.
  • .lazy synchroniseert op het event change in plaats van bij elke toetsaanslag.
html
<input id="name" v-model.trim="name">
<select id="rating" v-model.number="rating">

Versturen

Een HTML-formulier herlaadt de pagina bij het versturen. De modifier .prevent uit hoofdstuk 5 houdt dat tegen:

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

De functie valideert, stuurt de review naar de ouder en maakt daarna de velden leeg:

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>

Toon de foutmelding in de template, vlak voor de knop:

src/components/ReviewForm.vue
<p v-if="error" class="review-error" role="alert">{{ error }}</p>
Waarom geen alert()?

Een alert()-venster blokkeert de pagina, wordt door een schermlezer niet op het juiste moment voorgelezen en laat zich niet testen. Een paragraaf met role="alert" wordt door hulptechnologie aangekondigd zodra hij verschijnt, en blijft zichtbaar terwijl de bezoeker corrigeert.

De reviews tonen

Maak src/components/ReviewList.vue aan, die de lijst als prop ontvangt:

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>

Alles samenvoegen

ProductDisplay.vue wordt de ouder van de twee nieuwe componenten: hij bewaart de reviews en luistert naar het formulier, met het uitzenden van events uit het vorige hoofdstuk.

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>

De v-if="reviews.length" voorkomt dat er een leeg kader verschijnt zolang niemand een review heeft achtergelaten.

Verstuur het lege formulier: de foutmelding verschijnt. Vul het in: de review komt in de lijst en de velden worden leeggemaakt.

De reviews verdwijnen bij het herladen, want ze bestaan alleen in het geheugen. Om ze te bewaren heb je een database nodig: zie Firebase met Vue.

v-model op een component

v-model werkt ook op je eigen componenten. Sinds Vue 3.4 maakt de macro defineModel dat meteen mogelijk:

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

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

De ouder gebruikt hem dan als een gewoon veld:

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

De applicatie is af

Ze toont een product met zijn varianten, reageert op hover, houdt de voorraad bij, vult een winkelmandje en neemt gevalideerde reviews aan. Het volgende hoofdstuk zet er de laatste hand aan: de animaties bij het verschijnen en verdwijnen.

In de Options API

v-model en zijn modifiers schrijf je precies hetzelfde. De velden declareer je in data(), de validatie in methods, en het uitzenden gebeurt met this.$emit.

OefeningVoeg een checkbox “Ik raad dit product aan” toe die met v-model aan een boolean hangt, geef hem mee in de uitgezonden review en toon hem in de lijst.

Veelgemaakte fouten

.number vergeten op een score Een <select> en een <input type="number"> geven een string terug. Zonder .number mislukt een test met === 5 en plakt een berekening de waarden aan elkaar in plaats van op te tellen.
.prevent vergeten op het formulier De pagina herlaadt bij het versturen en de state gaat verloren. Schrijf @submit.prevent="onSubmit".
Valideren met alert() Het venster blokkeert de pagina en wordt door schermlezers niet correct aangekondigd. Toon een melding met role="alert".
Nieuwsbrief

Nieuwe tests, tutorials en projecten, per e-mail.

Reproduceerbare tests, geversioneerde code, gedateerde resultaten. Nooit spam.