Към съдържанието
wannacode

Props: данни отгоре

Накратко: Props са данните, които компонентът получава от родителя, както функцията получава параметри; детето не ги сменя, а слотовете подават съдържание отвън.

Проблем

Три пъти хляб

Каталогът трябва да покаже три продукта: хляб, мляко и сирене. Картата на продукта е готова от миналия урок, затова App я слага три пъти.

Vue

И трите карти са хляб. Продуктът е записан вътре в ProductCard, и всяка карта показва него. Другият път е BreadCard, MilkCard и CheeseCard: три копия на един и същ компонент, и всяка промяна на картата трябва да се направи три пъти. Липсва начин да кажеш на картата кой продукт да покаже.

Идеята

Props: данни отгоре

Компонентът е функция. Функцията получава данните си като параметри, а компонентът ги получава като атрибути от родителя: компонента, който го ползва. Тези атрибути се казват props (от properties). Родителят ги пише, детето ги чете.

App.vue
<ProductCard :product="bread" />
<ProductCard :product="milk" />
ProductCard.vue
<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>
Стъпка по стъпка, с думи
  1. App държи данните: три продукта. Под него са три ProductCard: един и същи компонент три пъти, и никой още не знае какво да покаже.
  2. :product="bread" подава обекта bread на първата карта. Атрибутите на компонента се казват props.
  3. Другите две карти получават своя продукт. Третата получава и discount = 10. Всяка карта има свои props.
  4. Всяка карта се рисува от своите props. Хлябът и млякото не са получили discount, затова е 0 по подразбиране. Сиренето е с 10% отстъпка: 7,11 €.
  5. Ако картата опита props.discount = 0, нагоре не тръгва нищо: Vue предупреждава „target is readonly“ и не прави промяната. Данните в App остават същите.
  6. Данните текат само в една посока: от родителя надолу към детето. За да се смени нещо горе, детето трябва да помоли родителя. Това е модулът „Състояние“.

Всяка карта знае само своите props. Не вижда другите карти и не знае откъде идват данните в App.

Виж го

Една карта, три продукта

Vue

Продуктите са в App, а ProductCard е един. Добави четвърти продукт в App (картинката img/apples.svg има) и още един <ProductCard … />. Картата не я пипаш. При 30 продукта ще искаш списък, а не 30 реда: той е следващият урок.

Пробвай

Намери грешката

Картата не се показва, а в конзолата пише „TypeError: Cannot read properties of undefined (reading 'toFixed')“.

Избери реда с грешката.

Идеята

Стойност по подразбиране

Някои продукти са с отстъпка. Картата получава втори 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 €.

Vue

Идеята

Само за четене

Данните са на този, който ги държи. Картата може да чете своите props, но не и да ги сменя. Пробвай бутона „Още едно“: детето опитва да увеличи своя qty.

Vue

Броят остава 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 решава къде е слотът.

Vue

Пробвай

Рамката

Card показва само заглавието. Направи го да показва и съдържанието, което App пише между <Card> и </Card>, под заглавието.

Vue
По-точно, за любопитните

Именувани слотове. Рамката може да има няколко места: <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 €.“.

Vue

Тест

Провери се

1. Какво са props?
2. Какво получава картата от <ProductCard product="bread" />?
3. Картата получава qty и иска да го увеличи с 1. Кое е вярно?
4. App пише <Card title="Доставка"><p>До 2 работни дни.</p></Card>. Как Card показва абзаца?
5. App пише <ProductCard product={bread} /> без discount. Какво е discount в картата?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·