Zum Inhalt
wannacode

Props: Daten von oben

Kurz gesagt: Props sind die Daten, die eine Komponente von ihrer Elternkomponente bekommt, so wie eine Funktion Parameter bekommt; die Kindkomponente ändert sie nicht, und Slots reichen Inhalt von außen herein.

Problem

Dreimal Brot

Der Katalog soll drei Produkte zeigen: Brot, Milch und Käse. Die Produktkarte ist aus der letzten Lektion fertig, also setzt App sie dreimal ein.

Vue

Alle drei Karten sind Brot. Das Produkt steht in ProductCard selbst, und jede Karte zeigt es. Der andere Weg wären BreadCard, MilkCard und CheeseCard: drei Kopien derselben Komponente, und jede Änderung an der Karte müsste dreimal gemacht werden. Es fehlt ein Weg, der Karte zu sagen, welches Produkt sie zeigen soll.

Die Idee

Props: Daten von oben

Eine Komponente ist eine Funktion. Eine Funktion bekommt ihre Daten als Parameter, und eine Komponente bekommt sie als Attribute von ihrer Elternkomponente: der Komponente, die sie benutzt. Diese Attribute heißen Props (von „properties“). Die Elternkomponente schreibt sie, die Kindkomponente liest sie.

App.vue
<ProductCard :product="bread" />
<ProductCard :product="milk" />
ProductCard.vue
<script setup>
defineProps({ product: Object });
</script>
<template>
<h2>{{ product.name }}</h2>
</template>

defineProps sagt, welche Props die Komponente annimmt. Das Template sieht sie direkt, unter ihrem Namen. Im Script stehen sie in dem Objekt, das defineProps zurückgibt: const props = defineProps(…), und dann props.product.name. Der Doppelpunkt in :product ist derselbe wie bei jedem Attribut: Der Wert ist ein Ausdruck, kein Text.

Es gibt eine Karte, und jede Verwendung hat andere Daten. Bei einer Funktion ist es genauso: eine Definition, viele Aufrufe mit unterschiedlichen Argumenten.

Sieh es dir an

Sieh, wie sie fließen

<script setup>
defineProps({
product: Object,
discount: { type: Number, default: 0 },
});
</script>
<template>
<article>
<h2>{{ product.name }}</h2>
<p>Preis: {{ money(product.price * (1 - discount / 100)) }}</p>
</article>
</template>
<!-- App.vue -->
<script setup>
const bread = { name: 'Weißbrot', price: 1.2 };
const milk = { name: 'Frischmilch', price: 1.6 };
const cheese = { name: 'Weißkäse', price: 7.9 };
</script>
<template>
<main>
<h1>Brot und Milch</h1>
<div>
<ProductCard :product="bread" />
<ProductCard :product="milk" />
<ProductCard :product="cheese" :discount="10" />
</div>
</main>
</template>
Schritt für Schritt, in Worten
  1. App hält die Daten: drei Produkte. Darunter stehen drei ProductCards: dieselbe Komponente dreimal, und keine weiß schon, was sie zeigen soll.
  2. :product="bread" übergibt das Objekt bread an die erste Karte. Die Attribute einer Komponente heißen Props.
  3. Die anderen beiden Karten bekommen ihr eigenes Produkt. Die dritte bekommt außerdem discount = 10. Jede Karte hat ihre eigenen Props.
  4. Jede Karte zeichnet sich aus ihren Props. Brot und Milch haben keinen discount bekommen, deshalb ist er standardmäßig 0. Der Käse ist um 10 % reduziert: 7,11 €.
  5. Versucht die Karte props.discount = 0, geht nichts nach oben: Vue warnt „target is readonly“ und führt die Änderung nicht aus. Die Daten in App bleiben gleich.
  6. Daten fließen nur in eine Richtung: von der Elternkomponente nach unten zur Kindkomponente. Um oben etwas zu ändern, muss die Kindkomponente die Elternkomponente bitten. Das ist das Modul „Zustand“.

Jede Karte kennt nur ihre eigenen Props. Sie sieht die anderen Karten nicht und weiß nicht, woher die Daten in App kommen.

Sieh es dir an

Eine Karte, drei Produkte

Vue

Die Produkte stehen in App, und es gibt eine ProductCard. Füge in App ein viertes Produkt hinzu (das Bild img/apples.svg gibt es) und noch ein <ProductCard … />. Die Karte fasst du nicht an. Bei 30 Produkten willst du eine Liste statt 30 Zeilen: Das ist die nächste Lektion.

Probier es aus

Finde den Fehler

Die Karte erscheint nicht, und in der Konsole steht „TypeError: Cannot read properties of undefined (reading 'toFixed')“.

Wähle die Zeile mit dem Fehler.

Die Idee

Ein Standardwert

Manche Produkte sind im Angebot. Die Karte bekommt ein zweites Prop, discount, in Prozent. Die meisten Produkte haben keinen Rabatt, und es ist nicht schön, wenn jede Zeile in App discount={0} schreiben muss. Ein fehlendes Prop ist undefined, wie ein fehlendes Argument einer Funktion. Der Preis wird zu price * (1 - undefined / 100), also NaN.

Deshalb bekommt das Prop einen Standardwert:

defineProps({
product: Object,
discount: { type: Number, default: 0 },
});

default ist der Wert, wenn die Elternkomponente nichts übergibt. type ist eine Prüfung: Übergibst du discount="10" (Text, ohne Doppelpunkt), warnt Vue in der Konsole „Invalid prop: type check failed“. Die Prüfung läuft nur im Entwicklungsmodus und warnt nur.

Probier es aus

Ein Rabatt

App gibt dem Käse einen discount von 10 (Prozent). Sorge dafür, dass die Karte discount annimmt, standardmäßig 0, und den Preis mit Rabatt zeigt: das Brot 1,20 €, die Milch 1,60 €, der Käse 7,11 €.

Vue

Die Idee

Schreibgeschützt

Die Daten gehören dem, der sie hält. Die Karte kann ihre Props lesen, aber nicht ändern. Probier den Button „Noch eins“: Die Kindkomponente versucht, ihr eigenes qty zu erhöhen.

Vue

Die Menge bleibt 2, und die Konsole meldet „[Vue warn] Set operation on key "qty" failed: target is readonly.“. Vue gibt der Komponente schreibgeschützte Props und warnt bei einem Schreibversuch.

Selbst wenn es durchginge, wäre die Änderung nur in der Kindkomponente: App würde weiter 2 sagen und bei der nächsten Zeichnung wieder 2 übergeben. Zwei Orte mit verschiedenen Wahrheiten sind der Fehler aus „Eine Liste aus Daten“. Die Regel: Daten fließen nach unten, und sie zu ändern ist Sache der Elternkomponente. Wie eine Kindkomponente ihre Elternkomponente bittet, etwas zu ändern, ist das Modul „Zustand“.

Die Idee

Inhalt von außen

Der Shop hat Rahmen, die gleich aussehen und Verschiedenes enthalten: „Lieferung“ mit zwei Absätzen, „Rückgabe“ mit einem. Der Titel ist ein Prop. Der Inhalt ist ein ganzes Stück HTML, und es ist handlicher, es zwischen das öffnende und das schließende Tag zu schreiben, wie bei <section>.

<Card title="Lieferung">
<p>Innerhalb von 2 Werktagen.</p>
</Card>
<template>
<section class="card">
<h2>{{ title }}</h2>
<slot />
</section>
</template>

Der Inhalt zwischen <Card> und </Card> kommt dorthin, wo <slot /> steht. Card entscheidet, wo der Slot ist.

Vue

Probier es aus

Der Rahmen

Card zeigt nur den Titel. Sorge dafür, dass es unter dem Titel auch den Inhalt zeigt, den App zwischen <Card> und </Card> schreibt.

Vue
Genauer, für Neugierige

Benannte Slots. Ein Rahmen kann mehrere Stellen haben: <slot /> für den Hauptteil und <slot name="footer" /> für den unteren Teil. Die Elternkomponente füllt einen benannten Slot mit <template #footer>…</template>.

Namen. Im Script ist ein Prop camelCase (productId), und im Template der Elternkomponente darf es auch kebab-case sein (:product-id="7"). Vue verbindet beide.

Props als Variablen. Seit Vue 3.5 kannst du const { product, discount = 0 } = defineProps(['product', 'discount']) schreiben: Der Standardwert funktioniert wie in JavaScript, und die Variablen bleiben mit der Elternkomponente verbunden.

Nur die erste Ebene ist eingefroren. props.qty = 3 warnt, aber props.product.price = 2 geht still durch: Vue schützt nur die erste Ebene vor dem Schreiben, und das Produktobjekt ist dasselbe wie in App. So ändert die Kindkomponente die Daten der Elternkomponente hinter deren Rücken. Tu das nicht.

Typen. Neben type und default gibt es required: true (eine Warnung, wenn die Elternkomponente das Prop nicht übergibt) und validator (eine Funktion, die den Wert prüft). Alles läuft nur im Entwicklungsmodus. Mit TypeScript schreibt man die Typen in defineProps<{ … }>().

Übung

Das Schaufenster

Bau das Schaufenster von Grund auf. ProductCard nimmt product und discount (standardmäßig 0) und zeigt das Bild (alt ist der Name), den Namen als Überschrift der Ebene zwei und „Preis: …“ mit dem Rabatt. Card nimmt title und zeigt den Titel (Ebene zwei) und den Inhalt von außen. In App, unter der Überschrift: die drei Karten in einem div (der Käse mit discount 10), dann eine Card „Lieferung“ mit „Innerhalb von 2 Werktagen.“ und „Kostenlos ab 30 €.“.

Vue

Test

Teste dich

1. Was sind Props?
2. Was bekommt die Karte von <ProductCard product="bread" />?
3. Die Karte bekommt qty und will 1 dazuzählen. Was stimmt?
4. App schreibt <Card title="Lieferung"><p>Innerhalb von 2 Werktagen.</p></Card>. Wie zeigt Card den Absatz?
5. App schreibt <ProductCard product={bread} /> ohne discount. Was ist discount in der Karte?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·