Bedingungen: leer, Fehler, lädt
Kurz gesagt: Bedingtes Rendern zeigt nur den Teil des Markups, der zu den Daten passt (lädt, Fehler, leer oder die Liste); in Vue mit v-if, v-else und v-show.
Problem
Lädt, Fehler und die Liste gleichzeitig
Der Katalog ist geladen. Unter der Überschrift steht aber „Wird geladen…“, dann „Die Produkte wurden nicht geladen.“, dann „In dieser Kategorie gibt es keine Produkte.“, und erst danach kommen die Produkte.
Konsole
Die Komponente Catalog bekommt status und die Produkte als Props. Aber sie schaut nicht darauf: Sie zeichnet immer alle vier Teile. Der Käufer weiß nicht, was er glauben soll, und ein Screenreader liest sowohl den Fehler als auch die Produkte vor.
Die Idee
Der Teil, der zu den Daten passt
Der Katalog hat vier Zustände: lädt, Fehler, leer und bereit. Nur der Teil für den aktuellen soll auf dem Bildschirm sein. Das ist bedingtes Rendern: Die Komponente entscheidet anhand der Daten, was sie zurückgibt.
In Vue sind die Bedingungen Attribute im Template:
<p v-if="status === 'loading'" role="status">Wird geladen…</p><p v-else-if="status === 'error'" role="alert">Die Produkte wurden nicht geladen.</p><p v-else-if="status === 'empty'">In dieser Kategorie gibt es keine Produkte.</p><ul v-else>…</ul>v-if zeichnet das Element nur, wenn der Ausdruck wahr ist. v-else-if und v-else setzen die Kette fort und müssen direkt nach dem vorherigen Element der Kette stehen. Genau ein Element der Kette wird gezeichnet.
Die beiden Bibliotheken lösen dasselbe auf ihre Weise: React mit JavaScript um das Markup herum, Vue mit Attributen darin. Das Ergebnis ist dasselbe: Das Element einer falschen Bedingung ist nicht im DOM. Es ist nicht versteckt, es existiert nicht.
Hier wird status als Prop übergeben. Wie der Katalog ihn beim Laden aus der API selbst ändert, steht im Modul „Effekte, Daten und Formulare“.
Sieh es dir an
Sieh, welcher Zweig hineinkommt
Ein Wert geht in die Komponente, und nur ein Zweig kommt ins DOM. Achte bei jeder Änderung von status auf die Spalte „DOM“.
<template> <p v-if="status === 'loading'">Wird geladen…</p> <p v-else-if="status === 'error'">Fehler</p> <p v-else-if="status === 'empty'">Keine Produkte</p> <ul v-else>…</ul></template>
<!-- im DOM, aber versteckt --><p v-show="status === 'loading'">Wird geladen…</p>Schritt für Schritt, in Worten
- status kommt als Prop in die Komponente. Er ist 'loading': v-if passt, und nur <p>Wird geladen…</p> wird gezeichnet. Die anderen Zweige sind gar nicht im DOM.
- status wird 'error'. Die Bibliothek zeichnet neu: Jetzt passt der zweite Zweig. <p>Wird geladen…</p> verlässt das DOM, und die Fehlermeldung kommt hinein.
- 'empty': nur die Meldung, dass die Kategorie leer ist.
- 'ready': Weder v-if noch v-else-if passt, deshalb wird v-else gezeichnet: die Liste. Im DOM ist immer ein Zweig.
- Der andere Weg: v-show zeichnet das Element immer und schaltet nur sein display um. Bei loading ist es sichtbar.
- status wird 'ready'. Das Element bleibt mit allem, was darin ist, im DOM und bekommt nur display: none. Niemand sieht es, und ein Screenreader überspringt es.
- v-if entfernt das Element aus dem DOM, v-show versteckt es nur. v-if ist der Normalfall; v-show ist für etwas, das oft umgeschaltet wird oder das Eingetippte behalten muss.
Der erste Teil ist bedingtes Rendern: Bei einem neuen Wert verlässt das alte Element das DOM, und das neue kommt hinein. Der zweite Teil ist Verstecken: Das Element ist die ganze Zeit im DOM und wird nur gezeigt oder versteckt. Darum geht es am Ende der Lektion.
Probier es aus
Die vier Zustände
Catalog zeichnet alles auf einmal. Baue es so um, dass jeder Abschnitt nur den Teil seines Zustands hat: „Wird geladen…“ bei lädt, den Fehler bei Fehler, „In dieser Kategorie gibt es keine Produkte.“ bei leer und die Liste bei bereit. Die anderen Teile dürfen nicht auf der Seite sein.
Konsole
Lösung
Catalog.vue<script setup>
import ProductCard from './ProductCard.vue';
defineProps({ status: String, products: Array });
</script>
<template>
<p v-if="status === 'loading'" role="status">Wird geladen…</p>
<p v-else-if="status === 'error'" role="alert">Die Produkte wurden nicht geladen.</p>
<p v-else-if="status === 'empty'">In dieser Kategorie gibt es keine Produkte.</p>
<ul v-else>
<li v-for="p in products" :key="p.id">
<ProductCard :product="p" />
</li>
</ul>
</template>
App.vue<script setup>
import Catalog from './Catalog.vue';
const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2, stock: 5 },
{ id: 3, name: 'Weißkäse', price: 7.9, stock: 0 },
{ id: 2, name: 'Frischmilch', price: 1.6, stock: 2 },
];
</script>
<template>
<main>
<section aria-label="Lädt">
<h2>Lädt</h2>
<Catalog status="loading" :products="[]" />
</section>
<section aria-label="Fehler">
<h2>Fehler</h2>
<Catalog status="error" :products="[]" />
</section>
<section aria-label="Leere Kategorie">
<h2>Leere Kategorie</h2>
<Catalog status="empty" :products="[]" />
</section>
<section aria-label="Bereit">
<h2>Bereit</h2>
<Catalog status="ready" :products="PRODUCTS" />
</section>
</main>
</template>
ProductCard.vue<script setup>
defineProps({ product: Object });
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
</script>
<template>
<article>
<h3>{{ product.name }}</h3>
<p>{{ money(product.price) }}</p>
<button type="button" :aria-label="`Hinzufügen ${product.name}`">Hinzufügen</button>
</article>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
v-if, zwei v-else-if und v-else am <ul>. Die Prüfung sucht in jedem Abschnitt sowohl die Meldungen als auch die Liste. Hat das <ul> kein v-else, ist es auch im Abschnitt „Lädt“, obwohl es leer ist: Ein <ul> ohne Zeilen ist für einen Screenreader trotzdem eine Liste.Probier es aus
Ergänze die Lücke
„Wird geladen…“ soll nur erscheinen, solange status loading ist.
<p ="status === 'loading'" role="status">Wird geladen…</p>v-if: Das Element wird nur gezeichnet, wenn der Ausdruck wahr ist.
Die Idee
Ausverkauft: ein Abzeichen und ein deaktivierter Button
Eine Bedingung gibt es nicht nur für ganze Teile. In der Produktkarte ändert sie einen Text und ein Attribut:
<p v-if="product.stock === 0" class="badge">Ausverkauft</p><p v-else>Nur noch {{ product.stock }} übrig</p><button type="button" :disabled="product.stock === 0">Hinzufügen</button>:disabled="true" setzt das Attribut disabled, und :disabled="false" entfernt es.
Ein deaktivierter Button lässt sich nicht drücken, bekommt mit Tab keinen Fokus, und ein Screenreader sagt, dass er nicht verfügbar ist. Das Abzeichen sagt, warum. Ein Button, den man drücken kann und der dann „nicht auf Lager“ sagt, ist schlechter: Der Käufer erfährt es erst nach dem Klick.
Probier es aus
Ausverkauft
Sorge dafür, dass ProductCard „Ausverkauft“ zeigt und den Button „Hinzufügen“ deaktiviert, wenn stock 0 ist. Sonst zeigt sie „Nur noch 5 übrig“, und der Button funktioniert. Der Käse ist ausverkauft.
Konsole
Lösung
ProductCard.vue<script setup>
defineProps({ product: Object });
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
</script>
<template>
<article>
<h3>{{ product.name }}</h3>
<p>{{ money(product.price) }}</p>
<p v-if="product.stock === 0" class="badge">Ausverkauft</p>
<p v-else>Nur noch {{ product.stock }} übrig</p>
<button type="button" :aria-label="`Hinzufügen ${product.name}`" :disabled="product.stock === 0">Hinzufügen</button>
</article>
</template>
App.vue<script setup>
import ProductCard from './ProductCard.vue';
const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2, stock: 5 },
{ id: 3, name: 'Weißkäse', price: 7.9, stock: 0 },
{ id: 2, name: 'Frischmilch', price: 1.6, stock: 2 },
];
</script>
<template>
<main>
<h1>Der Katalog</h1>
<ul>
<li v-for="p in PRODUCTS" :key="p.id">
<ProductCard :product="p" />
</li>
</ul>
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
v-if und v-else wählen zwischen dem Abzeichen und „Nur noch … übrig“, und :disabled="product.stock === 0" deaktiviert den Button. Ist die Bedingung vertauscht (v-if="product.stock"), steht das Abzeichen bei den Produkten auf Lager, und beim Käse steht „Nur noch 0 übrig“.Die Idee
Die Null auf dem Bildschirm
v-if schaut, ob der Wert wahr ist, also zeichnet v-if="items.length" bei einem leeren Warenkorb nichts. Die Null kommt heraus, wenn die Bedingung in {{ }} steht:
<p>{{ items.length && `Im Warenkorb: ${items.length}` }}</p> <!-- leerer Warenkorb: 0 --><p v-if="items.length > 0">Im Warenkorb: {{ items.length }}</p> <!-- leerer Warenkorb: nichts -->&& gibt nicht true oder false zurück. Es gibt die linke Seite zurück, wenn sie falsy ist: hier 0, und {{ }} zeigt sie. {{ false }} zeigt sogar das Wort „false“. Schreib Bedingungen mit v-if, und heb {{ }} für Werte auf.
Probier es aus
Finde den Fehler
Bei einem leeren Warenkorb steht in der oberen Ecke „0“.
Wähle die Zeile mit dem Fehler.
Zeile 3: items.length && … ist bei einem leeren Warenkorb 0, und {{ }} zeigt die 0. Es muss <p v-if="items.length > 0">Im Warenkorb: {{ items.length }}</p> heißen.
Die Idee
Entfernt oder versteckt
Manchmal muss ein Element im DOM bleiben und nur nicht zu sehen sein. Hier haben beide Panels ein Feld für eine Notiz. Öffne beide, tipp etwas ein, schließ sie und öffne sie wieder.
Konsole
v-if: Geschlossen ist es weg, und geöffnet ist es neu, mit leerem Feld. Im zweiten nutzt es v-show: Es bleibt mit display: none im DOM und behält das Eingetippte.Entfernen ist der Normalfall: weniger Elemente auf der Seite, nichts Verstecktes für einen Screenreader. Verstecken ist für Teile, die oft geöffnet und geschlossen werden oder behalten müssen, was darin eingegeben wurde.
Genauer, für Neugierige
v-if an <template>. Um mehrere Elemente unter einer Bedingung ohne Hülle zu zeigen, setz v-if an ein <template>: Es kommt nicht ins DOM. v-show funktioniert an <template> nicht, weil es kein Element gibt, dem man display: none geben könnte.
Der Preis. v-if baut und entfernt die Elemente bei jeder Änderung, während v-show sie einmal baut, beim ersten Zeichnen, und danach nur display umschaltet. v-show passt also zu Teilen, die sich oft ändern, und v-if zu Teilen, die selten gezeigt werden.
v-if und v-for. In Vue 3 läuft ein v-if am selben Element wie v-for zuerst und sieht die Schleifenvariable nicht. Setz das v-if an eine Hülle oder filtere die Daten.
Die Options API in altem Code nutzt dieselben v-if, v-else und v-show.
aria-hidden ist kein Verstecken. aria-hidden="true" versteckt das Element nur vor einem Screenreader, und es bleibt auf dem Bildschirm. Vor allen zu verstecken geht mit display: none: hidden, v-show oder Entfernen.
Übung
Der Warenkorb
App zeigt drei Warenkörbe: leer, mit Produkten und mit einem ausverkauften Produkt. Baue Cart so, dass der leere „Der Warenkorb ist leer.“ sagt, ohne Liste und ohne Summe, und der Button „Bestellen“ deaktiviert ist. Der volle zeigt die Zeilen, „Gesamt: 4,00 €“ und einen funktionierenden Button. Das ausverkaufte Produkt hat ein Abzeichen „Ausverkauft“, über dem Button steht „Entferne die ausverkauften Produkte, um zu bestellen.“ (Rolle alert), und der Button ist deaktiviert.
Konsole
Lösung
Cart.vue<script setup>
const props = defineProps({ items: Array });
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const total = () => props.items.reduce((sum, p) => sum + p.price * p.qty, 0);
const soldOut = () => props.items.some((p) => p.stock === 0);
</script>
<template>
<div>
<p v-if="items.length === 0">Der Warenkorb ist leer.</p>
<template v-else>
<ul>
<li v-for="p in items" :key="p.id">
{{ p.name }} × {{ p.qty }}
<span v-if="p.stock === 0" class="badge">Ausverkauft</span>
</li>
</ul>
<p>Gesamt: {{ money(total()) }}</p>
</template>
<p v-if="soldOut()" role="alert">Entferne die ausverkauften Produkte, um zu bestellen.</p>
<button type="button" :disabled="items.length === 0 || soldOut()">Bestellen</button>
</div>
</template>
App.vue<script setup>
import Cart from './Cart.vue';
</script>
<template>
<main>
<section aria-label="Leer">
<h2>Leer</h2>
<Cart :items="[]" />
</section>
<section aria-label="Mit Produkten">
<h2>Mit Produkten</h2>
<Cart :items="[
{ id: 1, name: 'Weißbrot', price: 1.2, qty: 2, stock: 5 },
{ id: 2, name: 'Frischmilch', price: 1.6, qty: 1, stock: 2 },
]" />
</section>
<section aria-label="Mit einem ausverkauften Produkt">
<h2>Mit einem ausverkauften Produkt</h2>
<Cart :items="[
{ id: 1, name: 'Weißbrot', price: 1.2, qty: 1, stock: 5 },
{ id: 3, name: 'Weißkäse', price: 7.9, qty: 1, stock: 0 },
]" />
</section>
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
v-if an der Meldung und v-else an einem <template> mit der Liste und der Summe: So wechseln die beiden als eins. v-if="soldOut()" zeigt die Warnung, und :disabled="items.length === 0 || soldOut()" deaktiviert den Button in beiden Fällen. Ohne v-else steht die Liste auch im leeren Warenkorb, und die Prüfung sucht genau das.Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Daten aus einer API · Die erste Komponente · Props: Daten von oben · Listen und Keys
- Geht weiter in: Objekte und Arrays aktualisieren · Abgeleiteter Zustand · Gemeinsamer Zustand: nach oben · Effekte
- Aufgaben zum Thema: Komponenten