v-model-Unterstützung zu benutzerdefinierten Vue-Komponenten hinzufügen

Die Direktive v-model gehört zu den integrierten Direktiven von Vue.js. Sie ermöglicht eine bidirektionale Datenbindung zwischen dem Anwendungszustand und Formularelementen, indem Property-Binding und Ereignisverarbeitung in einer einzigen Syntax zusammengeführt werden.

Ohne v-model müsste jedes Formularelement manuell verbunden werden. Dazu würde :value beziehungsweise das für das jeweilige Element passende Attribut gebunden und auf input– oder change-Ereignisse reagiert, um den neuen Wert wieder in den Anwendungszustand zu übernehmen. Dieses Vorgehen funktioniert, wird bei größeren Formularen jedoch schnell wiederholend. v-model lässt sich deshalb als syntaktische Vereinfachung verstehen, die diese Bindungen in einer einzigen Direktive zusammenfasst.

Durch die bidirektionale Datenbindung aktualisieren Änderungen an Formularelementen automatisch den Anwendungszustand. Umgekehrt werden Änderungen am Zustand direkt in der Benutzeroberfläche sichtbar, ohne dass das DOM (Document Object Model) manuell verändert werden muss. Vue behält dabei einen klaren Datenfluss bei: Die Parent-Komponente bleibt die maßgebliche Datenquelle, während die Child-Komponente den Parent informiert, wenn sich ein Wert ändern soll, anstatt Props direkt zu verändern.

Dieser Artikel erläutert, wie sich v-model bei nativen Elementen und benutzerdefinierten Vue-3-Komponenten verhält, wie sich der Vertrag von Vue 2 bei einer Migration unterscheidet und wie verschiedene Implementierungsvarianten funktionieren. Dazu gehören defineModel(), die manuelle Verdrahtung von modelValue, berechnete Setter sowie ein mit Watchern synchronisierter lokaler Zustand. Außerdem werden benannte v-model-Bindungen, eigene Modifikatoren, eine wiederverwendbare validierte Eingabekomponente und die Anwendung desselben Musters auf contenteditable-Editoren behandelt.

Die wichtigsten Erkenntnisse

  • v-model vereinfacht die bidirektionale Datenbindung in Vue, indem Prop-Binding und Ereignisverarbeitung in einer Direktive kombiniert werden. Formularelemente und Anwendungszustand bleiben dadurch automatisch synchron.
  • Vue ordnet verschiedenen Formularelementen intern unterschiedliche Kombinationen aus Property und Ereignis zu. Textfelder verwenden beispielsweise value zusammen mit input, während Checkboxen checked zusammen mit change verwenden.
  • Vue 3 hat den standardmäßigen v-model-Vertrag für benutzerdefinierte Komponenten auf modelValue und update:modelValue umgestellt. Vue 2 verwendete dagegen value und input.
  • Das mit Vue 3.4 eingeführte Makro defineModel() bietet eine besonders kompakte Möglichkeit, benutzerdefinierte Komponenten mit v-model zu unterstützen, da die Prop- und Emit-Verdrahtung automatisch übernommen wird.
  • Benutzerdefinierte Vue-Komponenten sollten Props als schreibgeschützt behandeln und Aktualisierungsereignisse auslösen, anstatt Props direkt zu verändern. Dadurch bleibt der unidirektionale Datenfluss von Vue erhalten und Synchronisierungsprobleme werden vermieden.
  • Vue unterstützt unterschiedliche Verfahren zur Implementierung von v-model, darunter explizite Props und Emits, berechnete Setter und eine Synchronisierung über Watcher. Welche Variante am besten geeignet ist, hängt von der Komplexität der Komponente und der verwendeten Vue-Version ab.
  • Vue 3 unterstützt benannte Bindungen wie v-model:first-name. Dadurch kann eine einzelne Komponente mehrere bidirektionale Bindungen verwalten. Dieses Verfahren ersetzt den älteren model-Ansatz aus Vue 2.
  • Das Verhalten von v-model lässt sich für contenteditable-Elemente manuell nachbilden, indem DOM-Inhalt und Komponentenstatus mit Refs, Ereignissen und Watchern synchronisiert werden. Dieses Muster kommt häufig bei WYSIWYG- und Rich-Text-Editoren zum Einsatz.

Voraussetzungen

Um diesem Artikel folgen zu können, sollten folgende Grundlagen vorhanden sein:

  • Grundkenntnisse über Vue-Komponenten und Props
  • Vertrautheit mit JavaScript-ES-Modulen und Funktionen
  • Eine lokal eingerichtete Vue-3-Entwicklungsumgebung oder ein entsprechender Online-Playground

Die Beispiele verwenden die Composition API von Vue 3 sowie die Syntax <script setup>. Einige Abschnitte beziehen sich zusätzlich auf das Verhalten von Vue 2, um Unterschiede bei Migrationen und ältere Implementierungsverfahren zu verdeutlichen.

Wie v-model intern funktioniert

In HTML gehören input, select und textarea zu den wichtigsten Elementen, über die Benutzerdaten an eine Anwendung übergeben werden. Bei diesen nativen Elementen ist v-model kein versteckter Mechanismus, sondern eine zur Compile-Zeit aufgelöste Kurzschreibweise. Der Vue-Leitfaden zu Formularen dokumentiert, welche DOM-Property und welches Ereignis Vue für die jeweiligen Eingabeelemente miteinander kombiniert.

