Zum Inhalt springen

Props

Auftrag

Martina Hostettler hat nach der Vorstandssitzung geschrieben. Am Tresen fragen Mitglieder immer wieder, ob die Bohrmaschine gerade da ist, und seit der Katalog aus Karten besteht, zeigt jede Karte dasselbe Ding.

Als Mitglied möchte ich im Katalog jedes Ding als Karte mit Name, Kategorie und Verfügbarkeit sehen, damit ich auf einen Blick weiss, was ich ausleihen kann.

Die Akzeptanzkriterien lauten:

  1. Jede Karte zeigt den Namen und die Kategorie des Dings.
  2. Ein ausgeliehenes Ding ist mit «Ausgeliehen» markiert, alle anderen mit «Verfügbar».
  3. Dieselbe Karte erscheint später auch auf der Seite mit den Favoriten.

Lernziele

Nach diesem Kapitel kannst du

  • einer Komponente mit defineProps Props geben und deren Typ festlegen,
  • Werte als Text oder mit einem Doppelpunkt als Ausdruck übergeben,
  • Pflicht-Props und Standardwerte einsetzen,
  • erklären, warum Daten nur von der Eltern- zur Kindkomponente fliessen,
  • entscheiden, ob eine Komponente einzelne Werte oder ein ganzes Objekt bekommt.

Du brauchst dafür die Template-Syntax aus Teil 3 und das Kapitel «Oberfläche zerlegen», in dem die Komponente ItemCard entstanden ist.

Im letzten Kapitel hast du die Karte eines Dings in die Komponente ItemCard ausgelagert. Der Katalog bindet sie dreimal ein. Schau dir die beiden Dateien an und überleg dir, was die Vorschau zeigt, bevor du das Beispiel im Playground öffnest.

App.vue

App.vue
<script setup>
import ItemCard from './ItemCard.vue'
</script>
<template>
<h1>Katalog</h1>
<div class="grid">
<ItemCard />
<ItemCard />
<ItemCard />
</div>
</template>
<style>
.grid {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
}
</style>

ItemCard.vue

ItemCard.vue
<template>
<article class="card">
<h2>Bohrmaschine</h2>
<p class="category">Werkzeug</p>
</article>
</template>
<style scoped>
.card {
padding: 0.75rem 1rem;
border: 1px solid #c8c8c8;
border-radius: 8px;
}
h2 {
margin: 0 0 0.25rem;
font-size: 1.1rem;
}
.category {
margin: 0;
color: #5f5f5f;
}
</style>

Alle drei Karten zeigen die Bohrmaschine. Am Katalog liegt das nicht. ItemCard hat Namen und Kategorie fest im Template stehen, und jede eingebundene Karte rendert dasselbe Template. Damit eine Komponente an verschiedenen Stellen Verschiedenes zeigen kann, braucht sie Eingaben von aussen. In Vue heissen diese Eingaben Props, kurz für Properties.

Eine Komponente mit Props funktioniert wie eine Funktion mit Parametern. Die Funktion legt fest, welche Werte sie erwartet, und wer sie aufruft, setzt die Werte ein. Bei ItemCard sind das der Name und die Kategorie. Die Elternkomponente, hier App.vue, besitzt die Daten und gibt sie beim Einbinden mit.

Props fliessen von der Eltern- zur Kindkomponente Oben steht die Komponente App mit dem Katalog. Drei Pfeile führen nach unten zu drei Karten der Komponente ItemCard. An jedem Pfeil stehen die Props, die App übergibt, zum Beispiel name gleich Bohrmaschine und category gleich Werkzeug. Jede Karte zeigt nur, was sie bekommen hat. Ein Pfeil zurück nach oben fehlt, die Daten fliessen nur von oben nach unten. App.vue besitzt die Daten name="Bohrmaschine" category="Werkzeug" ItemCard.vue Bohrmaschine Werkzeug name="Zelt für vier" category="Camping" ItemCard.vue Zelt für vier Camping name="Raclette-Ofen" category="Küche" ItemCard.vue Raclette-Ofen Küche Props, nur nach unten

