Modul 3: Komponenten

Events & emit

Kommunikation von Kind- zu Eltern-Komponenten — das Gegenstück zu Props.

Warum Events?

Props fließen nach unten, Events nach oben. Wenn eine Kind-Komponente die Eltern über eine Änderung informieren möchte, emittiert sie ein Event. Das ist Vues Variante des Observer-Patterns.

🛤️

Rails-Vergleich: Stimulus dispatch

In Stimulus nutzt du this.dispatch("increment", { detail: { count: 5 } }), um Events an übergeordnete Controller zu senden. Vue's emit funktioniert ähnlich — nur typsicher und ohne DOM-Events.

Events definieren mit defineEmits

Genau wie defineProps ist defineEmits ein Compiler-Makro, das die ausgehenden Events einer Komponente deklariert.

CounterButton.vue
<script setup lang="ts">
// TypeScript-Syntax (empfohlen)
const emit = defineEmits<{
  increment: [value: number]
  reset: []
  submit: [name: string, email: string]
}>()

function handleClick() {
  emit('increment', 1)
}

function handleReset() {
  emit('reset')
}
</script>

<template>
  <div>
    <button @click="handleClick">+1</button>
    <button @click="handleReset">Reset</button>
  </div>
</template>
💡

TypeScript-Validierung

Mit der generischen Syntax kannst du die Signatur jedes Events definieren. Der Compiler prüft dann, ob du die richtigen Argumente übergibst.

Events mit Argumenten

Events können beliebig viele Argumente mitgeben. Die Eltern-Komponente empfängt sie als Parameter im Event-Handler.

SearchInput.vue
<script setup lang="ts">
const emit = defineEmits<{
  search: [query: string, filters: { category: string }]
}>()

const query = ref('')
const category = ref('alle')

function onSearch() {
  emit('search', query.value, { category: category.value })
}
</script>

<template>
  <form @submit.prevent="onSearch">
    <input v-model="query" placeholder="Suchen..." />
    <select v-model="category">
      <option value="alle">Alle</option>
      <option value="artikel">Artikel</option>
    </select>
    <button type="submit">Suchen</button>
  </form>
</template>
Eltern-Komponente
<template>
  <SearchInput @search="handleSearch" />
</template>

<script setup lang="ts">
function handleSearch(query: string, filters: { category: string }) {
  console.log('Suche nach:', query)
  console.log('Kategorie:', filters.category)
}
</script>

v-model auf Komponenten (defineModel)

Seit Vue 3.4 gibt es defineModel — ein Makro, das Two-Way-Binding zwischen Eltern und Kind drastisch vereinfacht. Es ersetzt das manuelle Muster aus modelValue-Prop + update:modelValue-Event.

RatingStars.vue
<script setup lang="ts">
// defineModel ersetzt modelValue-Prop + emit
const rating = defineModel<number>({ required: true })
</script>

<template>
  <div class="stars">
    <button
      v-for="star in 5"
      :key="star"
      @click="rating = star"
      :class="{ active: star <= rating }"
    >
      ★
    </button>
  </div>
</template>
Verwendung
<template>
  <!-- Verwendung mit v-model -->
  <RatingStars v-model="userRating" />
  <p>Bewertung: {{ userRating }} Sterne</p>
</template>

<script setup lang="ts">
const userRating = ref(3)
</script>
ℹ️

Mehrere v-model-Bindings

Du kannst mehrere benannte Models definieren: const title = defineModel<string>('title') und dann <MyComp v-model:title="pageTitle" />.

Das manuelle v-model-Pattern (vor 3.4)

Falls du ältere Codebases siehst, wirst du dieses Muster finden. defineModel ersetzt es komplett.

CustomInput.vue (manuell)
<script setup lang="ts">
// Das manuelle Pattern (vor Vue 3.4)
const props = defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

function onInput(e: Event) {
  emit('update:modelValue', (e.target as HTMLInputElement).value)
}
</script>

<template>
  <input :value="modelValue" @input="onInput" />
</template>

Interaktive Demo

Die Kind-Komponente (Zähler) emittiert Änderungen an die Eltern-Komponente. Beobachte, wie die Events nach oben fließen:

Emit-Zähler
Interaktiv
📦 Eltern-Komponente
Gesamt: 0
Letzte Aktion:
⬆️ emit('change', wert)
🧩 Kind-Komponente (Zähler)
0
Event-Log:
Noch keine Events …

Zusammenfassung

ℹ️

Kernpunkte

  • defineEmits deklariert die ausgehenden Events einer Komponente
  • Events transportieren Daten von Kind zu Eltern
  • defineModel (Vue 3.4+) vereinfacht Two-Way-Binding enorm
  • Immer TypeScript-Generics für typsichere Event-Signaturen nutzen
  • Events benennen: camelCase im Code, kebab-case im Template