Props: данни отгоре
Накратко: Props са данните, които компонентът получава от родителя, както функцията получава параметри; детето не ги сменя, а слотовете подават съдържание отвън.
Проблем
Три пъти хляб
Каталогът трябва да покаже три продукта: хляб, мляко и сирене. Картата на продукта е готова от миналия урок, затова App я слага три пъти.
Конзола
И трите карти са хляб. Продуктът е записан вътре в ProductCard, и всяка карта показва него. Другият път е BreadCard, MilkCard и CheeseCard: три копия на един и същ компонент, и всяка промяна на картата трябва да се направи три пъти. Липсва начин да кажеш на картата кой продукт да покаже.
Идеята
Props: данни отгоре
Компонентът е функция. Функцията получава данните си като параметри, а компонентът ги получава като атрибути от родителя: компонента, който го ползва. Тези атрибути се казват props (от properties). Родителят ги пише, детето ги чете.
<ProductCard :product="bread" /><ProductCard :product="milk" /><script setup>defineProps({ product: Object });</script>
<template> <h2>{{ product.name }}</h2></template>defineProps казва кои props приема компонентът. Шаблонът ги вижда директно, по име. В скрипта са в обекта, който defineProps връща: const props = defineProps(…) и после props.product.name. Двоеточието в :product е същото като при всеки атрибут: стойността е израз, а не текст.
Картата е една, а всяко ползване е с други данни. Така е и с функцията: една дефиниция, много извиквания с различни аргументи.
Виж го
Виж как текат
<script setup>defineProps({ product: Object, discount: { type: Number, default: 0 },});</script>
<template> <article> <h2>{{ product.name }}</h2> <p>Цена: {{ money(product.price * (1 - discount / 100)) }}</p> </article></template>
<!-- App.vue --><script setup>const bread = { name: 'Бял хляб', price: 1.2 };const milk = { name: 'Прясно мляко', price: 1.6 };const cheese = { name: 'Сирене', price: 7.9 };</script>
<template> <main> <h1>Хляб и мляко</h1> <div> <ProductCard :product="bread" /> <ProductCard :product="milk" /> <ProductCard :product="cheese" :discount="10" /> </div> </main></template>Стъпка по стъпка, с думи
- App държи данните: три продукта. Под него са три ProductCard: един и същи компонент три пъти, и никой още не знае какво да покаже.
- :product="bread" подава обекта bread на първата карта. Атрибутите на компонента се казват props.
- Другите две карти получават своя продукт. Третата получава и discount = 10. Всяка карта има свои props.
- Всяка карта се рисува от своите props. Хлябът и млякото не са получили discount, затова е 0 по подразбиране. Сиренето е с 10% отстъпка: 7,11 €.
- Ако картата опита props.discount = 0, нагоре не тръгва нищо: Vue предупреждава „target is readonly“ и не прави промяната. Данните в App остават същите.
- Данните текат само в една посока: от родителя надолу към детето. За да се смени нещо горе, детето трябва да помоли родителя. Това е модулът „Състояние“.
Всяка карта знае само своите props. Не вижда другите карти и не знае откъде идват данните в App.
Виж го
Една карта, три продукта
Конзола
Продуктите са в App, а ProductCard е един. Добави четвърти продукт в App (картинката img/apples.svg има) и още един <ProductCard … />. Картата не я пипаш. При 30 продукта ще искаш списък, а не 30 реда: той е следващият урок.
Пробвай
Намери грешката
Картата не се показва, а в конзолата пише „TypeError: Cannot read properties of undefined (reading 'toFixed')“.
Избери реда с грешката.
product="bread" без двоеточие подава текста „bread“, а не обекта. В текста няма price, затова product.price е undefined и money пада на toFixed. Обектът се подава с двоеточие: :product="bread". Ако картата е казала product: Object, Vue предупреждава и преди това: „Invalid prop: type check failed for prop "product". Expected Object, got String“.Идеята
Стойност по подразбиране
Някои продукти са с отстъпка. Картата получава втори prop, discount, в проценти. Повечето продукти нямат отстъпка и не е хубаво всеки ред в App да пише discount={0}. Ако prop липсва, той е undefined, както липсващ аргумент на функция. Цената става price * (1 - undefined / 100), тоест NaN.
Затова prop-ът получава стойност по подразбиране:
defineProps({ product: Object, discount: { type: Number, default: 0 },});default е стойността, ако родителят не подаде нищо. type е проверка: ако подадеш discount="10" (текст, без двоеточие), Vue предупреждава в конзолата „Invalid prop: type check failed“. Проверката е само в режима за разработка и само предупреждава.
Пробвай
Отстъпка
App подава на сиренето discount 10 (процента). Направи картата да приема discount, 0 по подразбиране, и цената да е с отстъпката: хлябът 1,20 €, млякото 1,60 €, сиренето 7,11 €.
Конзола
Решение
ProductCard.vue<script setup>
defineProps({
product: Object,
discount: { type: Number, default: 0 },
});
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
</script>
<template>
<article>
<h2>{{ product.name }}</h2>
<p>Цена: {{ money(product.price * (1 - discount / 100)) }}</p>
</article>
</template>
App.vue<script setup>
import ProductCard from './ProductCard.vue';
const bread = { name: 'Бял хляб', price: 1.2 };
const milk = { name: 'Прясно мляко', price: 1.6 };
const cheese = { name: 'Сирене', price: 7.9 };
</script>
<template>
<main>
<h1>Хляб и мляко</h1>
<div>
<ProductCard :product="bread" />
<ProductCard :product="milk" />
<ProductCard :product="cheese" :discount="10" />
</div>
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
main > div { display: flex; flex-wrap: wrap; gap: 12px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 8px 12px; width: 140px; }
article img { width: 56px; height: 56px; }
h2 { font-size: 16px; margin: 4px 0; }
p { margin: 4px 0; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; background: #fbf8f2; padding: 4px 12px; margin-top: 12px; max-width: 320px; }
button { font: inherit; margin-left: 8px; }
discount: { type: Number, default: 0 } в defineProps и цената money(product.price * (1 - discount / 100)). С discount: Number без default хлябът и млякото показват „NaN €“: проверката гледа и трите цени.
Идеята
Само за четене
Данните са на този, който ги държи. Картата може да чете своите props, но не и да ги сменя. Пробвай бутона „Още едно“: детето опитва да увеличи своя qty.
Конзола
Броят остава 2, а конзолата казва „[Vue warn] Set operation on key "qty" failed: target is readonly.“. Vue дава на компонента props само за четене и предупреждава при опит за запис.
Дори да мине, промяната щеше да е само в детето: в App пак щеше да пише 2, и при следващото рисуване App щеше да подаде 2 наново. Две места с различна истина са бъгът от „Списък от данни“. Правилото е: данните текат надолу, а промяната е работа на родителя. Как детето моли родителя да смени нещо, е модулът „Състояние“.
Идеята
Съдържание отвън
Магазинът има рамки, които изглеждат еднакво, а вътре е различно: „Доставка“ с два абзаца, „Връщане“ с един. Заглавието е prop. Съдържанието е цял HTML и е по-удобно да се пише между отварящия и затварящия таг, както при <section>.
<Card title="Доставка"> <p>До 2 работни дни.</p></Card><template> <section class="card"> <h2>{{ title }}</h2> <slot /> </section></template>Съдържанието между <Card> и </Card> влиза на мястото на <slot /> (слот). Card решава къде е слотът.
Конзола
Пробвай
Рамката
Card показва само заглавието. Направи го да показва и съдържанието, което App пише между <Card> и </Card>, под заглавието.
Конзола
Решение
Card.vue<script setup>
defineProps({ title: String });
</script>
<template>
<section class="card">
<h2>{{ title }}</h2>
<slot />
</section>
</template>
App.vue<script setup>
import Card from './Card.vue';
</script>
<template>
<main>
<h1>Кошницата</h1>
<Card title="Доставка">
<p>До 2 работни дни.</p>
<p>Безплатна над 30 €.</p>
</Card>
<Card title="Връщане">
<p>До 14 дни от доставката.</p>
</Card>
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
main > div { display: flex; flex-wrap: wrap; gap: 12px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 8px 12px; width: 140px; }
article img { width: 56px; height: 56px; }
h2 { font-size: 16px; margin: 4px 0; }
p { margin: 4px 0; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; background: #fbf8f2; padding: 4px 12px; margin-top: 12px; max-width: 320px; }
button { font: inherit; margin-left: 8px; }
<slot /> под заглавието. Именуван слот (<slot name="content" />) не става: App не казва име, затова съдържанието отива в слота по подразбиране, а такъв няма. Ако сложиш слота в <h2>, абзаците стават част от заглавието и то вече не е „Доставка“.
По-точно, за любопитните
Именувани слотове. Рамката може да има няколко места: <slot /> за основното и <slot name="footer" /> за долната част. Родителят пълни именувания слот с <template #footer>…</template>.
Имената. В скрипта prop-ът е с camelCase (productId), а в шаблона на родителя може да е и kebab-case (:product-id="7"). Vue ги свързва.
Props като променливи. От Vue 3.5 може const { product, discount = 0 } = defineProps(['product', 'discount']): стойността по подразбиране е както в JavaScript, а променливите остават свързани с родителя.
Замразено е само първото ниво. props.qty = 3 предупреждава, но props.product.price = 2 минава тихо: Vue пази от запис само първото ниво, а обектът на продукта е същият като в App. Така детето сменя данните на родителя зад гърба му. Не го прави.
Типовете. Освен type и default има required: true (предупреждение, ако родителят не подаде prop) и validator (функция, която проверява стойността). Всичко това върви само в режима за разработка. С TypeScript типовете се пишат в defineProps<{ … }>().
Упражнение
Витрината
Направи витрината от празно. ProductCard получава product и discount (0 по подразбиране) и показва картинката (alt е името), името като заглавие от второ ниво и „Цена: …“ с отстъпката. Card получава title и показва заглавието (второ ниво) и съдържанието отвън. В App, под заглавието: трите карти в div (сиренето с discount 10), после Card „Доставка“ с „До 2 работни дни.“ и „Безплатна над 30 €.“.
Конзола
Решение
App.vue<script setup>
import ProductCard from './ProductCard.vue';
import Card from './Card.vue';
const bread = { name: 'Бял хляб', price: 1.2, image: 'img/bread.svg' };
const milk = { name: 'Прясно мляко', price: 1.6, image: 'img/milk.svg' };
const cheese = { name: 'Сирене', price: 7.9, image: 'img/cheese.svg' };
</script>
<template>
<main>
<h1>Витрината</h1>
<div>
<ProductCard :product="bread" />
<ProductCard :product="milk" />
<ProductCard :product="cheese" :discount="10" />
</div>
<Card title="Доставка">
<p>До 2 работни дни.</p>
<p>Безплатна над 30 €.</p>
</Card>
</main>
</template>
ProductCard.vue<script setup>
defineProps({
product: Object,
discount: { type: Number, default: 0 },
});
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
</script>
<template>
<article>
<img :src="product.image" :alt="product.name" />
<h2>{{ product.name }}</h2>
<p>Цена: {{ money(product.price * (1 - discount / 100)) }}</p>
</article>
</template>
Card.vue<script setup>
defineProps({ title: String });
</script>
<template>
<section class="card">
<h2>{{ title }}</h2>
<slot />
</section>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
main > div { display: flex; flex-wrap: wrap; gap: 12px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 8px 12px; width: 140px; }
article img { width: 56px; height: 56px; }
h2 { font-size: 16px; margin: 4px 0; }
p { margin: 4px 0; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; background: #fbf8f2; padding: 4px 12px; margin-top: 12px; max-width: 320px; }
button { font: inherit; margin-left: 8px; }
ProductCard: defineProps({ product: Object, discount: { type: Number, default: 0 } }), <img :src="product.image" :alt="product.name" />, <h2>{{ product.name }}</h2> и цената с отстъпката. Card: defineProps({ title: String }), <section class="card">, <h2>{{ title }}</h2> и <slot />. App: <ProductCard :product="cheese" :discount="10" /> и съдържанието между <Card title="Доставка"> и </Card>. Проверката търси картините по alt, цените на трите карти и абзаците вътре в .card.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Текст, връзки, картинки, таблици · Достъпност · Списък от данни · Първият компонент
- Продължава в: Списъци и ключове · Условия: празно, грешка, зарежда · Състояние · Изчислено състояние
- Задачи по темата: Компоненти