Abhängig vom Elementtyp entscheidet Vue, wie Werte gelesen und aktualisiert werden:

Elementtyp Property Ereignis
<input> für Text und die meisten Typen, <textarea> value input
<input type="checkbox">, <input type="radio"> checked change
<select> value change

Bei einem Textfeld kann v-model beispielsweise so verwendet werden:

Der Template-Compiler erweitert diese Schreibweise zu einem Code, der ungefähr dem folgenden Beispiel entspricht:

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

Dasselbe Prinzip verwendet Vue bei textarea, select und den jeweils passenden input-Typen. Bei Radiobuttons und Checkboxen wird checked gebunden und auf change reagiert, anstatt value mit input zu kombinieren.

Werden mehrere Checkboxen an dasselbe Array oder ein Set gebunden oder wird ein <select multiple> eingesetzt, sammelt v-model die ausgewählten Werte automatisch. Dasselbe Verhalten wird bei Checkbox-Bindungen sowie im Abschnitt zu Select-Elementen im Vue-Formularleitfaden beschrieben.

Ein wichtiges Standardverhalten besteht darin, dass v-model anfängliche value-, checked– oder selected-Attribute, die direkt im Markup eingetragen wurden, ignoriert. Vue betrachtet den JavaScript-Zustand, beispielsweise ein in <script setup> initialisiertes ref, als maßgebliche Datenquelle. Ein Anfangswert sollte deshalb im Script und nicht ausschließlich im HTML festgelegt werden.

Dasselbe Grundprinzip gilt für benutzerdefinierte Komponenten. Eine Komponente mit v-model-Unterstützung muss eine Prop entgegennehmen, die den aktuellen Wert enthält, und ein Ereignis auslösen, sobald der Parent diesen Wert aktualisieren soll. Bei Vue 3 verwenden Komponenten standardmäßig nicht value und input. Stattdessen kommen modelValue und update:modelValue zum Einsatz.

v-model-Verträge in Vue 2 und Vue 3

Vue 2 und Vue 3 verwenden unterschiedliche Standardkombinationen aus Prop und Ereignis für v-model.

Vue-Version Prop Ereignis
Vue 2 value input
Vue 3 modelValue update:modelValue

Bei Komponenten kompiliert Vue 3 v-model zu einer modelValue-Prop und einem update:modelValue-Ereignis. Dies ist der Standardvertrag für benutzerdefinierte Eingabekomponenten.

Vue 2 verwendete stattdessen eine value-Prop und ein input-Ereignis. Bei der Migration eines älteren Projekts gehört diese Änderung der Benennung zu den wichtigsten Unterschieden. Der Vue-3-Migrationsleitfaden für v-model beschreibt die entsprechenden Änderungen ausführlich.

Wird <MyInput v-model="email" /> in Vue 3 auf einer Komponente eingesetzt, erweitert der Compiler diese Schreibweise zu einer Prop-Bindung und einem Listener für Aktualisierungen, ähnlich wie bei nativen Elementen:

<MyInput
  :model-value="email"
  @update:model-value="newValue => email = newValue"
/>

Im Script kann je nach Build die camelCase-Schreibweise modelValue verwendet werden. Beide Varianten beziehen sich auf denselben Vertrag.

v-model zu benutzerdefinierten Komponenten hinzufügen

Damit eine benutzerdefinierte Komponente v-model unterstützt, muss die Child-Komponente den gebundenen Wert als Prop empfangen und den Parent informieren, sobald dieser Wert geändert werden soll. In Vue 3 heißt die Standard-Prop modelValue, während das zugehörige Ereignis update:modelValue lautet. Die Child-Komponente sollte modelValue als schreibgeschützt behandeln und den neuen Wert über update:modelValue ausgeben. Eine direkte Zuweisung an die Prop sollte vermieden werden, damit der Parent weiterhin die einzige maßgebliche Datenquelle bleibt.

Empfohlene Methode: defineModel() ab Vue 3.4

Die einfachste Variante ist das Makro defineModel() innerhalb von <script setup>. Dabei handelt es sich um ein Komfort-Makro, das vom Compiler zu einer mit einem lokalen Ref synchronisierten modelValue-Prop und einem update:modelValue-Emit erweitert wird. Ändert sich das Ref, wird das entsprechende Aktualisierungsereignis ausgelöst. Dieses Verhalten wird im Abschnitt Under the Hood des Leitfadens zu Komponenten und v-model beschrieben.

BasicInput.vue

<template>
  <input v-model="model" />
</template>

<script setup>
const model = defineModel()
</script>

Die Komponente kann anschließend folgendermaßen verwendet werden:

<BasicInput v-model="email" />

Der von defineModel() zurückgegebene Wert ist ein Ref:

  • Sein .value bleibt mit dem Wert synchron, den der Parent über v-model übergibt.
  • Verändert die Child-Komponente model.value, löst Vue update:modelValue aus, wodurch auch der Zustand im Parent aktualisiert wird.

Aus diesem Grund wird v-model innerhalb von Wrapper-Komponenten häufig direkt an ein natives Eingabeelement weitergegeben. Der Parent verwendet weiterhin die einfache v-model-Schnittstelle der benutzerdefinierten Komponente, während die eigentliche Eingabe über ein darunterliegendes <input> erfolgt.

An defineModel() können außerdem Prop-Optionen wie required: true oder ein default-Wert übergeben werden:

const model = defineModel({ required: true })
// or
const model = defineModel({ default: '' })

