Modul 2: Vue Grundlagen

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.

BasicInput.vue
<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.

TextInputs.vue
<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.

Checkboxes.vue
<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.

RadioButtons.vue
<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.

SelectDropdown.vue
<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.

Modifiers.vue
<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
<!-- 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.

Profil-Formular mit Live-Vorschau
Interaktiv
Interessen
Erfahrungslevel

Live-Vorschau

Name:
E-Mail:
Bio:
Newsletter:
❌ Nein
Interessen:
Erfahrung:
Framework: