Základy komponent
Komponenty nám umožňují rozdělit UI na nezávislé a znovupoužitelné části a přemýšlet o každé části samostatně. Je běžné, že je aplikace organizována do stromu vnořených komponent:
Je to velmi podobné tomu, jak vnořujeme nativní HTML elementy, ale Vue implementuje svůj vlastní model komponent, který nám umožňuje zapouzdřit do každé komponenty její vlastní obsah a logiku. Vue také dobře funguje s nativními Web Components. Pokud vás zajímá vztah mezi Vue komponentami a Web Components, přečtěte si více zde.
Definice komponenty
Při použití build fáze obvykle definujeme každou Vue komponentu ve vyhrazeném souboru pomocí přípony .vue
– známém jako Single-File komponenta (zkráceně SFC):
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">Klikli jste na mě {{ count }} krát.</button>
</template>
Když nepoužíváte build fázi, lze Vue komponentu definovat jako prostý JavaScript objekt obsahující vlastnosti specifické pro Vue:
js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">
Klikli jste na mě {{ count }} krát.
</button>`
// může také adresovat in-DOM šablonu
// template: '#my-template-element'
}
Šablona je zde vložena inline jako JavaScript string, který Vue za běhu zkompiluje. Můžete také použít ID selektor ukazující na element (obvykle nativní <template>
elementy) – Vue použije jeho obsah jako zdroj pro šablonu.
Výše uvedený příklad definuje jednu komponentu a exportuje ji jako default export souboru .js
. Můžete však použít pojmenované exporty (named exports) k exportu více komponent ze stejného souboru.
Použití komponenty
TIP
Ve zbytku tohoto průvodce budeme používat SFC syntaxi – koncepty týkající se komponent jsou stejné bez ohledu na to, zda build fázi používáte nebo ne.
Sekce Příklady ukazuje použití komponent v obou scénářích.
Abychom mohli použít komponentu potomka, musíme ji do komponenty rodiče importovat. Za předpokladu, že jsme naši komponentu „counter“ tlačítka umístili do souboru s názvem ButtonCounter.vue
, bude tato komponenta vystavena jako default export souboru:
vue
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<h1>Zde je komponenta potomka!</h1>
<ButtonCounter />
</template>
S využitím <script setup>
budou importované komponenty v šabloně dostupné automaticky.
Je také možné zaregistrovat komponentu globálně a zpřístupnit ji všem komponentám v dané aplikaci, aniž byste ji museli importovat. Klady a zápory globální vs. lokální registrace jsou rozebírány ve vyhrazené části Registrace komponent.
Komponenty lze použít opakovaně, kolikrát budete chtít:
template
<h1>Zde jsou komponenty potomků!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
Všimněte si, že při kliknutí na tlačítka si každé z nich zachovává svůj vlastní, samostatný count
. Je to proto, že pokaždé, když komponentu použijete, vytvoří se nová instance.
V SFC se pro názvy tagů podřízených komponent doporučuje používat PascalCase
, aby se odlišily od nativních HTML elementů. Přestože nativní názvy HTML elementů malá a velká písmena nerozlišují, Vue SFC je kompilovaný formát, takže v něm názvy rozlišující malá a velká písmena používat můžete. K uzavření tagu můžeme také použít />
.
Pokud vaše šablony vytváříte přímo v DOM (např. jako obsah nativního elementu <template>
), bude šablona při analýze HTML podléhat nativnímu chování prohlížeče. V takových případech budete muset pro názvy komponent použít kebab-case
a explicitní uzavírací tagy:
template
<!-- pokud je šablona napsaná v DOM -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
Pro více detailů se podívejte na upozornění na omezení při analýze in-DOM šablon.
Předávání vlastností (props)
Pokud vytváříme blog, budeme pravděpodobně potřebovat komponentu představující příspěvek na blogu. Chceme, aby všechny příspěvky sdílely stejné vizuální rozvržení, ale s jiným obsahem. Taková komponenta nebude užitečná, pokud jí nebudete moci předat data, jako je název a obsah konkrétního příspěvku, který chceme zobrazit. Zde přicházejí na řadu vlastnosti (props).
Props jsou vlastní atributy, kter�� můžete na komponentě zaregistrovat. Abychom naší komponentě předali název blogového příspěvku, musíme jej deklarovat v seznamu vlastností, které tato komponenta přijímá, pomocí makra defineProps
:
vue
<script setup>
defineProps(['titulek'])
</script>
<template>
<h4>{{ titulek }}</h4>
</template>
defineProps
je makro překladače, které je dostupné pouze ve <script setup>
a nemusí být explicitně importováno. Deklarované vlastnosti jsou automaticky zpřístupněny šabloně. defineProps
také vrátí objekt, který obsahuje všechny vlastnosti předané komponentě, takže k nim můžeme v případě potřeby přistupovat v JavaScriptu:
js
const props = defineProps(['titulek'])
console.log(props.titulek)
Viz také: Typování vlastností komponenty
Pokud nepoužíváte <script setup>
, props by měly být deklarovány pomocí možnosti props
a props objekt předán funkci setup()
jako první parametr:
js
export default {
props: ['titulek'],
setup(props) {
console.log(props.titulek)
}
}
Komponenta může mít tolik props, kolik chcete, a ve výchozím nastavení lze libovolné z nich předat libovolnou hodnotu.
Jakmile je vlastnost zaregistrována, můžete jí předávat data skrz vlastní atribut, například takto:
template
<BlogPost titulek="Moje cesta k Vue" />
<BlogPost titulek="Blogování s Vue" />
<BlogPost titulek="Proč je Vue tak zábavné" />
V typické aplikaci však pravděpodobně budete mít v komponentě rodiče pole příspěvků:
js
const posts = ref([
{ id: 1, titulek: 'Moje cesta k Vue' },
{ id: 2, titulek: 'Blogování s Vue' },
{ id: 3, titulek: 'Proč je Vue tak zábavné' }
])
A potom pro každý z nich vykreslit jeho vlastní komponentu pomocí v-for
:
template
<BlogPost
v-for="post in posts"
:key="post.id"
:titulek="post.titulek"
/>
Všimněte si, jak je k předávání dynamických prop hodnot použitá zkrácená v-bind
syntaxe (:titulek="post.titulek"
). To je užitečné zejména tehdy, když předem přesně nevíte, jaký obsah se chystáte vykreslit.
To je zatím vše, co o vlastnostech (props) potřebujete vědět. Poté, co si přečtete tuto stránku a budete se s jejím obsahem cítit seznámeni, však doporučujeme později se vrátit a přečíst si úplného průvodce pro Vlastnosti (Props).
Naslouchání událostem (events)
Jak vyvíjíme naši komponentu <BlogPost>
, některé funkce mohou vyžadovat zpětnou komunikaci do komponenty rodiče. Můžeme se například rozhodnout zahrnout funkci usnadnění pro zvětšení textu blogových příspěvků, zatímco zbytek stránky ponecháme ve výchozí velikosti.
V komponentě rodiče můžeme tuto funkci podporovat přidáním ref hodnoty postFontSize
:
js
const posts = ref([
/* ... */
])
const postFontSize = ref(1)
Která může být použita v šabloně k ovládání velikosti písma všech blogových příspěvků:
template
<div :style="{ fontSize: postFontSize + 'em' }">
<BlogPost
v-for="post in posts"
:key="post.id"
:titulek="post.titulek"
/>
</div>
Nyní pojďme přidat tlačítko do šablony <BlogPost>
komponenty:
vue
<!-- sekce <script> vynechána -->
<template>
<div class="blog-post">
<h4>{{ titulek }}</h4>
<button>Zvětšit text</button>
</div>
</template>
Tlačítko zatím nic nedělá – chceme kliknutím na tlačítko sdělit komponentě rodiče, že má zvětšit text všech příspěvků. K vyřešení tohoto problému poskytují komponenty vlastní systém událostí (events). Rodič se může rozhodnout poslouchat libovolnou událost na instanci komponenty potomka pomocí v-on
nebo @
, stejně jako bychom to dělali s nativní událostí DOM:
template
<BlogPost
...
@zvetsit-text="postFontSize += 0.1"
/>
Potom může komponenta potomka vyvolat událost sama na sobě voláním vestavěné metody $emit
a předáním názvu události:
vue
<!-- sekce <script> vynechána -->
<template>
<div class="blog-post">
<h4>{{ titulek }}</h4>
<button @click="$emit('zvetsit-text')">Zvětšit text</button>
</div>
</template>
Díky event listeneru @zvetsit-text="postFontSize += 0.1"
obdrží komponenta rodiče volání a provede aktualizaci hodnoty postFontSize
.
Vysílané (emit) události můžeme nepovinně deklarovat s pomocí makra defineEmits
:
vue
<script setup>
defineProps(['titulek'])
defineEmits(['zvetsit-text'])
</script>
To dokumentuje všechny události, které komponenta vysílá, a volitelně je validuje. Také to Vue umožňuje vyhnout se jejich implicitnímu použití jako nativních event listenerů na root elementu komponenty potomka.
Stejně jako defineProps
, je i defineEmits
použitelné pouze ve <script setup>
a není třeba ho importovat. Vrací funkci emit
, která je ekvivalentní metodě $emit
. Může být použita k vyvolání událostí uvnitř <script setup>
v komponentě, kde není $emit
přímo dostupné:
vue
<script setup>
const emit = defineEmits(['zvetsit-text'])
emit('zvetsit-text')
</script>
Viz také: Typování emitovaných událostí komponenty
Pokud nepoužíváte <script setup>
, můžete deklarovat emitované události prostřednictvím možnosti emits
. K funkci emit
můžete přistupit jako k vlastnosti setup kontextu (předávaný do setup()
jako druhý parametr):
js
export default {
emits: ['zvetsit-text'],
setup(props, ctx) {
ctx.emit('zvetsit-text')
}
}
To je zatím vše, co potřebujete vědět o vlastních událostech komponenty. Poté, co si přečtete tuto stránku a budete se s jejím obsahem cítit seznámeni, však doporučujeme později se vrátit a přečíst si úplného průvodce pro Události komponent (Events).
Distribuce obsahu pomocí slotů (slots)
Stejně jako u HTML elementů je často užitečné mít možnost předat obsah komponentě, jako je tato:
template
<AlertBox>
Stala se chyba.
</AlertBox>
Což by mohlo vykreslit něco jako:
Toto je chyba pro testovací účely
Stala se chyba.
Toho lze dosáhnout použitím speciálního Vue elementu <slot>
:
vue
<template>
<div class="alert-box">
<strong>Toto je chyba pro testovací účely</strong>
<slot />
</div>
</template>
<style scoped>
.alert-box {
/* ... */
}
</style>
Jak vidíte výše, používáme <slot>
jako zástupný symbol v místě, kde chceme umístit obsah – a to je vše. Máme hotovo!
To je zatím vše, co potřebujete vědět o slotech. Poté, co si přečtete tuto stránku a budete se s jejím obsahem cítit seznámeni, však doporučujeme později se vrátit a přečíst si úplného průvodce pro Sloty (Slots).
Dynamické komponenty
Někdy je užitečné mezi komponentami dynamicky přepínat, například v rozhraní s více taby:
Výše uvedené je možné s použitím Vue elementu <component>
a jeho speciálního atributu is
:
template
<!-- Komponenta se změní, když se změní `currentTab` -->
<component :is="tabs[currentTab]"></component>
V předchozím příkladu může hodnota předávaná do :is
obsahovat:
- string se jménem registrované komponenty, NEBO
- vlastní importovaný objekt komponenty
Atribut is
můžete také použít pro vytváření běžných HTML elementů.
Při přepínání mezi více komponentami pomocí <component :is="...">
bude komponenta odpojena (unmounted), když je z ní přepnuto jinam. Neaktivní komponenty můžete donutit, aby zůstaly „naživu“ pomocí vestavěné komponenty <KeepAlive>
.
Omezení při parsování in-DOM šablon
Pokud píšete své Vue šablony přímo v DOM, Vue bude muset definici šablony (string template) z DOM načíst. To vede k určitým omezením kvůli chování prohlížečů při nativní analýze HTML.
TIP
Je třeba poznamenat, že níže popsaná omezení platí pouze v případě, že své šablony píšete přímo v DOM. NEPLATÍ, pokud používáte string templates z následujících zdrojů:
- Single-File komponenty (SFC)
- Inlined template strings (např.
template: '...'
) <script type="text/x-template">
Necitlivost na malá a velká písmena
HTML tagy a názvy atributů nerozlišují velká a malá písmena, takže prohlížeče budou všechna velká písmena interpretovat jako malá. To znamená, že když používáte in-DOM šablony, PascalCase názvy komponent a camelCased názvy vlastností (props) nebo názvy v-on
událostí (events), musí všechny používat jejich ekvivalenty ve formátu kebab-case (oddělené pomlčkou):
js
// camelCase in JavaScript
const BlogPost = {
props: ['postTitle'],
emits: ['updatePost'],
template: `
<h3>{{ postTitle }}</h3>
`
}
template
<!-- kebab-case in HTML -->
<blog-post post-title="Ahoj!" @update-post="onUpdatePost"></blog-post>
Samouzavírací tagy
V předchozích ukázkách kódu jsme pro komponenty používali samouzavírací tagy:
template
<MyComponent />
Je to proto, že parser Vue šablon respektuje />
jako indikaci ukončení jakéhokoli tagu, bez ohledu na jeho typ.
V in-DOM šablonách však musíme vždy zahrnout explicitní uzavírací tagy:
template
<my-component></my-component>
Je to proto, že specifikace HTML umožňuje pouze několika konkrétním prvkům vynechat uzavírací tag, nejběžnější jsou <input>
a <img>
. Pokud u všech ostatních prvků uzavírací tag vynecháte, nativní HTML parser si bude myslet, že jste úvodní tag nikdy neukončili. Například následující kus kódu:
template
<my-component /> <!-- zde chceme tag ukončit... -->
<span>ahoj</span>
Bude vyhodnocen jako:
template
<my-component>
<span>ahoj</span>
</my-component> <!-- prohlížeč ho však ukončí až tady -->
Omezení umístění elementů
Některé HTML elementy, jako jsou <ul>
, <ol>
, <table>
a <select>
mají omezení ohledně toho, jaké prvky se v nich mohou objevit, a některé elementy jako <li>
, <tr>
a <option>
se mohou objevit pouze uvnitř určitých jiných elementů.
To povede k problémům při používání komponent s elementy, které mají taková omezení. Například:
template
<table>
<blog-post-row></blog-post-row>
</table>
Naše komponenta <blog-post-row>
bude vytažena (hoisted) jako neplatný obsah, což v případném vykresleném výstupu způsobí chyby. Toto můžeme obejít s použitím speciálního atributu is
:
template
<table>
<tr is="vue:blog-post-row"></tr>
</table>
TIP
Při použití na nativní HTML elementy musí být hodnota is
uvedena předponou vue:
, aby mohla být interpretována jako Vue komponenta. Je to nutné, aby nedošlo k záměně s nativními custom built-in elementy.
To je vše, co zatím potřebujete vědět o omezeních při parsování in-DOM šablon ‑ a vlastně konec Základů Vue. Gratulujeme! Je stále co se učit, ale nejprve doporučujeme, abyste si udělali přestávku a sami si s Vue hráli – vytvořit něco zábavného, nebo se podívat na některé Příklady, pokud jste tak ještě neučinili.
Jakmile si budete jisti znalostmi, které jste právě nabrali, pokračujte v průvodci, abyste se o komponentách dozvěděli více do hloubky.