Beim Einsatz von default ist Vorsicht geboten, wenn der Parent kein v-model bereitstellt. Die offizielle Dokumentation zu defineModel() weist darauf hin, dass das Ref im Parent weiterhin undefined sein kann, während die Child-Komponente bereits mit einem Standardwert wie 1 initialisiert wurde. Dadurch können Parent und Child unterschiedliche Werte besitzen. Dieser Sonderfall sollte dokumentiert oder durch das Vermeiden widersprüchlicher Standardwerte berücksichtigt werden, wenn die Bindung im Parent optional ist.

Manuelle Deklaration für jede Vue-3-Version

Wenn defineModel() nicht zur Verfügung steht, beispielsweise in Vue 3.3 oder älter, können Prop und Ereignis ausdrücklich deklariert werden. Dieses Verfahren wird auch für die Verwendung vor Vue 3.4 gezeigt.

BasicInput.vue

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

Das <BasicInput v-model="email" /> des Parents wird zu derselben Kombination aus :modelValue und @update:modelValue kompiliert, die im Leitfaden zu Komponenten und v-model beschrieben wird. Das zugrunde liegende Prinzip bleibt immer gleich: Die Child-Komponente empfängt eine Prop und löst ein Ereignis aus, wenn der Parent den Wert aktualisieren soll.

In Vue 2 verwendete die entsprechende Implementierung stattdessen eine value-Prop und gab ein input-Ereignis aus:

BasicInput.vue

<template>
  <input
    :value="value"
    @input="$emit('input', $event.target.value)"
  />
</template>

<script>
export default {
  props: ['value']
}
</script>

Die Child-Komponente löst input aus, weil Vue 2 genau auf dieses Ereignis hört, wenn v-model bei benutzerdefinierten Komponenten verwendet wird. Ein anderer Ereignisname erfordert eine zusätzliche Konfiguration über die model-Option von Vue 2, die später behandelt wird.

Beim Übertragen einer solchen Komponente auf Vue 3 sollte die Prop in modelValue umbenannt, das ausgelöste Ereignis auf update:modelValue geändert und das Parent-Template entsprechend angepasst werden. Alternativ kann defineModel() eingesetzt werden, damit der Compiler die Verdrahtung übernimmt.

Implementierungsstrategien für v-model im Vergleich

Vue stellt mehrere Möglichkeiten bereit, um das Verhalten von v-model in benutzerdefinierten Komponenten umzusetzen. Alle Varianten folgen gegenüber dem Parent demselben Vertrag: standardmäßig modelValue zusammen mit update:modelValue oder einer entsprechenden Kombination für ein benanntes Argument. Die passende Methode hängt von der Vue-Version, der Komplexität der Komponente, den Anforderungen an die Synchronisierung und einer möglichen Unterstützung älterer Projekte ab.

Der Leitfaden zu Komponenten und v-model behandelt defineModel(), manuelle Props und Emits, beschreibbare Computed Properties sowie den Umgang mit Modifikatoren. Die folgende Übersicht zeigt, für welche Situationen die einzelnen Ansätze geeignet sind.

Übliche Implementierungsmuster für v-model

Ansatz Geeignet für Vorteile Nachteile
defineModel() Moderne Anwendungen ab Vue 3.4 Wenig Boilerplate, leicht lesbar und offiziell empfohlen Erfordert Vue 3.4 oder neuer
Manuelles modelValue + update:modelValue Kompatibilität ab Vue 3.0 Explizit und flexibel Mehr wiederholender Boilerplate-Code
Computed Getter/Setter Transformation oder Aufbereitung von Werten vor dem Emit Transformationslogik wird zentral gebündelt Für Einsteiger etwas schwieriger nachzuvollziehen
Lokale Refs mit Watchern Komplexe Editoren oder asynchrone Synchronisierung Nützlich, wenn sich lokaler Zustand vorübergehend vom Parent-Zustand unterscheidet Mehr Synchronisierungslogik und zusätzliche Komponenten

Für die meisten neuen Anwendungen ab Vue 3.4 ist defineModel() die bevorzugte Methode, weil wiederholende Prop- und Emit-Deklarationen entfallen und der standardmäßige v-model-Vertrag trotzdem eingehalten wird.

Computed Getter und Setter verwenden

Eine weitere häufig verwendete Variante basiert auf einer Computed Property mit Getter und Setter. Die Vue-Dokumentation beschreibt dies als alternative Möglichkeit, v-model in einer Child-Komponente umzusetzen: Der Getter liefert modelValue, während der Setter update:modelValue auslöst. Dieser Ansatz eignet sich besonders dann, wenn eine Child-Komponente Daten vor dem Zurücksenden verändern oder validieren soll, beispielsweise durch das Entfernen von Leerzeichen, das Konvertieren von Zahlen oder das Normalisieren von Benutzereingaben.

BasicInput.vue

<template>
  <input v-model="value" />
</template>

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

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const value = computed({
  get: () => props.modelValue,
  set: (newValue) => {
    emit('update:modelValue', newValue.trim())
  }
})
</script>

In diesem Beispiel entfernt der Setter überflüssige Leerzeichen, bevor der Wert mit der Parent-Komponente synchronisiert wird. Dasselbe Verfahren kann für andere Regeln eingesetzt werden, beispielsweise zur Umwandlung in Großbuchstaben, zum Entfernen ungültiger Zeichen oder zur Begrenzung eines numerischen Wertebereichs, ohne die Verwendung von v-model im Parent ändern zu müssen.

