Skip to content

Sloty (Slots)

Tato stránka předpokládá, že už jste četli Základy komponent. Pokud jsou pro vás komponenty nové, přečtěte si je jako první.

Obsah a výstup slotů

Naučili jsme se, že komponenty mohou přijímat vlastnosti (props), což mohou být JavaScript hodnoty libovolného typu. Ale jak je to s obsahem šablony? V některých případech můžeme chtít předat fragment šablony komponentě potomka a nechat ji, aby fragment v rámci své vlastní šablony vykreslila.

Například můžeme mít komponentu <FancyButton>, která podporuje následující použití:

template
<FancyButton>
  Klikni na mě! <!-- obsah slotu -->
</FancyButton>

Šablona komponenty <FancyButton> vypadá takto:

template
<button class="fancy-btn">
  <slot></slot> <!-- obsah slotu -->
</button>

Element <slot> je výstup (outlet) slotu, který indikuje, kde se má obsah (content) slotu poskytnutý komponentou rodiče vykreslit.

slot diagram

Výsledný vykreslený DOM bude:

html
<button class="fancy-btn">Klikni na mě!</button>

S použitím slotů je <FancyButton> zodpovědný za vykreslení vnějšího <button> (a jeho „fancy“ stylování), zatímco vnitřní obsah je dodaný komponentou rodiče.

Další způsob, jak pochopit sloty, je porovnat je s JavaScript funkcemi:

js
// komponenta rodiče předává obsah slotu
FancyButton('Klikni na mě!')

// FancyButton vykresluje obsah slotu uvnitř své vlastní šablony
function FancyButton(slotContent) {
  return `<button class="fancy-btn">
      ${slotContent}
    </button>`
}

Obsah slotů není omezen pouze na text. Může to být jakýkoli platný obsah šablony. Můžeme například předat více elementů nebo i jiné komponenty:

template
<FancyButton>
  <span style="color:red">Klikni na mě!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

S použitím slotů je naše komponenta <FancyButton> více flexibilní a znovupoužitelná. Nyní ji můžeme použít na různých místech s různým vnitřním obsahem, ale se stejným efektním stylováním.

Mechanismus slotů ve Vue komponentách je inspirován nativním Web Component elementem <slot>, ovšem s dalšími možnostmi, které uvidíme později.

Rozsah vykreslování

Obsah slotu má přístup k datovému scope komponenty rodiče, protože je definován v ní. Například:

template
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

Zde obě interpolace {{ message }} vykreslí ten samý obsah.

Obsah slotu nemá přístup k datům komponenty potomka. Výrazy ve Vue šablonách mohou přistupovat pouze ke scope, ve kterém jsou definovány, v souladu s lexikálním rozsahem jazyka JavaScript. Jinými slovy:

Výrazy v šabloně rodiče mají přístup pouze ke scope rodiče; výrazy v šabloně potomka mají přístup pouze ke scope potomka.

Fallback obsah

V některých případech je užitečné specifikovat pro slot náhradní (fallback) obsah, tj. výchozí obsah, který se vykreslí pouze v případě, že žádný obsah není shora zadán. Například v komponentě <SubmitButton>:

template
<button type="submit">
  <slot></slot>
</button>

Můžeme chtít, aby se uvnitř <button> zobrazil text „Odeslat“, pokud rodič žádný obsah slotu neposkytl. Aby byl text „Odeslat“ fallback obsahem, můžeme jej umístit mezi tagy <slot>:

template
<button type="submit">
  <slot>
    Odeslat <!-- fallback obsah -->
  </slot>
</button>

Nyní když <SubmitButton> použijeme v komponentě rodiče bez poskytnutí obsahu slotu:

template
<SubmitButton />

Bude vykreslen fallback obsah, „Odeslat“:

html
<button type="submit">Odeslat</button>

Pokud však obsah zadáme:

template
<SubmitButton>Uložit</SubmitButton>

Potom bude místo toho vykreslen poskytnutý obsah:

html
<button type="submit">Uložit</button>

Pojmenované sloty

Jsou chvíle, kdy je užitečné mít v jedné komponentě více slotů. Například v komponentě <BaseLayout> s následující šablonou:

template
<div class="container">
  <header>
    <!-- Zde chceme mít hlavičku -->
  </header>
  <main>
    <!-- Zde chceme mít hlavní obsah -->
  </main>
  <footer>
    <!-- Zde chceme mít patičku -->
  </footer>
</div>

Pro tyto případy má element <slot> speciální atribut name, který lze použít k přiřazení jedinečného ID různým slotům, aby bylo možné určit, kde se má obsah vykreslit:

template
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

Výstup pro <slot> bez name má implicitně název „default“.

V komponentě rodiče, která <BaseLayout> používá potřebujeme způsob, jak předat více fragmentů obsahu slotů, z nichž každý je zaměřen na jiný výstup slotu. K tomu slouží pojmenované sloty.

Pro předání pojmenovaného slotu musíme použít prvek <template> s direktivou v-slot a té předat název slotu jako parametr:

template
<BaseLayout>
  <template v-slot:header>
    <!-- obsah pro slot `header` -->
  </template>
</BaseLayout>

v-slot má učený zkrácený zápis #, takže <template v-slot:header> může být zkráceno jen na <template #header>. Představte si to jako pokyn „vykreslit tento fragment šablony do slotu 'header' komponenty potomka“.

named slots diagram

Zde je kód, který předává obsah do všech tří slotů <BaseLayout> s využitím zkráceného zápisu:

template
<BaseLayout>
  <template #header>
    <h1>Zde může být titulek stránky</h1>
  </template>

  <template #default>
    <p>Odstavec pro hlavní obsah</p>
    <p>A další...</p>
  </template>

  <template #footer>
    <p>Zde jsou kontaktní informace</p>
  </template>
</BaseLayout>

Pokud komponenta akceptuje výchozí slot i pojmenované sloty, jsou všechny elementy nejvyšší úrovně, které nejsou <template>, implicitně považovány za obsah výchozího slotu. Výše uvedené lze tedy zapsat také jako:

template
<BaseLayout>
  <template #header>
    <h1>Zde může být titulek stránky</h1>
  </template>

  <!-- implicitní obsah slotu `default` -->
  <p>Odstavec pro hlavní obsah</p>
  <p>A další...</p>

  <template #footer>
    <p>Zde jsou kontaktní informace</p>
  </template>
</BaseLayout>

Nyní bude všechno uvnitř <template> elementů bude předáno do odpovídajících slotů. Cílové vykreslené HTML bude:

html
<div class="container">
  <header>
    <h1>Zde může být titulek stránky</h1>
  </header>
  <main>
    <p>Odstavec pro hlavní obsah</p>
    <p>A další...</p>
  </main>
  <footer>
    <p>Zde jsou kontaktní informace</p>
  </footer>
</div>

Možná vám opět pomůže lépe pochopit pojmenované sloty analogie s JavaScript funkcemi:

js
// předávání více fragmentů šablony pod různými názvy
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> je vykresí na různá místa
function BaseLayout(slots) {
  return `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
}

Podmíněné sloty

Někdy chcete část obsahu vykreslit v závislosti na tom, zda slot je nebo není předán.

Abyste toho dosáhli, můžete použít vlastnost $slots v kombinaci s v-if.

V příkladu níže definujeme komponentu Card s třemi podmíněnými sloty: header, footer a výchozím default. Pokud je header / footer / default slot předán (přítomen v objektu), chceme jej obalit dodatečným stylováním:

template
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
    
    <div v-if="$slots.default" class="card-content">
      <slot />
    </div>
    
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

Vyzkoušejte si to

Dynamické pojmenované sloty

Dynamické parametry fungují i na v-slot, což umožňuje definici dynamických názvů slotů:

template
<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- zkrácený zápis -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

Mějte na paměti, že výraz podléhá pravidlům syntaxe dynamických parametrů pro direktivy.

Scoped sloty

Jak už bylo rozebíráno v oddílu rozsah vykreslování, obsah slotu nemá přístup ke stavovým proměnným komponenty potomka.

V některých případech by však mohlo být užitečné, kdyby obsah slotu mohl využívat data ze scope rodiče i potomka. Abychom toho dosáhli, potřebujeme způsob, jak může komponenta potomka předat svá data do slotu při jeho vykreslování.

Vlastně můžeme udělat přesně to - můžeme předávat atributy do výstupu slotu stejně jako se předávají vlastnosti komponentě:

template
<!-- <MyComponent> template -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

Přijímání vlastností slotů se trochu liší při použití jednoho výchozího slotu a při použití pojmenovaných slotů. Nejprve si ukážeme, jak přijímat vlastností pomocí jediného výchozího slotu, a to pomocí v-slot přímo na tagu komponenty potomka:

template
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

scoped slots diagram

Vlastnosti předané komponentou potomka do slotu jsou k dispozici jako hodnota příslušné direktivy v-slot, ke které lze přistupovat pomocí výrazů uvnitř slotu.

Scoped slot si můžete představit jako funkci předávanou do komponenty potomka. Ta ji pak zavolá a jako parametry předá vlastnosti:

js
MyComponent({
  // předávání do slotu `default`, ale v podobě funkce
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`
  }
})

function MyComponent(slots) {
  const greetingMessage = 'hello'
  return `<div>${
    // volání funkce pro slot s parametry!
    slots.default({ text: greetingMessage, count: 1 })
  }</div>`
}

V podstatě je to velmi podobné tomu, jak jsou scoped sloty kompilovány a jak se scoped sloty používají v manuálních funkcích vykreslování.

Všimněte si, jak v-slot="slotProps" odpovídá signatuře slot funkce. Stejně jako parametry funkce je v rámci v-slot můžeme destrukturovat:

template
<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

Pojmenované scoped sloty

Pojmenované scoped sloty fugují podobně - vlastnosti slotu jsou přístupné jako hodnota direktivy v-slot: v-slot:name="slotProps". Při použití zkráceného zápisu to vypadá takto:

template
<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

Předávání vlastností do pojemnovaného slotu:

template
<slot name="header" message="ahoj"></slot>

Pamatujte si, že atribut name pojmenovaného slotu nebude jako vlastnost zahrnut, protože jde o vyhrazené klíčové slovo - takže výsledný objekt headerProps bude { message: 'ahoj' }.

Pokud kombinujete pojmenované sloty s výchozím scoped slotem, musíte pro výchozí slot použít explicitní tag <template>. Pokus umístit direktivu v-slot přímo na komponentu způsobí kompilační chybu. Tím se Vue brání nejasnostem ohledně scope pro vlastnosti výchozího slotu. Například:

template
<!-- šablona <MyComponent> -->
<div>
  <slot :message="hello"></slot>
  <slot name="footer" />
</div>
template
<!-- tato šablona se nezkompiluje -->
<MyComponent v-slot="{ message }">
  <p>{{ message }}</p>
  <template #footer>
    <!-- `message` patří do výchozího slotu, a zde není přístupná -->
    <p>{{ message }}</p>
  </template>
</MyComponent>

Použití explicitního tagu <template> pro výchozí slot pomáhá ujasnit si, že vlastnost message není přístupná v jiném slotu:

template
<MyComponent>
  <!-- použitího explicitního výchozího slotu -->
  <template #default="{ message }">
    <p>{{ message }}</p>
  </template>

  <template #footer>
    <p>Zde jsou kontaktní informace</p>
  </template>
</MyComponent>

Příklad - Fancy List

Možná se ptáte, jaké by byl pro scoped sloty vhodné využití. Zde je příklad: představte si komponentu <FancyList>, která vykresluje seznam položek – může zapouzdřit logiku pro načítání vzdálených dat, používat data k zobrazení seznamu nebo dokonce pokročilé funkce, jako je stránkování nebo nekonečný scrolling. Chceme však, aby byla flexibilní ohledně toho, jak vypadá každá položka, a ponechat styl každé položky na komponentě rodiče, která ji implementuje. Požadované použití tedy může vypadat takto:

template
<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>by {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>

Uvnitř <FancyList> můžeme vykreslit stejný <slot> vícekrát s různými daty (všimněte si, že používáme v-bind pro předání objektu s vlastnostmi slotu):

template
<ul>
  <li v-for="item in items">
    <slot name="item" v-bind="item"></slot>
  </li>
</ul>

Komponenty bez vykreslení

Příklad s <FancyList>, o kterém jsme hovořili výše, zapouzdřuje jak opakovaně použitelnou logiku (načítání dat, stránkování atd.), tak vizuální výstup, přičemž část vizuálního výstupu deleguje na komponentu rodiče prostřednictvím scoped slotů.

Pokud tento koncept posuneme ještě o něco dále, můžeme přijít s komponentami, které pouze zapouzdřují logiku a samy o sobě nic nevykreslují – vizuální výstup je plně delegován na komponentu rodiče s použitím scoped slotů. Tento typ komponenty nazýváme komponenty bez vykreslení (renderless).

Příklad renderless komponenty může být taková, která zapouzdřuje logiku pro sledování aktuální polohy myši:

template
<MouseTracker v-slot="{ x, y }">
  Myš je na: {{ x }}, {{ y }}
</MouseTracker>

Ačkoli je to zajímavý vzor, většinu toho, čeho lze dosáhnout s renderless komponentami, lze zajistit efektivněji s Composition API, aniž by to znamenalo dodatečnou režii dalšího vnořování komponent. Později uvidíme, jak můžeme implementovat stejnou funkci sledování myši ve formě composable.

Scoped sloty jsou ovšem stále užitečné v případech, kdy potřebujeme zapouzdřit logiku a zároveň tvořit vizuální výstup, jako v příkladu <FancyList>.

Sloty (Slots) has loaded