In <script setup> deklarierst du Props mit defineProps. Du übergibst ein Objekt, in dem jeder Schlüssel eine Prop ist und der Wert beschreibt, was sie erwartet.

ItemCard.vue
<script setup>
defineProps({
name: { type: String, required: true },
category: { type: String, required: true },
})
</script>

type: String legt fest, dass die Prop Text erwartet, und required: true macht sie zur Pflicht. Im Template verwendest du Props wie Variablen, {{ name }} zeigt den Namen. defineProps musst du nicht importieren. Es ist ein Makro, das der Vue-Compiler beim Übersetzen der Komponente auflöst, darum funktioniert es nur in <script setup>.

Die Elternkomponente übergibt die Werte als Attribute, wie bei einem HTML-Element.

App.vue

App.vue
<script setup>
import ItemCard from './ItemCard.vue'
</script>
<template>
<h1>Katalog</h1>
<div class="grid">
<ItemCard name="Bohrmaschine" category="Werkzeug" />
<ItemCard name="Zelt für vier Personen" category="Camping" />
<ItemCard name="Raclette-Ofen" category="Küche" />
</div>
</template>
<style>
.grid {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
}
</style>

ItemCard.vue

ItemCard.vue
<script setup>
defineProps({
name: { type: String, required: true },
category: { type: String, required: true },
})
</script>
<template>
<article class="card">
<h2>{{ name }}</h2>
<p class="category">{{ category }}</p>
</article>
</template>
<style scoped>
.card {
padding: 0.75rem 1rem;
border: 1px solid #c8c8c8;
border-radius: 8px;
}
h2 {
margin: 0 0 0.25rem;
font-size: 1.1rem;
}
.category {
margin: 0;
color: #5f5f5f;
}
</style>

Jede Karte bekommt jetzt ihre eigenen Werte, und dieselbe Komponente zeigt drei verschiedene Dinge.

Für das zweite Akzeptanzkriterium braucht die Karte eine dritte Prop, die sagt, ob ein Ding verfügbar ist. Das ist ein Wahrheitswert, also der Typ Boolean.

ItemCard.vue
<script setup>
defineProps({
name: { type: String, required: true },
category: { type: String, required: true },
available: { type: Boolean, default: true },
})
</script>
<template>
<article class="card">
<h2>{{ name }}</h2>
<p class="category">{{ category }}</p>
<p v-if="available" class="state free">Verfügbar</p>
<p v-else class="state lent">Ausgeliehen</p>
</article>
</template>

Bei der Übergabe passiert der häufigste Fehler mit Props. Ein Attribut ohne Doppelpunkt übergibt immer Text. available="false" übergibt den Text «false», und ein Text, der nicht leer ist, gilt in JavaScript als wahr. Erst mit dem Doppelpunkt wertet Vue den Inhalt als JavaScript-Ausdruck aus, :available="false" übergibt den Wahrheitswert false. Dasselbe gilt für Zahlen, Arrays, Objekte und Variablen. Der Doppelpunkt ist die Kurzform von v-bind, die du aus Teil 3 kennst.

App.vue
<div class="grid">
<ItemCard name="Bohrmaschine" category="Werkzeug" />
<ItemCard name="Zelt für vier Personen" category="Camping" :available="false" />
<ItemCard name="Raclette-Ofen" category="Küche" :available="true" />
</div>
Im TemplateWas in der Komponente ankommt
name="Bohrmaschine"der Text «Bohrmaschine»
:available="false"der Wahrheitswert false
:deposit="20"die Zahl 20
:name="item.name"der Wert von item.name
available ohne Werttrue, das gilt nur für Props vom Typ Boolean

Die Prop available hat den Standardwert true. Fehlt sie beim Einbinden, nimmt Vue diesen Wert, darum ist die Bohrmaschine verfügbar, obwohl App.vue dazu nichts angibt. Eine Prop ist entweder Pflicht oder hat einen Standardwert, beides zusammen ergibt keinen Sinn.

Fehlt eine Pflicht-Prop oder hat sie den falschen Typ, rendert Vue die Komponente trotzdem und schreibt eine Warnung in die Konsole des Browsers, zum Beispiel Missing required prop: "category". Diese Prüfung läuft nur während der Entwicklung, im fertigen Build prüft Vue nichts mehr. Die Angaben in defineProps schützen die App also nicht zur Laufzeit. Sie helfen beim Entwickeln und dokumentieren, was eine Komponente braucht, für dich und für alle, die sie später einsetzen.

Bei Objekten und Arrays muss der Standardwert eine Funktion sein, die jedes Mal ein neues Objekt liefert, zum Beispiel default: () => []. Sonst teilen sich alle Karten dasselbe Array. Mit validator kannst du eine Prop zusätzlich prüfen lassen, etwa ob eine Zahl nicht negativ ist, mit validator: (value) => value >= 0.

Exkurs (optional) Props destrukturieren

Seit Vue 3.5 kannst du Props direkt in Variablen zerlegen und dabei den Standardwert setzen.

ItemCard.vue
<script setup>
// Since Vue 3.5 destructured props stay reactive, the default moves into the destructuring
const { name, category, available = true } = defineProps({
name: { type: String, required: true },
category: { type: String, required: true },
available: { type: Boolean },
})
</script>

Die Karte bleibt trotzdem reaktiv, weil der Compiler name intern wieder als props.name liest. Übergibst du eine solche Variable allerdings an eine andere Funktion, bekommt diese nur den aktuellen Wert. Für watch aus Teil 5 schreibst du deshalb watch(() => available, ...). Der Kurs verwendet meist const props = defineProps(...), weil dort sichtbar bleibt, was eine Prop ist.

Props sind eine Einbahnstrasse. Die Elternkomponente besitzt die Daten und gibt sie nach unten weiter, die Kindkomponente liest sie nur. Ändert sich ein Wert in App.vue, rendert Vue die betroffenen Karten neu. Die Karte selbst darf ihre Props nicht ändern.

<script setup>
const props = defineProps({ available: Boolean })
function reserve() {
props.available = false // blocked, Vue warns that props are readonly
}
</script>

Vue blockiert diese Zuweisung und warnt in der Konsole, dass die Prop schreibgeschützt ist. Der Grund ist die Nachvollziehbarkeit. Könnte jede Karte die Daten des Katalogs ändern, wüsste bald niemand mehr, woher ein Wert kommt. So gibt es für jeden Wert genau eine Komponente, die ihn besitzt und ändert.

Braucht die Karte einen Wert, den sie selbst verändert, etwa die gewünschte Anzahl Tage, legt sie dafür einen eigenen Zustand mit ref an. Muss sie der Elternkomponente etwas mitteilen, etwa dass jemand auf «Reservieren» geklickt hat, löst sie ein Event aus. Das ist das Thema des nächsten Kapitels.

Im Katalog stehen die Dinge in einem Array, und mit v-for entsteht für jedes Ding eine Karte. Statt Name, Kategorie und Verfügbarkeit einzeln zu übergeben, kannst du auch das ganze Objekt als eine Prop weitergeben.

App.vue

App.vue
<script setup>
import { ref } from 'vue'
import ItemCard from './ItemCard.vue'
const items = ref([
{ id: 1, name: 'Bohrmaschine', category: 'Werkzeug', available: true },
{ id: 2, name: 'Zelt für vier Personen', category: 'Camping', available: false },
{ id: 3, name: 'Raclette-Ofen', category: 'Küche', available: true },
{ id: 4, name: 'Festbankgarnitur', category: 'Feste', available: true },
])
</script>
<template>
<h1>Katalog</h1>
<div class="grid">
<ItemCard v-for="item in items" :key="item.id" :item="item" />
</div>
</template>
<style>
.grid {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
}
</style>

ItemCard.vue

ItemCard.vue
<script setup>
defineProps({
item: { type: Object, required: true },
})
</script>
<template>
<article class="card">
<h2>{{ item.name }}</h2>
<p class="category">{{ item.category }}</p>
<p v-if="item.available" class="state free">Verfügbar</p>
<p v-else class="state lent">Ausgeliehen</p>
</article>
</template>
<style scoped>
.card {
padding: 0.75rem 1rem;
border: 1px solid #c8c8c8;
border-radius: 8px;
}
h2 {
margin: 0 0 0.25rem;
font-size: 1.1rem;
}
.category {
margin: 0;
color: #5f5f5f;
}
.state {
margin: 0.5rem 0 0;
font-weight: 600;
}
.free {
color: #1f6b4f;
}
.lent {
color: #a3341f;
}
</style>

Beide Wege sind verbreitet, und beide haben Vor- und Nachteile.

Einzelne PropsEin Objekt als Prop
Schnittstellezeigt genau, was die Karte brauchthängt vom Aufbau der Daten ab
Wiederverwendungpasst auch zu Daten aus einer anderen Quelle, etwa einer Favoritenlisteandere Daten müssen erst dieselbe Form bekommen
Testswenige Werte genügenein passendes Objekt muss gebaut werden
Schreibaufwandgrösser, bei vielen Feldern unübersichtlichkleiner
Fallekeine besondereFelder des Objekts lassen sich ohne Warnung ändern

Vue schützt nur die Prop selbst. props.item = ... blockiert Vue, props.item.available = false dagegen nicht, denn das Objekt bleibt dasselbe, auf das auch App.vue zeigt. Die Änderung landet ohne Warnung in den Daten der Elternkomponente. Wer ein Objekt übergibt, ändert dessen Felder deshalb nie in der Kindkomponente.

Für kleine Komponenten, die nur etwas anzeigen, wie die Karte, sind einzelne Props meist die bessere Wahl. Für Komponenten, die mit einem ganzen Datensatz arbeiten, etwa ein Formular zum Bearbeiten eines Dings, passt ein Objekt besser.

Arbeite mit dem Beispiel zur Verfügbarkeit. Lies zuerst den Code und schreib auf, was du erwartest, bevor du etwas änderst.

App.vue

App.vue
<script setup>
import ItemCard from './ItemCard.vue'
</script>
<template>
<h1>Katalog</h1>
<div class="grid">
<ItemCard name="Bohrmaschine" category="Werkzeug" />
<ItemCard name="Zelt für vier Personen" category="Camping" :available="false" />
<ItemCard name="Raclette-Ofen" category="Küche" :available="true" />
</div>
</template>
<style>
.grid {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
}
</style>

ItemCard.vue

ItemCard.vue
<script setup>
defineProps({
name: { type: String, required: true },
category: { type: String, required: true },
available: { type: Boolean, default: true },
})
</script>
<template>
<article class="card">
<h2>{{ name }}</h2>
<p class="category">{{ category }}</p>
<p v-if="available" class="state free">Verfügbar</p>
<p v-else class="state lent">Ausgeliehen</p>
</article>
</template>
<style scoped>
.card {
padding: 0.75rem 1rem;
border: 1px solid #c8c8c8;
border-radius: 8px;
}
h2 {
margin: 0 0 0.25rem;
font-size: 1.1rem;
}
.category {
margin: 0;
color: #5f5f5f;
}
.state {
margin: 0.5rem 0 0;
font-weight: 600;
}
.free {
color: #1f6b4f;
}
.lent {
color: #a3341f;
}
</style>
  1. Was zeigt die dritte Karte, wenn du :available="true" ganz weglässt? Was zeigt die zweite, wenn du dort den Doppelpunkt entfernst?
  2. Öffne das Beispiel im Playground und prüfe deine Vermutungen.
  3. Entferne bei einer Karte die Kategorie. Welche Warnung erscheint in der Konsole des Playgrounds, und was zeigt die Karte?
  4. Ändere den Standardwert von available auf false. Welche Karten ändern sich, und warum genau diese?

Danach baust du in den Aufgaben selbst.

Die Aufgaben bauen auf einem kleinen Projekt auf, das den Katalog von Lorraine Commons mit sechs Dingen zeigt. Du findest es im Kursrepo im Ordner exercises/04-components/props/de. Kopiere den Ordner, installiere die Pakete und starte die Tests.

Terminal-Fenster
npm install
npm test

Mit npm run dev siehst du den Katalog im Browser. Die Tests im Ordner tests zeigen dir, welche Aufgaben erfüllt sind. Zu Beginn schlagen fast alle fehl, das ist so gewollt.

Aufgabe Einstieg

Für jedes Ding verlangt Lorraine Commons ein Pfand. Gib ItemCard eine Pflicht-Prop deposit vom Typ Number und zeig das Pfand unter der Kategorie an, zum Beispiel Pfand: CHF 20.00. Mit toFixed(2) bekommt eine Zahl zwei Stellen nach dem Punkt. Ergänze in App.vue die Übergabe. Die Tests in level-1.test.js prüfen beides.

Lösung

In ItemCard.vue kommt die Prop dazu, und das Template zeigt sie an.

ItemCard.vue
const props = defineProps({
name: { type: String, required: true },
category: { type: String, required: true },
deposit: { type: Number, required: true },
availableFrom: { type: String, default: null },
})
ItemCard.vue
<template>
<article class="card">
<h2>{{ name }}</h2>
<p class="category">{{ category }}</p>
<p>Pfand: CHF {{ deposit.toFixed(2) }}</p>
<p class="state" :class="{ lent: availableFrom !== null }">{{ state }}</p>
</article>
</template>

In App.vue braucht die Übergabe einen Doppelpunkt, :deposit="item.deposit". Ohne ihn käme der Text «item.deposit» an, Vue würde vor dem falschen Typ warnen, und der erste Test schlüge fehl.

Aufgabe Vertiefung

Ein Ding, das gerade ausgeliehen ist, hat im Datensatz ein Datum in availableFrom, sonst steht dort null. Gib der Karte eine Prop availableFrom vom Typ String mit dem Standardwert null. Die Karte zeigt «Verfügbar», solange kein Datum da ist, und sonst zum Beispiel «Wieder verfügbar ab 12.11.2026». Berechne diesen Text mit computed aus Teil 3. Damit computed an die Prop kommt, brauchst du const props = defineProps(...). Das Datum formatierst du mit new Date(props.availableFrom).toLocaleDateString('de-CH', { timeZone: 'UTC' }).

Lösung
ItemCard.vue
<script setup>
import { computed } from 'vue'
const props = defineProps({
name: { type: String, required: true },
category: { type: String, required: true },
deposit: { type: Number, required: true },
availableFrom: { type: String, default: null },
})
// A date string such as 2026-11-12 counts as midnight UTC, so it is formatted in UTC as well
const state = computed(() => {
if (props.availableFrom === null) return 'Verfügbar'
const date = new Date(props.availableFrom).toLocaleDateString('de-CH', { timeZone: 'UTC' })
return `Wieder verfügbar ab ${date}`
})
</script>

Ein Datum ohne Uhrzeit wie 2026-11-12 liest JavaScript als Mitternacht in UTC. Wer das in der Zeitzone der Schweiz formatiert, bekommt trotzdem den richtigen Tag, in Amerika aber den Vortag. Mit timeZone: 'UTC' stimmt der Tag überall. Weil state ein computed ist, rechnet Vue den Text neu, sobald sich die Prop ändert, das prüft der dritte Test.

Aufgabe Knobelei

Lege eine neue Komponente ItemList.vue an. Sie bekommt das Array der Dinge als Pflicht-Prop items, zeigt für jedes Ding eine ItemCard und gibt alle vier Werte weiter. Ist das Array leer, zeigt sie «Keine Dinge gefunden.». Baue ItemList in App.vue ein. Überlege dir danach, warum es sich lohnt, dass ItemList einzelne Props an die Karte gibt und nicht das ganze Objekt, wenn du an die Favoritenseite aus dem Auftrag denkst.

Lösung
ItemList.vue
<script setup>
import ItemCard from './ItemCard.vue'
defineProps({
items: { type: Array, required: true },
})
</script>
<template>
<p v-if="items.length === 0" class="empty">Keine Dinge gefunden.</p>
<div v-else class="grid">
<ItemCard
v-for="item in items"
:key="item.id"
:name="item.name"
:category="item.category"
:deposit="item.deposit"
:available-from="item.availableFrom"
/>
</div>
</template>
App.vue
<script setup>
import ItemList from './components/ItemList.vue'
import { items } from './items.js'
</script>
<template>
<h1>Lorraine Commons</h1>
<ItemList :items="items" />
</template>

Im Template schreibt ItemList die Prop als available-from. Vue setzt diese Schreibweise mit availableFrom gleich, in Templates sind beide erlaubt. Weil die Karte nur einzelne Werte kennt, kann die Favoritenseite sie mit Daten aus einer anderen Quelle füttern, etwa aus einer Liste von Reservationen, ohne dass diese Daten genau wie der Katalog aufgebaut sein müssen.

FehlerWas hilft
Die Kindkomponente ändert eine Prop, etwa mit props.available = false.Den Wert in der Elternkomponente ändern und ihr mit einem Event Bescheid geben (nächstes Kapitel).
deposit="20" ohne Doppelpunkt, die Prop bekommt den Text «20».Bei allem, was kein fester Text ist, den Doppelpunkt setzen. Die Warnung zum Typ in der Konsole zeigt den Fehler.
Die Kindkomponente ändert Felder eines Objekts, das sie als Prop bekommen hat.Felder nie in der Kindkomponente ändern. Die Elternkomponente informieren oder mit einer Kopie arbeiten.
Unsicherheit, ob available-from im Template und availableFrom im Script dieselbe Prop sind.Vue setzt beide Schreibweisen gleich. Im Script camelCase, im Template ist beides erlaubt.
Ein KI-Assistent schreibt props: { ... } und this.name in <script setup>.Das ist die Options API. In <script setup> gibt es kein this, Props kommen aus defineProps.
Ein KI-Assistent importiert defineProps aus vue.Den Import weglassen. defineProps ist ein Makro des Compilers, der beim Import warnt.
Ein KI-Assistent schlägt den Modifikator .sync vor..sync stammt aus Vue 2. In Vue 3 übernimmt v-model auf Komponenten diese Aufgabe, auch das kommt im nächsten Kapitel.

Selbstcheck

  1. Was ist der Unterschied zwischen available="false" und :available="false"?
  2. Eine Pflicht-Prop fehlt beim Einbinden. Was passiert während der Entwicklung, und was im fertigen Build?
  3. Warum muss der Standardwert einer Prop vom Typ Array eine Funktion sein?
  4. Eine Karte bekommt ein Objekt item und setzt item.available = false. Wer sieht diese Änderung, und warum warnt Vue nicht?
Antworten
  1. Ohne Doppelpunkt kommt der Text «false» an, und der gilt in JavaScript als wahr. Mit Doppelpunkt wertet Vue den Ausdruck aus und übergibt den Wahrheitswert false.
  2. Während der Entwicklung rendert Vue die Komponente und warnt in der Konsole. Im fertigen Build prüft Vue die Props nicht mehr, an der Stelle der fehlenden Prop bleibt die Karte einfach leer.
  3. Sonst teilen sich alle Instanzen der Komponente dasselbe Array, und eine Änderung an einer Stelle wäre überall zu sehen.
  4. Alle Komponenten, die dasselbe Objekt verwenden, also App.vue und jede andere Stelle, die dieses Ding zeigt. Vue schützt nur die Prop selbst vor einer neuen Zuweisung, nicht die Felder des Objekts dahinter.

Auftrag

Martina Hostettler gefallen die Karten, und sie hat gleich den nächsten Wunsch. Mitglieder sollen ein Ding direkt auf der Karte reservieren können. Der Knopf dafür sitzt in ItemCard, über die Seiten der App entscheidet aber App. Props tragen Daten nur nach unten, für den Weg zurück nach oben braucht es Events. Darum geht es im nächsten Kapitel.

Texte und Abbildungen stehen unter CC BY-SA 4.0, der Code unter der MIT-Lizenz und die Daten des Kurses unter CC0 1.0. Die Schrift der Abbildungen ist Kalam unter der SIL Open Font License 1.1. Die Quellen liegen auf GitHub.