Lokalen Zustand und Watcher verwenden

Einige Komponenten wie Rich-Text-Editoren, verzögerte Suchfelder oder Formular-Builder benötigen einen vorübergehenden lokalen Zustand, bevor der Parent aktualisiert wird. In solchen Fällen soll sich der Parent nicht unbedingt bei jedem Tastendruck oder jeder DOM-Änderung verändern. Stattdessen verwaltet die Child-Komponente eine lokale Arbeitskopie und übernimmt Änderungen erst zu einem geeigneten Zeitpunkt.

Ist dieses Verhalten erforderlich, kann ein lokales Ref über watch() mit dem Parent synchronisiert werden:

SearchInput.vue

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

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const localValue = ref(props.modelValue)

watch(
  () => props.modelValue,
  (newValue) => {
    localValue.value = newValue
  }
)

watch(localValue, (newValue) => {
  if (newValue !== props.modelValue) {
    emit('update:modelValue', newValue)
  }
})
</script>

Dieser Ansatz eignet sich insbesondere für fortgeschrittene Komponenten, bei denen Aktualisierungen verzögert, transformiert oder mit externen Bibliotheken synchronisiert werden. Ein Watcher übernimmt Änderungen des Parents in localValue. Ein zweiter Watcher oder ein verzögerter Handler gibt Änderungen der Child-Komponente anschließend über emit('update:modelValue', ...) zurück. Dadurch lassen sich Endlosschleifen bei Aktualisierungen vermeiden.

Die Vue-2-Option model für ältere Projekte

In Vue 2 konnten Komponenten das Verhalten von v-model über die Option model anpassen. Dadurch ließen sich sowohl der von v-model verwendete Prop-Name als auch der Ereignisname ändern. Pro Komponente war dabei eine individuelle Kombination möglich.

Eine Checkbox-Komponente kann beispielsweise checked und change anstelle der standardmäßigen Kombination aus value und input verwenden:

CustomCheckbox.vue

<template>
  <input
    type="checkbox"
    :checked="checked"
    @change="$emit('change', $event.target.checked)"
  />
</template>

<script>
export default {
  model: {
    prop: 'checked',
    event: 'change'
  },

  props: {
    checked: Boolean
  }
}
</script>

Verwendung im Parent:

<CustomCheckbox v-model="isEnabled" />

Durch diese Konfiguration ordnet Vue v-model automatisch der checked-Prop zu und reagiert auf das Ereignis change.

Vue 3 hat dieses Verfahren durch benannte v-model-Argumente wie v-model:checked ersetzt. Diese Schreibweise ist im Template leichter zu erkennen und ermöglicht mehrere bidirektionale Bindungen innerhalb einer einzigen Komponente, ohne ein separates model-Konfigurationsobjekt verwenden zu müssen.

Benanntes v-model und mehrere Bindungen

Die aus Vue 2 bekannte model-Option zur Anpassung von Prop und Ereignis existiert in Vue 3 nicht mehr. Stattdessen wird ein Argument an v-model übergeben. Wie in der Dokumentation zu v-model-Argumenten beschrieben, synchronisiert v-model:propName die Prop propName und reagiert auf update:propName.

Diese Benennung entspricht der Vue-Konvention für Komponentenereignisse: Das Präfix update: wird mit dem Namen der Prop kombiniert. Gleichzeitig werden Konflikte mit anderen Props derselben Komponente verhindert. Das ist beispielsweise hilfreich, wenn ein Feld tatsächlich value heißt oder wenn eine Checkbox-Bindung über checked parallel zur standardmäßigen modelValue-Bindung für Text verwendet werden soll.

Bei defineModel() wird der Argumentname als erster Parameter angegeben:

BasicInput.vue

<template>
  <input type="text" v-model="hidden" />
</template>

<script setup>
const hidden = defineModel('hidden')
</script>

Verwendung im Parent:

<BasicInput v-model:hidden="email" />

Der Compiler erzeugt daraus eine :hidden-Bindung und einen @update:hidden-Listener. Dadurch entstehen keine Konflikte mit anderen Props oder mit der standardmäßigen modelValue-Bindung. Argumente in Kebab-Case wie v-model:first-name werden in Templates gemäß den üblichen Vue-Regeln für Prop-Namen auf camelCase-Props wie firstName im Script abgebildet.

Eine einzelne Komponente kann gleichzeitig mehrere v-model-Bindungen verwenden, wie in der Dokumentation zu mehreren v-model-Bindungen gezeigt wird:

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>

UserName.vue

<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

Optionale Prop-Einstellungen können als zweites Argument angegeben werden, beispielsweise defineModel('title', { required: true }).

Eigene Modifikatoren für v-model in Komponenten

Native Formularelemente unterstützen integrierte Modifikatoren wie .lazy, .number und .trim, die im Formularleitfaden beschrieben werden. Auch benutzerdefinierte Komponenten können Modifikatoren unterstützen. Wird im Parent beispielsweise v-model.capitalize="text" verwendet, erhält die Child-Komponente eine entsprechende capitalize-Information. Mit defineModel() kann der Rückgabewert über const [model, modifiers] = defineModel() destrukturiert werden. Alternativ können die Optionen get und set genutzt werden, um Werte zu transformieren. Benannte Argumente verwenden eine entsprechende {arg}Modifiers-Prop, beispielsweise titleModifiers für v-model:title.capitalize. Die Vue-Dokumentation zum Umgang mit v-model-Modifikatoren enthält vollständige Beispiele.

Praxisbeispiel: Eine wiederverwendbare Eingabekomponente erstellen

Das folgende Beispiel kombiniert mehrere der zuvor beschriebenen Konzepte in einer wiederverwendbaren Vue-3-Formularkomponente. Für den Feldwert wird defineModel() eingesetzt, während reguläre defineProps-Deklarationen für Darstellung und Validierungsregeln zuständig sind. Eine Computed Property erzeugt den abgeleiteten Fehlertext. Dadurch behält der Parent eine einfache v-model-Schnittstelle, während die Child-Komponente Beschriftungen und Validierungsmeldungen verwaltet.

Diese Komponente:

  • akzeptiert eine label-Prop
  • unterstützt v-model über defineModel()
  • zeigt Rückmeldungen zur Validierung an
  • hält den Zustand im Parent automatisch synchron

BaseInput.vue

<template>
  <label class="input-wrapper">
    <span>{{ label }}</span>

    <input
      v-model="model"
      :placeholder="placeholder"
      :class="{ invalid: errorMessage }"
    />

    <small v-if="errorMessage">
      {{ errorMessage }}
    </small>
  </label>
</template>

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

const model = defineModel()

const props = defineProps({
  label: {
    type: String,
    required: true
  },

  placeholder: {
    type: String,
    default: ''
  },

  minLength: {
    type: Number,
    default: 0
  }
})

const errorMessage = computed(() => {
  if (
    props.minLength &&
    (model.value ?? '').length < props.minLength
  ) {
    return `Input must be at least ${props.minLength} characters long.`
  }

  return ''
})
</script>

Die Komponente lässt sich in einer Parent-Komponente folgendermaßen verwenden:

<template>
  <BaseInput
    v-model="username"
    label="Username"
    placeholder="Enter your username"
    :minLength="5"
  />

  <p>Current value: {{ username }}</p>
</template>

<script setup>
import { ref } from 'vue'
import BaseInput from './BaseInput.vue'

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

Dieses Muster eignet sich gut für wiederverwendbare Formularsysteme, weil die Komponente mit dem standardmäßigen v-model-Verhalten von Vue kompatibel bleibt und gleichzeitig Validierung, Formatierung sowie zusätzliche Funktionen der Benutzeroberfläche unterstützen kann. Die Validierung in diesem Beispiel wird ausschließlich in der Child-Komponente für die Anzeige ausgeführt. Für Regeln, die ein vollständiges Formular betreffen, kann die Komponente mit einer speziellen Validierungsbibliothek oder der Constraint Validation API des Browsers kombiniert werden. Der v-model-Vertrag von BaseInput bleibt für den Parent dabei unverändert.

v-model mit contenteditable verwenden

Ein contenteditable-Element ist ein div oder ein vergleichbares Element, das so konfiguriert werden kann, dass es sich wie ein Eingabefeld verhält. Im Unterschied zu <input> und <textarea> handelt es sich jedoch nicht um ein klassisches Formularelement. Deshalb wird die integrierte Erweiterung von v-model durch Vue nicht automatisch darauf angewendet.

Ein contenteditable-Element wird erzeugt, indem das Attribut contenteditable hinzugefügt wird:

<div
  class="editor"
  contenteditable="true"
  ref="editor"
></div>

contenteditable-Elemente werden häufig für WYSIWYG-Editoren eingesetzt, weil sie sich flexibel bearbeiten lassen und von modernen Browsern breit unterstützt werden. MDN beschreibt, dass das Attribut die Werte true, false oder die Zeichenfolge "plaintext-only" akzeptiert, wenn ausschließlich einfacher Text ohne Rich-Text-Formatierung bearbeitet werden soll.

Vue bietet für contenteditable-Elemente nicht dieselbe native v-model-Unterstützung wie für Formularelemente. Der entsprechende Vertrag muss deshalb manuell umgesetzt werden: Beim input-Ereignis wird der Inhalt des Elements gelesen, Änderungen am Modell werden wieder in das DOM geschrieben und Rückkopplungsschleifen müssen vermieden werden, wenn Parent und Child denselben Knoten aktualisieren.

Das folgende Beispiel verwendet defineModel() zusammen mit einem Template Ref:

ContentEditor.vue

<template>
  <div
    ref="editorRef"
    class="editor"
    contenteditable="true"
    @input="onInput"
  ></div>
</template>

<script setup>
import { ref, watch, onMounted } from 'vue'

const model = defineModel()
const editorRef = ref(null)

function onInput() {
  const el = editorRef.value
  if (!el) return
  model.value = el.innerText
}

onMounted(() => {
  const el = editorRef.value
  if (el && model.value != null) {
    el.innerText = model.value
  }
})

watch(model, (val) => {
  const el = editorRef.value
  if (!el || el.innerText === val) return
  el.innerText = val ?? ''
})
</script>

Der Parent kann <ContentEditor v-model="content" /> anschließend genauso wie jede andere benutzerdefinierte Eingabekomponente verwenden.

Einige praktische Details sind dabei wichtig:

  • innerText im Vergleich zu innerHTML: innerText speichert einfachen Text und ist für einfache Editoren meist die sicherere Variante. innerHTML erhält HTML-Markup, muss bei Inhalten von Benutzern jedoch bereinigt werden, um XSS zu verhindern.
  • Schleifen vermeiden: Der watch-Handler prüft vor dem Schreiben el.innerText === val. Dadurch führt eine Aktualisierung aus der Child-Komponente nicht unmittelbar zu einem überflüssigen erneuten DOM-Schreibvorgang.
  • Anfangsinhalt: onMounted initialisiert den Editor, wenn im Parent bereits ein Wert vorhanden ist. Damit wird dem im Vue-Leitfaden zu Formularen beschriebenen Prinzip gefolgt, nach dem der JavaScript-Zustand die maßgebliche Datenquelle darstellt.

Bei produktiven WYSIWYG-Editoren wird häufig eine spezialisierte Bibliothek wie TipTap, Quill oder eine vergleichbare Lösung in eine eigene Komponente eingebunden. Diese Wrapper-Komponente kann weiterhin v-model über dasselbe Muster mit defineModel() oder update:modelValue bereitstellen.

Häufige Fehler bei v-model und wie sie sich vermeiden lassen

Bei der Arbeit mit v-model in benutzerdefinierten Komponenten treten einige typische Implementierungsfehler auf. Das gilt besonders beim Wechsel zwischen Mustern aus Vue 2 und Vue 3. Wer diese Probleme kennt, kann Synchronisierungsfehler und schwer nachvollziehbares Komponentenverhalten leichter vermeiden.

Props direkt verändern

Zu den häufigsten Fehlern gehört das direkte Verändern einer Prop innerhalb einer Child-Komponente.

Das folgende Beispiel ist daher nicht korrekt:

<script setup>
const props = defineProps(['modelValue'])

function updateValue(newValue) {
  props.modelValue = newValue
}
</script>

Props sind in Vue schreibgeschützt. Werden sie direkt verändert, erzeugt Vue Warnungen, da der Parent die maßgebliche Datenquelle besitzt.

Stattdessen sollte ein Aktualisierungsereignis ausgelöst oder defineModel() verwendet werden:

<script setup>
const emit = defineEmits(['update:modelValue'])

function updateValue(newValue) {
  emit('update:modelValue', newValue)
}
</script>

Alternativ lässt sich defineModel() einsetzen:

const model = defineModel()

model.value = 'Updated value'

Dadurch bleibt die vorgesehene Richtung von Zustandsänderungen erhalten, während weiterhin eine bidirektionale Datenbindung möglich ist.

In Vue 3 den falschen Ereignisnamen verwenden

Ein weiterer häufiger Fehler bei Migrationen besteht darin, in Vue-3-Komponenten weiterhin das aus Vue 2 bekannte input-Ereignis auszulösen.

Das folgende Vue-2-Muster funktioniert mit dem standardmäßigen v-model-Verhalten von Vue 3 nicht korrekt:

<input
  :value="modelValue"
  @input="$emit('input', $event.target.value)"
/>

In Vue 3 lautet das richtige Ereignis update:modelValue:

<input
  :value="modelValue"
  @input="$emit('update:modelValue', $event.target.value)"
/>

Bei der Verwendung von v-model auf einer benutzerdefinierten Komponente erwartet Vue 3 ausdrücklich das Ereignis update:modelValue.

Emits nicht deklarieren

In Vue 3 sollten Komponenten die von ihnen ausgelösten Ereignisse ausdrücklich deklarieren.

Zum Beispiel:

defineEmits(['update:modelValue'])

Auch wenn bestimmte Beispiele ohne defineEmits() weiterhin funktionieren können, kann das Weglassen zu folgenden Nachteilen führen:

  • fehlende Typinferenz
  • weniger klar definierte Komponenten-APIs
  • Laufzeitwarnungen in strengeren Konfigurationen

Die Deklaration von Emits verbessert außerdem die Verständlichkeit und Wartbarkeit einer Komponente, weil andere Entwickler unmittelbar erkennen können, welche Ereignisse von ihr unterstützt werden.

Muster aus Vue 2 und Vue 3 während einer Migration vermischen

Bei Projekten, die von Vue 2 auf Vue 3 umgestellt werden, werden häufig versehentlich Konventionen aus beiden Versionen miteinander kombiniert.

Beispiele dafür sind:

  • eine value-Prop zusammen mit update:modelValue verwenden
  • modelValue verwenden und gleichzeitig weiterhin input auslösen
  • die Vue-2-Option model mit benannten v-model-Argumenten aus Vue 3 kombinieren

Solche nicht zusammenpassenden Verträge führen normalerweise dazu, dass die Synchronisierung über v-model nicht mehr wie erwartet funktioniert.

Um die Implementierung konsistent zu halten:

  • in Vue 2 konsequent value zusammen mit input verwenden
  • in Vue 3 konsequent modelValue zusammen mit update:modelValue verwenden
  • in modernen Anwendungen ab Vue 3.4 bevorzugt defineModel() einsetzen

Watcher bei einfachen Eingabefeldern übermäßig einsetzen

Watcher sind für fortgeschrittene Anwendungsfälle wie verzögerte Eingaben, Rich-Text-Editoren oder Integrationen mit externen Bibliotheken hilfreich. Bei gewöhnlichen Formularelementen sind sie jedoch häufig nicht notwendig.

Das folgende Muster ist für ein einfaches Eingabefeld in der Regel aufwendiger als erforderlich:

watch(localValue, (newValue) => {
  emit('update:modelValue', newValue)
})

Bei einfachen Eingaben ist defineModel() oder ein Computed Setter normalerweise übersichtlicher und leichter zu warten.

Watcher sollten vor allem dann verwendet werden, wenn eine Komponente tatsächlich einen vorübergehenden lokalen Zustand oder eine asynchrone Synchronisierung benötigt.

Synchronisierung von Standardwerten übersehen

Wird an defineModel() eine default-Option übergeben, gilt dieser Standardwert ausschließlich auf Seiten der Child-Komponente. Das gebundene Ref im Parent übernimmt diesen Standardwert nicht automatisch, wenn es nicht initialisiert wurde oder der Parent v-model ganz weglässt. Die offizielle Dokumentation zu defineModel() weist darauf hin, dass die Child-Komponente einen bestimmten Wert anzeigen kann, während der Parent weiterhin undefined enthält. Dieses Verhalten kann bei Tests leicht übersehen werden.

Ein Modell kann beispielsweise einen leeren String als Fallback verwenden:

const model = defineModel({
  default: ''
})

Verwendet der Parent <MyInput /> ohne v-model oder bindet ein Ref, das zunächst undefined enthält, kann das Eingabefeld leer erscheinen, während parentRef weiterhin undefined bleibt. Die Oberfläche wirkt korrekt, aber Logik, die den Wert im Parent ausliest, erhält nicht den Standardwert der Child-Komponente.

Bei wiederverwendbaren Komponentenbibliotheken lässt sich diese Abweichung reduzieren, indem:

  • das Ref im Parent ausdrücklich initialisiert wird, beispielsweise mit const email = ref(''), bevor es über v-model übergeben wird
  • dokumentiert wird, welche Standardwerte die Child-Komponente verwendet und wann der Parent selbst einen Wert bereitstellen muss
  • widersprüchliche Standardwerte vermieden werden, wenn sowohl Parent als auch Child eigene Fallback-Werte definieren

Wer die Standardwerte auf beiden Seiten aufeinander abstimmt, kann schwer erkennbare Synchronisierungsprobleme vermeiden. Das ist insbesondere bei größeren Formularen wichtig, in denen viele Wrapper-Komponenten nach demselben Muster arbeiten.

Häufig gestellte Fragen zu v-model

Die folgenden Antworten fassen häufige Fragen zu v-model bei benutzerdefinierten Komponenten zusammen. Sie orientieren sich an der Vue-3-Dokumentation zu Komponenten und v-model sowie an dem zuvor beschriebenen Verhalten.

1. Wie verwende ich v-model in einer benutzerdefinierten Vue-Komponente?

Die Komponente muss eine Prop annehmen, die den gebundenen Wert enthält, und eine Aktualisierung auslösen, sobald sich dieser Wert ändern soll. In Vue 2 lautet die standardmäßige Prop value und das Ereignis input. In Vue 3 werden dagegen modelValue und update:modelValue verwendet. Der Parent kann anschließend <MyInput v-model="email" /> schreiben. Vue erweitert diese Syntax automatisch zur passenden Prop-Bindung und zum entsprechenden Aktualisierungs-Listener. Das folgt demselben Prinzip wie bei nativen Eingabeelementen, verwendet aber die für Komponenten vorgesehenen Prop- und Ereignisnamen. Ab Vue 3.4 kann dieser Vertrag über defineModel() umgesetzt werden, anstatt Prop und Emit manuell zu deklarieren. Das Makro wird intern weiterhin auf dieselbe Schnittstelle aus modelValue und update:modelValue abgebildet.

2. Wie unterscheidet sich v-model zwischen Vue 2 und Vue 3?

In Vue 2 bindet v-model bei einer Komponente standardmäßig an eine Prop namens value und hört auf ein input-Ereignis. Vue 3 bindet stattdessen an modelValue und reagiert auf update:modelValue. Diese Benennung passt besser zu den allgemeinen Konventionen von Vue für Props und Ereignisse. Auch die Anpassungsmöglichkeiten wurden geändert: Vue 2 verwendete die komponentenweite Option model, um eine andere Prop und ein anderes Ereignis festzulegen. Vue 3 verwendet stattdessen benannte v-model-Argumente wie v-model:title. Diese werden ohne zusätzliches Konfigurationsobjekt auf title und update:title abgebildet.

3. Was ist defineModel() in Vue 3 und wann sollte es eingesetzt werden?

defineModel() ist ein mit Vue 3.4 eingeführtes Compiler-Makro für die Verwendung innerhalb von <script setup>. Es deklariert automatisch die modelValue-Prop und das update:modelValue-Emit und liefert ein Ref zurück, das mit dem vom Parent gebundenen Wert synchron bleibt. Ab Vue 3.4 eignet sich dieses Makro besonders dann, wenn eine kurze und übersichtliche Möglichkeit zur Unterstützung des standardmäßigen v-model benötigt wird. Das gilt insbesondere für Wrapper-Komponenten, bei denen sonst in jeder Komponente defineProps und defineEmits wiederholt werden müssten. Auch reguläre Prop-Einstellungen wie required oder default können an defineModel() übergeben werden. Für eine benannte Bindung wird der Name als erstes Argument angegeben, beispielsweise defineModel('title').

4. Kann eine Vue-3-Komponente mehrere v-model-Bindungen unterstützen?

Ja. Eine einzelne Komponenteninstanz kann mehrere bidirektionale Bindungen bereitstellen. Im Parent wird für jedes Feld v-model:propName="value" verwendet, beispielsweise v-model:first-name und v-model:last-name. In der Child-Komponente entspricht jedes Argument einer eigenen Prop und einem eigenen update:propName-Ereignis. Mit defineModel() können dafür beispielsweise defineModel('firstName') und defineModel('lastName') verwendet werden. Alternativ lässt sich derselbe Vertrag manuell implementieren. Dadurch wird jede Eingabe mit dem passenden Zustand im Parent synchronisiert.

