Formulare & v-model
Two-Way Data Binding und Formularverarbeitung in Vue.js meistern
v-model Grundlagen
Wenn du aus der Rails-Welt kommst, kennst du das Problem: Ein User tippt etwas in ein Formularfeld, und du siehst die Änderung erst nach dem Absenden des Formulars auf dem Server. Vue dreht dieses Modell komplett um — mit v-model hast du eine direkte, bidirektionale Verbindung zwischen deinem HTML-Input und deinen JavaScript-Daten. Jeder Tastendruck aktualisiert sofort deine Variable, und jede Änderung an der Variable aktualisiert sofort das Eingabefeld.
Das Konzept nennt sich Two-Way Data Binding, und es ist eines der mächtigsten Features von Vue. Statt manuell Event-Listener zu registrieren und DOM-Elemente zu manipulieren, schreibst du einfach v-model auf dein Element — fertig.
<script setup>
import { ref } from 'vue'
const name = ref('')
</script>
<template>
<div>
<label for="name">Dein Name:</label>
<input id="name" v-model="name" placeholder="Name eingeben" />
<p>Hallo, {{ name || 'Fremder' }}!</p>
</div>
</template>v-model ist syntaktischer Zucker
Unter der Haube macht v-model nichts Magisches. Es ist lediglich eine Kurzform für zwei Dinge gleichzeitig: ein :value-Binding und einen @input-Event-Listener. Das hier:
<input v-model="name" />
ist identisch mit:
<input :value="name" @input="name = $event.target.value" />
Vue nimmt dir also nur die Tipparbeit ab — das Verständnis, was darunter passiert, hilft dir aber enorm beim Debugging.
Text-Eingaben
Die häufigsten Formularelemente sind einfache Textfelder und Textbereiche. Mit v-model bindest du sie direkt an eine reaktive Variable — egal ob <input type="text"> oder <textarea>.
Input und Textarea
Bei einem normalen Input-Feld ist alles straightforward. Bei <textarea> gibt es einen wichtigen Unterschied zu beachten: In normalem HTML kannst du den Standardinhalt zwischen die Tags schreiben (<textarea>Hallo</textarea>). Vue ignoriert diesen Inhalt komplett — du musst v-model verwenden, um den Wert zu setzen und zu lesen.
<script setup>
import { ref } from 'vue'
const username = ref('')
const bio = ref('')
</script>
<template>
<div class="space-y-4">
<!-- Einfaches Textfeld -->
<div>
<label for="username">Benutzername:</label>
<input id="username" v-model="username" type="text" />
<p>Benutzername: {{ username }}</p>
</div>
<!-- Textarea — Inhalt zwischen den Tags wird ignoriert! -->
<div>
<label for="bio">Bio:</label>
<textarea id="bio" v-model="bio" rows="4" />
<p>Zeichen: {{ bio.length }}</p>
</div>
</div>
</template>Checkboxen
Checkboxen sind in Vue besonders flexibel. Je nachdem, ob du eine einzelne Checkbox oder mehrere Checkboxen an dasselbe Model bindest, verhält sich v-model unterschiedlich:
Einzelne Checkbox → Boolean
Eine einzelne Checkbox wird an einen boolean-Wert gebunden. Ist die Checkbox aktiviert, ist der Wert true, sonst false. Perfekt für Zustimmungsfelder, Newsletter-Opt-ins oder Feature-Toggles.
Mehrere Checkboxen → Array
Wenn mehrere Checkboxen dasselbe v-model teilen, sammelt Vue die value-Attribute der aktivierten Checkboxen automatisch in einem Array. Das ist perfekt für Mehrfachauswahlen wie Interessen, Tags oder Berechtigungen.
<script setup>
import { ref } from 'vue'
// Einzelne Checkbox → Boolean
const newsletter = ref(false)
// Mehrere Checkboxen → Array
const interests = ref([])
</script>
<template>
<div class="space-y-6">
<!-- Einzelne Checkbox -->
<div>
<label>
<input v-model="newsletter" type="checkbox" />
Newsletter abonnieren
</label>
<p>Newsletter: {{ newsletter ? 'Ja' : 'Nein' }}</p>
</div>
<!-- Mehrere Checkboxen mit demselben v-model -->
<div>
<p class="font-medium">Interessen:</p>
<label>
<input v-model="interests" type="checkbox" value="Frontend" />
Frontend
</label>
<label>
<input v-model="interests" type="checkbox" value="Backend" />
Backend
</label>
<label>
<input v-model="interests" type="checkbox" value="DevOps" />
DevOps
</label>
<p>Ausgewählt: {{ interests.join(', ') || 'Nichts' }}</p>
</div>
</div>
</template>Radio Buttons
Radio Buttons funktionieren nach dem gleichen Prinzip wie Checkboxen, nur dass immer genau eine Option ausgewählt sein kann. Alle Radio Buttons, die dasselbe v-model teilen, bilden automatisch eine Gruppe. Der Wert der Variable entspricht dem value-Attribut des aktuell ausgewählten Buttons.
<script setup>
import { ref } from 'vue'
const experience = ref('')
</script>
<template>
<div>
<p class="font-medium">Erfahrungslevel:</p>
<label>
<input v-model="experience" type="radio" value="Anfänger" />
Anfänger
</label>
<label>
<input v-model="experience" type="radio" value="Fortgeschritten" />
Fortgeschritten
</label>
<label>
<input v-model="experience" type="radio" value="Experte" />
Experte
</label>
<p>Dein Level: {{ experience || 'Noch nicht gewählt' }}</p>
</div>
</template>Select / Dropdown
Dropdowns sind in Formularen allgegenwärtig — ob für Länderauswahl, Kategorien oder Rollen. Vue unterstützt sowohl Einzel- als auch Mehrfachauswahl mit v-model. Bei der Einfachauswahl enthält die Variable einen einzelnen Wert, bei multiple ein Array.
<script setup>
import { ref } from 'vue'
const framework = ref('')
const languages = ref([])
</script>
<template>
<div class="space-y-6">
<!-- Einfachauswahl -->
<div>
<label for="framework">Lieblings-Framework:</label>
<select id="framework" v-model="framework">
<option value="" disabled>Bitte wählen...</option>
<option>Vue</option>
<option>React</option>
<option>Angular</option>
<option>Svelte</option>
</select>
<p>Gewählt: {{ framework || 'Nichts' }}</p>
</div>
<!-- Mehrfachauswahl -->
<div>
<label for="languages">Sprachen (Ctrl/Cmd + Klick):</label>
<select id="languages" v-model="languages" multiple>
<option>JavaScript</option>
<option>TypeScript</option>
<option>Ruby</option>
<option>Python</option>
</select>
<p>Gewählt: {{ languages.join(', ') || 'Keine' }}</p>
</div>
</div>
</template>Dynamische Optionen mit v-for
In der Praxis kommen deine Select-Optionen selten hardcoded daher. Nutze v-for, um Optionen dynamisch aus einem Array zu rendern:
<option v-for="opt in options" :key="opt.value" :value="opt.value"> {{ opt.label }}</option>
So kannst du Optionen aus einer API laden, filtern oder sortieren — alles reaktiv.
v-model Modifizierer
Vue bietet drei eingebaute Modifizierer für v-model, die dir häufige Transformationen abnehmen. Du hängst sie einfach mit einem Punkt an:
.lazy — Synchronisierung bei Change statt Input
Standardmäßig synchronisiert v-model bei jedem input-Event, also bei jedem Tastendruck. Mit .lazy wird stattdessen erst beim change-Event synchronisiert — also wenn das Feld den Fokus verliert oder der User Enter drückt. Nützlich für Felder, bei denen du nicht bei jedem Buchstaben eine Validierung auslösen willst.
.number — Automatische Typkonvertierung
HTML-Inputs liefern immer Strings. Wenn du mit Zahlen arbeiten willst (z.B. Alter, Preis, Menge), musst du normalerweise manuell parsen. .number erledigt das automatisch — der Wert wird mit parseFloat() konvertiert. Falls die Konvertierung fehlschlägt, wird der Originalwert beibehalten.
.trim — Automatisches Whitespace-Trimming
Leerzeichen am Anfang und Ende von Eingaben sind fast immer unerwünscht. .trim entfernt sie automatisch. Perfekt für Namen, E-Mail-Adressen und alle Textfelder, bei denen Whitespace-Padding keinen Sinn macht.
<script setup>
import { ref } from 'vue'
const searchQuery = ref('')
const age = ref(0)
const email = ref('')
</script>
<template>
<div class="space-y-6">
<!-- .lazy: Synchronisiert erst bei "change" (Fokusverlust / Enter) -->
<div>
<label>Suche (.lazy):</label>
<input v-model.lazy="searchQuery" placeholder="Erst bei Enter/Blur..." />
<p>Suchbegriff: "{{ searchQuery }}"</p>
</div>
<!-- .number: Automatische Konvertierung in Number -->
<div>
<label>Alter (.number):</label>
<input v-model.number="age" type="number" />
<p>Alter: {{ age }} (Typ: {{ typeof age }})</p>
</div>
<!-- .trim: Entfernt Whitespace am Anfang und Ende -->
<div>
<label>E-Mail (.trim):</label>
<input v-model.trim="email" type="email" placeholder=" test@example.com " />
<p>E-Mail: "{{ email }}"</p>
</div>
</div>
</template>Custom v-model auf Komponenten
Das Schöne an v-model ist, dass es nicht auf native HTML-Elemente beschränkt ist. Du kannst es auch auf deinen eigenen Komponenten verwenden. Seit Vue 3.4 gibt es dafür das defineModel()-Macro, das die Implementierung drastisch vereinfacht.
<!-- CustomRating.vue — die Kindkomponente -->
<script setup>
const rating = defineModel({ default: 0 })
</script>
<template>
<div class="flex gap-1">
<button
v-for="star in 5"
:key="star"
@click="rating = star"
:class="star <= rating ? 'text-yellow-400' : 'text-gray-300'"
>
★
</button>
</div>
</template>
<!-- Elternkomponente — v-model funktioniert wie gewohnt -->
<!-- <CustomRating v-model="userRating" /> -->
<!-- <p>Bewertung: {{ userRating }} von 5</p> -->Mehr dazu im Komponenten-Kapitel
Custom v-model auf Komponenten ist ein mächtiges Konzept, das wir im Kapitel über Komponenten ausführlich behandeln. Dort lernst du auch, wie du mehrere v-model-Bindings auf einer Komponente nutzt und eigene Modifizierer definierst.
Rails-Vergleich
Wenn du Rails-Formulare gewohnt bist, fragst du dich vielleicht: Warum sollte ich das anders machen? Die kurze Antwort: Echtzeit-Feedback ohne Server-Roundtrip. Schauen wir uns den Unterschied konkret an.
Rails form_with vs. Vue v-model
In Rails baust du Formulare mit form_with und den zugehörigen Helpern. Die Daten fließen erst zum Server, wenn der User das Formular abschickt. Validierungs- feedback bekommt der User erst nach einem kompletten Request-Response-Zyklus:
<%= form_with model: @user do |f| %> <%= f.label :name %> <%= f.text_field :name %> <%= f.label :email %> <%= f.email_field :email %> <%= f.label :bio %> <%= f.text_area :bio %> <%= f.check_box :newsletter %> <%= f.label :newsletter, "Newsletter abonnieren" %> <%= f.submit "Speichern" %> <% end %>
In Vue dagegen:
<form @submit.prevent="save">
<label>Name</label>
<input v-model="user.name" />
<label>E-Mail</label>
<input v-model="user.email" type="email" />
<label>Bio</label>
<textarea v-model="user.bio" />
<label>
<input v-model="user.newsletter" type="checkbox" />
Newsletter abonnieren
</label>
<button type="submit">Speichern</button>
</form>
<!-- Live-Vorschau — aktualisiert sich bei jedem Tastendruck! -->
<p>Hallo, {{ user.name }}!</p>Der entscheidende Unterschied: Bei Rails fließen die Daten Client → Server → Client. Bei Vue bleiben sie lokal im Browser und sind sofort verfügbar. Das ermöglicht Live-Validierung, Echtzeit-Vorschauen und ein deutlich flüssigeres User-Erlebnis. Die Daten schickst du erst zum Server, wenn der User das Formular tatsächlich absendet — typischerweise per fetch() oder axios.
Interaktive Demo
Jetzt wird es praktisch! Hier ist ein vollständiges Profil-Formular, das alle Konzepte aus diesem Kapitel kombiniert. Fülle die Felder aus und beobachte, wie sich die Live-Vorschau in Echtzeit aktualisiert — ohne eine einzige Zeile für Event-Handling schreiben zu müssen.
Live-Vorschau
- Name:
- –
- E-Mail:
- –
- Bio:
- –
- Newsletter:
- ❌ Nein
- Interessen:
- –
- Erfahrung:
- –
- Framework:
- –