Условия: празно, грешка, зарежда
Накратко: Условното рендериране показва само частта от разметката, която отговаря на данните (зарежда, грешка, празно или списък); във Vue с v-if, v-else и v-show.
Проблем
Зарежда, грешка и списък едновременно
Каталогът е зареден. Под заглавието обаче пише „Зареждане…“, после „Продуктите не се заредиха.“, после „Няма продукти в тази категория.“ и чак тогава са продуктите.
Конзола
Компонентът Catalog получава status и продуктите като props. Той обаче не ги гледа: рисува всичките четири части винаги. Купувачът не знае на кое да вярва, а екранният четец прочита и грешката, и продуктите.
Идеята
Частта, която отговаря на данните
Каталогът има четири състояния: зарежда, грешка, празно и готов. На екрана трябва да е само частта за текущото. Това е условно показване: компонентът решава какво да върне според данните.
Във Vue условията са атрибути в шаблона:
<p v-if="status === 'loading'" role="status">Зареждане…</p><p v-else-if="status === 'error'" role="alert">Продуктите не се заредиха.</p><p v-else-if="status === 'empty'">Няма продукти в тази категория.</p><ul v-else>…</ul>v-if рисува елемента само ако изразът е верен. v-else-if и v-else продължават веригата и трябва да са веднага след предишния елемент от нея. От веригата се рисува точно един елемент.
Двете библиотеки решават едно и също по свой начин: React с JavaScript около разметката, Vue с атрибути в нея. Резултатът е един и същ: елементът на невярното условие го няма в DOM-а. Не е скрит, а не съществува.
Тук status е даден като prop. Как каталогът го сменя сам, докато зарежда от API, е в модула „Ефекти, данни и формуляри“.
Виж го
Виж кой клон влиза
Една стойност влиза в компонента, а в DOM-а влиза само един клон. Гледай колона „DOM“ при всяка смяна на status.
<template> <p v-if="status === 'loading'">Зареждане…</p> <p v-else-if="status === 'error'">Грешка</p> <p v-else-if="status === 'empty'">Няма продукти</p> <ul v-else>…</ul></template>
<!-- в DOM, но скрито --><p v-show="status === 'loading'">Зареждане…</p>Стъпка по стъпка, с думи
- status идва в компонента като prop. Тя е 'loading': v-if съвпада и се рисува само <p>Зареждане…</p>. Другите клонове ги няма в DOM-а.
- status става 'error'. Библиотеката рисува наново: сега съвпада вторият клон. <p>Зареждане…</p> излиза от DOM-а, а съобщението за грешка влиза.
- 'empty': само съобщението, че категорията е празна.
- 'ready': нито v-if, нито v-else-if съвпада, затова се рисува v-else: списъкът. В DOM-а е винаги един клон.
- Другият начин: v-show рисува елемента винаги и сменя само display. При loading той се вижда.
- status става 'ready'. Елементът остава в DOM-а, с всичко в него, и само получава display: none. Не се вижда, а екранният четец го прескача.
- v-if маха елемента от DOM-а, v-show само го скрива. v-if е по подразбиране, а v-show е за нещо, което се сменя често или трябва да пази написаното.
Първата част е условно показване: при нова стойност старият елемент излиза от DOM-а, а новият влиза. Втората част е скриване: елементът е в DOM-а през цялото време и само се показва или скрива. За нея е краят на урока.
Пробвай
Четирите състояния
Catalog рисува всичко наведнъж. Направи го така, че във всеки раздел да е само частта на неговото състояние: „Зареждане…“ при loading, грешката при error, „Няма продукти в тази категория.“ при empty и списъкът при ready. Другите части да ги няма в страницата.
Конзола
Решение
Catalog.vue<script setup>
import ProductCard from './ProductCard.vue';
defineProps({ status: String, products: Array });
</script>
<template>
<p v-if="status === 'loading'" role="status">Зареждане…</p>
<p v-else-if="status === 'error'" role="alert">Продуктите не се заредиха.</p>
<p v-else-if="status === 'empty'">Няма продукти в тази категория.</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: 'Бял хляб', price: 1.2, stock: 5 },
{ id: 3, name: 'Сирене', price: 7.9, stock: 0 },
{ id: 2, name: 'Прясно мляко', price: 1.6, stock: 2 },
];
</script>
<template>
<main>
<section aria-label="Зарежда">
<h2>Зарежда</h2>
<Catalog status="loading" :products="[]" />
</section>
<section aria-label="Грешка">
<h2>Грешка</h2>
<Catalog status="error" :products="[]" />
</section>
<section aria-label="Празна категория">
<h2>Празна категория</h2>
<Catalog status="empty" :products="[]" />
</section>
<section aria-label="Готов">
<h2>Готов</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="`Добави ${product.name}`">Добави</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, два v-else-if и v-else на <ul>. Проверката търси във всеки раздел и съобщенията, и списъка. Ако <ul> е без v-else, той е и в раздела „Зарежда“, макар и празен: <ul> без редове пак е списък за екранния четец.Пробвай
Попълни
„Зареждане…“ трябва да се вижда само докато status е loading.
<p ="status === 'loading'" role="status">Зареждане…</p>v-if: елементът се рисува само ако изразът е верен.
Идеята
Изчерпан: значка и изключен бутон
Условието не е само за цели части. В картата на продукта то сменя текст и атрибут:
<p v-if="product.stock === 0" class="badge">Изчерпан</p><p v-else>Остават {{ product.stock }} бр.</p><button type="button" :disabled="product.stock === 0">Добави</button>:disabled="true" слага атрибута disabled, а :disabled="false" го маха.
Изключеният бутон не се натиска, не взима фокус с Tab, а екранният четец казва, че е недостъпен. Значката казва защо. Бутон, който се натиска, а после казва „няма наличност“, е по-лош: купувачът разбира едва след клика.
Пробвай
Изчерпан
Направи ProductCard така, че при stock 0 да показва „Изчерпан“ и бутонът „Добави“ да е изключен. Иначе да показва „Остават 5 бр.“ и бутонът да работи. Сиренето е изчерпано.
Конзола
Решение
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">Изчерпан</p>
<p v-else>Остават {{ product.stock }} бр.</p>
<button type="button" :aria-label="`Добави ${product.name}`" :disabled="product.stock === 0">Добави</button>
</article>
</template>
App.vue<script setup>
import ProductCard from './ProductCard.vue';
const PRODUCTS = [
{ id: 1, name: 'Бял хляб', price: 1.2, stock: 5 },
{ id: 3, name: 'Сирене', price: 7.9, stock: 0 },
{ id: 2, name: 'Прясно мляко', price: 1.6, stock: 2 },
];
</script>
<template>
<main>
<h1>Каталогът</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 и v-else избират между значката и „Остават … бр.“, а :disabled="product.stock === 0" изключва бутона. Ако условието е обърнато (v-if="product.stock"), значката е при наличните, а сиренето казва „Остават 0 бр.“.Идеята
Нулата на екрана
v-if гледа дали стойността е вярна, затова v-if="items.length" при празна кошница не рисува нищо. Нулата излиза, ако условието е в {{ }}:
<p>{{ items.length && `В кошницата: ${items.length}` }}</p> <!-- празна кошница: 0 --><p v-if="items.length > 0">В кошницата: {{ items.length }}</p> <!-- празна кошница: нищо -->&& не връща true или false. Той връща лявата страна, ако тя е невярна: тук 0, а {{ }} я показва. {{ false }} показва дори думата „false“. Условията пиши с v-if, а {{ }} остави за стойности.
Пробвай
Намери грешката
При празна кошница в горния ъгъл пише „0“.
Избери реда с грешката.
Ред 3: items.length && … при празна кошница е 0 и {{ }} го показва. Трябва <p v-if="items.length > 0">В кошницата: {{ items.length }}</p>.
Идеята
Махнато или скрито
Понякога елементът трябва да остане в DOM-а и само да не се вижда. Тук и двата панела имат поле за бележка. Отвори двата, напиши нещо, затвори ги и ги отвори пак.
Конзола
v-if: при затваряне го няма, а при отваряне е нов, с празно поле. Във втория е с v-show: той остава в DOM-а със display: none и пази написаното.Махането е по подразбиране: по-малко елементи в страницата, нищо скрито за екранния четец. Скриването е за части, които се отварят и затварят често или трябва да пазят какво е въведено в тях.
По-точно, за любопитните
v-if на <template>. За да покажеш няколко елемента при едно условие без обвивка, сложи v-if на <template>: той не влиза в DOM-а. v-show не работи на <template>, защото няма елемент, на който да сложи display: none.
Цената. v-if прави и маха елементите при всяка смяна, а v-show ги прави веднъж, още при първото рисуване, и после сменя само display. Затова v-show е за части, които се сменят често, а v-if е за части, които рядко се показват.
v-if и v-for. Във Vue 3 v-if на същия елемент като v-for се изпълнява първи и не вижда променливата от цикъла. Сложи v-if на обвивка или филтрирай данните.
Options API в стария код ползва същите v-if, v-else и v-show.
aria-hidden не е скриване. aria-hidden="true" скрива елемента само от екранния четец, а на екрана той остава. За скриване от всички е display: none: hidden, v-show или махане.
Упражнение
Кошницата
App показва три кошници: празна, с продукти и с изчерпан продукт. Направи Cart така, че празната да казва „Кошницата е празна.“, без списък и сума, и бутонът „Поръчай“ да е изключен. Пълната показва редовете, „Общо: 4,00 €“ и работещ бутон. Изчерпаният продукт има значка „Изчерпан“, над бутона пише „Махни изчерпаните продукти, за да поръчаш.“ (role alert), а бутонът е изключен.
Конзола
Решение
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">Кошницата е празна.</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">Изчерпан</span>
</li>
</ul>
<p>Общо: {{ money(total()) }}</p>
</template>
<p v-if="soldOut()" role="alert">Махни изчерпаните продукти, за да поръчаш.</p>
<button type="button" :disabled="items.length === 0 || soldOut()">Поръчай</button>
</div>
</template>
App.vue<script setup>
import Cart from './Cart.vue';
</script>
<template>
<main>
<section aria-label="Празна">
<h2>Празна</h2>
<Cart :items="[]" />
</section>
<section aria-label="С продукти">
<h2>С продукти</h2>
<Cart :items="[
{ id: 1, name: 'Бял хляб', price: 1.2, qty: 2, stock: 5 },
{ id: 2, name: 'Прясно мляко', price: 1.6, qty: 1, stock: 2 },
]" />
</section>
<section aria-label="С изчерпан продукт">
<h2>С изчерпан продукт</h2>
<Cart :items="[
{ id: 1, name: 'Бял хляб', price: 1.2, qty: 1, stock: 5 },
{ id: 3, name: 'Сирене', 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 на съобщението и v-else на <template> със списъка и сумата: така двете се сменят като едно. v-if="soldOut()" показва предупреждението, а :disabled="items.length === 0 || soldOut()" изключва бутона в двата случая. Без v-else списъкът е и в празната кошница, а проверката го търси.Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Данни от API · Първият компонент · Props: данни отгоре · Списъци и ключове
- Продължава в: Обновяване на обекти и масиви · Изчислено състояние · Вдигане на състоянието · Ефекти
- Задачи по темата: Компоненти