Wat is two-way binding?

Bij een form-element heb je twee stromingen van data:

  • Van JS naar input: de waarde van je ref tonen in het veld
  • Van input naar JS: als de gebruiker typt, update de ref

Met v-model regelt Vue beide kanten in één regel. Geen aparte @input handler nodig.

Zonder v-model (omslachtig)

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

Met v-model (zo doe je het)

<input v-model="name" />

Vue vs React: in React schrijf je voor élke input een onChange handler. In Vue is v-model alles wat je nodig hebt. Dit scheelt heel veel code in formulieren.

Tekst-inputs

v-model werkt op alle tekst-achtige inputs:

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

const name = ref('')
const password = ref('')
const age = ref(0)
</script>

<template>
  <input v-model="name" type="text" placeholder="Naam" />
  <input v-model="password" type="password" />
  <input v-model="age" type="number" />

  <p>Hallo {{ name }}, je bent {{ age }} jaar oud</p>
</template>

Let op bij type="number": de waarde is een string, niet een number. Wil je rekenen? Gebruik Number(age.value) of een computed.

Textarea

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

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

<template>
  <textarea v-model="message" rows="4"></textarea>
  <p>Je bericht: {{ message }}</p>
</template>

Fout:

<textarea v-model="message">{{ message }}</textarea>   <!-- ❌ -->

Zet de waarde niet tussen de tags. v-model doet dat al voor je.

Checkbox

Een enkele checkbox bindt op een boolean (true / false):

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

const acceptTerms = ref(false)
</script>

<template>
  <label>
    <input v-model="acceptTerms" type="checkbox" />
    Ik ga akkoord met de voorwaarden
  </label>

  <button :disabled="!acceptTerms">Verstuur</button>
</template>

Radio buttons

Bij een groep radio buttons bindt v-model op de value van de aangevinkte optie:

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

const size = ref('M')
</script>

<template>
  <label>
    <input v-model="size" type="radio" value="S" /> Small
  </label>
  <label>
    <input v-model="size" type="radio" value="M" /> Medium
  </label>
  <label>
    <input v-model="size" type="radio" value="L" /> Large
  </label>

  <p>Gekozen maat: {{ size }}</p>
</template>

Belangrijk: Alle radios in een groep gebruiken dezelfde v-model. Vue weet dan dat ze bij elkaar horen. Geen name-attribuut nodig.

Select (dropdown)

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

const country = ref('NL')
</script>

<template>
  <select v-model="country">
    <option value="NL">Nederland</option>
    <option value="BE">België</option>
    <option value="DE">Duitsland</option>
  </select>

  <p>Gekozen land: {{ country }}</p>
</template>

Met v-for opties

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

const selected = ref('')
const options = [
  { value: 'NL', label: 'Nederland' },
  { value: 'BE', label: 'België' },
  { value: 'DE', label: 'Duitsland' }
]
</script>

<template>
  <select v-model="selected">
    <option v-for="opt in options" :key="opt.value" :value="opt.value">
      {{ opt.label }}
    </option>
  </select>
</template>

v-model op je eigen component

Je kunt v-model ook gebruiken op een component dat je zelf maakt. Dat doe je met defineModel().

Het child component

<!-- src/components/MyInput.vue -->
<script setup>
const model = defineModel()
</script>

<template>
  <input v-model="model" class="my-fancy-input" />
</template>

Het parent component

<!-- src/App.vue -->
<script setup>
import { ref } from 'vue'
import MyInput from './components/MyInput.vue'

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

<template>
  <MyInput v-model="name" />
  <p>Hallo {{ name }}!</p>
</template>

Wat doet defineModel()? Het maakt een two-way binding tussen parent en child. De ref model in het child gedraagt zich gewoon als een normale ref — verander hem, en de parent ziet de wijziging meteen.

Vue 3.4+: defineModel() is nieuw sinds Vue 3.4 (januari 2024). Kom je oudere code tegen met props.modelValue en emit('update:modelValue')? Dat is de oude, omslachtigere manier. Gebruik altijd defineModel().

Belangrijke Regels

  • v-model = two-way binding — vervangt :value + @input
  • Checkbox → boolean (true/false)
  • Radio & select → de gekozen value
  • Tekst-inputs → string (ook type="number" geeft een string!)
  • Je eigen component → gebruik defineModel()

Veelgemaakte Fouten

Fout — v-model én :value tegelijk:

<input v-model="name" :value="name" />   <!-- ❌ dubbel, conflict -->

Goed:

<input v-model="name" />                  <!-- ✅ v-model regelt :value -->

Fout — waarde in textarea-content:

<textarea v-model="msg">{{ msg }}</textarea>   <!-- ❌ -->

Goed:

<textarea v-model="msg"></textarea>             <!-- ✅ -->

Fout — vergeten value bij radio:

<input v-model="size" type="radio" /> Small
<input v-model="size" type="radio" /> Medium
<!-- ❌ zonder value weet Vue niet welke gekozen is -->

Goed:

<input v-model="size" type="radio" value="S" /> Small
<input v-model="size" type="radio" value="M" /> Medium