5. Was ist ein VNode in Vue und wie hängt er mit v-model zusammen?

Ein VNode oder Virtual Node ist die leichtgewichtige Beschreibung eines DOM-Elements oder einer Komponente innerhalb des virtuellen DOM-Baums von Vue. Auch bei der Verwendung von v-model wandelt der Compiler das Template in VNode-Strukturen um, an denen die entsprechenden Props und Ereignis-Listener hinterlegt sind. Für die Implementierung von v-model müssen VNodes nicht manuell erzeugt werden. Das Verständnis dieses Konzepts erklärt jedoch, weshalb Prop- und Ereignisbindungen über erneute Render-Vorgänge hinweg erhalten bleiben, ohne dass das reale DOM direkt bearbeitet werden muss.

6. Was ist die Direktive v-model in Vue?

v-model ist eine integrierte Direktive, die über eine bidirektionale Datenbindung den Anwendungszustand mit der sichtbaren Benutzereingabe verbindet. Bei nativen Elementen wie <input> stellt sie eine Kurzschreibweise für das Binden von :value oder der jeweils passenden Property wie checked bei Checkboxen sowie das Reagieren auf ein input– oder change-Ereignis dar. Der Vue-3-Leitfaden zu Formularen beschreibt die jeweiligen Kombinationen aus Property und Ereignis. Bei benutzerdefinierten Komponenten steht v-model entsprechend für die Bindung der Modell-Prop, beispielsweise modelValue, und das Lauschen auf das passende Aktualisierungsereignis. Parent und Child bleiben dadurch synchron, ohne dass in jedem Parent-Template eigene Event-Handler geschrieben werden müssen. Modifikatoren wie .trim und .number können bei nativen Eingaben eingesetzt werden. Benutzerdefinierte Komponenten können eigene Modifikatoren über defineModel() oder modelModifiers auswerten.

7. Warum sollten Props nicht direkt verändert werden, anstatt v-model korrekt zu verwenden?

Vue erwartet, dass Daten über Props vom Parent zur Child-Komponente fließen. Weist die Child-Komponente einer Prop direkt einen neuen Wert zu, beispielsweise mit props.modelValue = 'new text', erzeugt Vue zur Laufzeit eine Warnung und die maßgebliche Datenquelle im Parent wird nicht zuverlässig aktualisiert. Das vorgesehene Verfahren besteht darin, die Prop in der Child-Komponente als schreibgeschützt zu behandeln und anschließend update:modelValue oder eine benannte Variante auszulösen. Auf diese Weise kann der Parent das gebundene Ref oder den entsprechenden Datenwert aktualisieren. Genau diesem Prinzip folgt eine korrekte v-model-Implementierung unabhängig davon, ob dafür defineModel() oder explizite Emits verwendet werden.

8. Wie füge ich einer benutzerdefinierten Checkbox-Komponente v-model-Unterstützung hinzu?

Checkboxen verwenden die standardmäßige Kombination aus value beziehungsweise modelValue nicht auf dieselbe Weise wie Textfelder. Ihr Auswahlzustand wird über checked dargestellt und normalerweise durch change aktualisiert. In Vue 2 kann die model-Option der Komponente deshalb mit prop: 'checked' und event: 'change' konfiguriert werden. In Vue 3 kann im Parent eine benannte Bindung wie v-model:checked="isOn" verwendet und in der Child-Komponente defineModel('checked') eingesetzt werden. Alternativ können eine checked-Prop und ein update:checked-Emit manuell deklariert werden. Die checked-Property der nativen Checkbox wird anschließend mit diesem Modell verbunden und Änderungen werden beim change-Ereignis ausgegeben.

Fazit

Es wurde gezeigt, wie v-model bei nativen Elementen und benutzerdefinierten Vue-3-Komponenten erweitert wird. In Vue 3 verwendet der Standardvertrag modelValue und update:modelValue, während Vue 2 mit value, input und der Option model arbeitete. Derselbe Vertrag kann mit defineModel(), ausdrücklich deklarierten Props und Emits, Computed Settern oder einem über Watcher synchronisierten lokalen Zustand umgesetzt werden. Zusätzlich lässt sich das Konzept mit benannten v-model-Bindungen, eigenen Modifikatoren und einer manuellen Synchronisierung für contenteditable-Editoren erweitern, wie das wiederverwendbare Beispiel mit BaseInput zeigt. In allen Varianten besitzt der Parent die Daten, die Child-Komponente gibt Aktualisierungen aus und ein einzelnes v-model im Template sorgt für eine übersichtliche öffentliche Schnittstelle.

Für weiterführende Informationen können die offizielle Dokumentation zu v-model bei Komponenten, eine Anleitung zu v-model für native Formularelemente, ein Leitfaden zur bidirektionalen Datenbindung in benutzerdefinierten Vue-Komponenten sowie die Vue-3-Migrationshinweise zu v-model bei der Pflege von Vue-2-Code herangezogen werden. Weitere Vue.js-Ressourcen können zusätzlich Übungen und Projekte bereitstellen.

Quelle: digitalocean.com

Jetzt 200€ Guthaben sichern

Registrieren Sie sich jetzt in unserer ccloud³ und erhalten Sie 200€ Startguthaben für Ihr Projekt.

Das könnte Sie auch interessieren: