Skip to content

Слоти

Ця сторінка передбачає, що ви вже прочитали Основи компонентів. Прочитайте це спочатку, якщо ви новачок у компонентах.

Вміст та вивід слота

Ми дізналися, що компоненти можуть приймати властивості, якими можуть бути значення JavaScript будь-якого типу. Але як щодо вмісту шаблону? У деяких випадках ми можемо захотіти передати фрагмент шаблону в дочірній компонент і дозволити дочірньому компоненту відтворити фрагмент у своєму власному шаблоні.

Наприклад, у нас може бути компонент <FancyButton>, який підтримує наступне використання:

template
<FancyButton>
  Натисніть мене! <!-- вміст слота -->
</FancyButton>

Шаблон <FancyButton> виглядає так:

template
<button class="fancy-btn">
  <slot></slot> <!-- вивід слота -->
</button>

Елемент <slot> — це вивід слота, який вказує, де має наданий батьком вміст слота відтворюватися.

Діаграма слота

І остаточний відрендерений DOM:

html
<button class="fancy-btn">Натисніть мене!</button>

Завдяки слотам, <FancyButton> відповідає за візуалізацію зовнішнього <button> (та його гарного стилю), тоді як внутрішній вміст надається батьківським компонентом.

Ще один спосіб зрозуміти слоти – порівняти їх із функціями JavaScript:

js
// батьківський компонент передає вміст слота
FancyButton('Натисніть мене!')

// FancyButton рендерить вміст слота у власному шаблоні
function FancyButton(slotContent) {
  return `<button class="fancy-btn">
      ${slotContent}
    </button>`
}

Вміст слота не обмежується лише текстом. Це може бути будь-який дійсний вміст шаблону. Наприклад, ми можемо передати кілька елементів або навіть інші компоненти:

template
<FancyButton>
  <span style="color:red">Натисніть мене!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

Завдяки слотам, наш <FancyButton> є більш гнучким і придатним для повторного використання. Тепер ми можемо використовувати його в різних місцях з різним внутрішнім вмістом, але всі з однаковим стильним оформленням.

Механізм слотів у Vue був натхненний нативним елементом <slot> Веб Компонента, але з додатковими можливостями, які ми побачимо пізніше.

Область візуалізації

Вміст слота має доступ до області даних батьківського компонента, оскільки він визначений у батьківському компоненті. Наприклад:

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

Тут обидві інтерполяції {{ message }} відображатимуть однаковий вміст.

Вміст слота не має доступу до даних дочірнього компонента. Вирази в шаблонах Vue можуть отримати доступ лише до області, у якій вони визначені, відповідно до лексичного діапазону JavaScript. Іншими словами:

Вирази в батьківському шаблоні мають доступ лише до батьківської області; вирази в дочірньому шаблоні мають доступ лише до дочірньої області.

Резервний вміст

Бувають випадки, коли для слота корисно вказати резервний вміст (тобто, вміст за замовчуванням), який буде відображатися лише тоді, коли вміст не надано. Наприклад, у компоненті <SubmitButton>:

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

Ми можемо захотіти, щоб текст «Надіслати» відображався всередині <button>, якщо батьківський елемент не надав вмісту слота. Щоб «Надіслати» резервний вміст, ми можемо розмістити його між тегами <slot>:

template
<button type="submit">
  <slot>
    Надіслати <!-- резервний вміст -->
  </slot>
</button>

Тепер, коли ми використовуємо <SubmitButton> у батьківському компоненті, не надаючи вмісту для слота:

template
<SubmitButton />

Це відобразить резервний вміст, «Надіслати»:

html
<button type="submit">Надіслати</button>

Але якщо ми надаємо контент:

template
<SubmitButton>Зберегти</SubmitButton>

Тоді замість цього буде відображено наданий вміст:

html
<button type="submit">Зберегти</button>

Іменовані слоти

Бувають випадки, коли корисно мати кілька виводів в одному компоненті. Наприклад, у компоненті <BaseLayout> із таким шаблоном:

template
<div class="container">
  <header>
    <!-- Тут нам потрібен вміст заголовка -->
  </header>
  <main>
    <!-- Тут нам потрібен основний вміст -->
  </main>
  <footer>
    <!-- Тут ми хочемо вміст нижнього колонтитула -->
  </footer>
</div>

Для цих випадків елемент <slot> має спеціальний атрибут name, за допомогою якого можна призначити унікальний ідентифікатор різним слотам, щоб ви могли визначити, де має відтворюватися вміст:

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

Вивід <slot> без name неявно має назву "default".

У батьківському компоненті, що використовує <BaseLayout>, нам потрібен спосіб передати кілька фрагментів вмісту слота, кожен з яких націлений на інший вивід слота. Ось де з'являються іменовані слоти.

Щоб передати іменований слот, нам потрібно використати елемент <template> з директивою v-slot, а потім передати назву слота як аргумент v-slot:

template
<BaseLayout>
  <template v-slot:header>
    <!-- вміст для слота заголовка -->
  </template>
</BaseLayout>

v-slot має спеціальне скорочення #, тому <template v-slot:header> можна скоротити до просто <template #header>. Подумайте про це як про «відобразити цей фрагмент шаблону в слоті 'header' дочірнього компонента».

діаграма іменованих слотів

Ось код, який передає вміст для всіх трьох слотів у <BaseLayout> за допомогою скороченого синтаксису:

template
<BaseLayout>
  <template #header>
    <h1>Тут може бути назва сторінки</h1>
  </template>

  <template #default>
    <p>Абзац для основного змісту.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Ось трохи контактної інформації</p>
  </template>
</BaseLayout>

Коли компонент приймає як слот за замовчуванням, так і іменовані слоти, усі вузли верхнього рівня, які не є <template>, неявно розглядаються як вміст для слота за замовчуванням. Отже, вищесказане також можна записати так:

template
<BaseLayout>
  <template #header>
    <h1>Тут може бути назва сторінки</h1>
  </template>

  <!-- неявний слот за замовчуванням -->
  <p>Абзац для основного змісту.</p>
  <p>І ще один.</p>

  <template #footer>
    <p>Ось трохи контактної інформації</p>
  </template>
</BaseLayout>

Тепер усе всередині елементів <template> буде передано до відповідних слотів. Остаточний відтворений HTML буде таким:

html
<div class="container">
  <header>
    <h1>Тут може бути назва сторінки</h1>
  </header>
  <main>
    <p>Абзац для основного змісту.</p>
    <p>І ще один.</p>
  </main>
  <footer>
    <p>Ось трохи контактної інформації</p>
  </footer>
</div>

Знову ж таки, це може допомогти вам краще зрозуміти іменовані слоти за допомогою аналогії функції JavaScript:

js
// передача кількох фрагментів слота з різними іменами
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> відображає їх у різних місцях
function BaseLayout(slots) {
  return `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
}

Динамічні назви слотів

Аргументи динамічних директив також спрацюють з v-slot, що дозволяє визначати динамічні назви слотів:

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

  <!-- зі скороченням -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

Зверніть увагу, що на вираз поширюються синтаксичні обмеження аргументів динамічних директив.

Обмежені слоти

Як зазначено в розділі область візуалізації, вміст слота не має доступу до стану в дочірньому компоненті.

Проте є випадки, коли може бути корисно, якщо вміст слота може використовувати дані як з батьківської, так і з дочірньої областей. Щоб досягти цього, нам потрібен спосіб, за допомогою якого дочірній компонент передає дані в слот під час їх рендерингу.

Фактично, ми можемо зробити саме це - ми можемо передати атрибути до виводу так само як передати атрибути до компонента:

template
<!-- шаблон <MyComponent> -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

Отримання реквізитів слотів дещо відрізняється при використанні одного слота за замовчуванням порівняно з використанням іменованих слотів. Ми збираємося показати, як отримати реквізити, використовуючи спочатку один слот за замовчуванням завдяки v-slot безпосередньо в тегу дочірнього компонента:

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

діаграма обмежених слотів

Реквізити, передані дочірнім слотом, доступні як значення відповідної директиви v-slot, доступ до якої можна отримати за допомогою виразів у слоті.

Ви можете розглядати обмежений слот як функцію, яка передається дочірньому компоненту. Потім дочірній компонент викликає його, передаючи властивості як аргументи:

js
MyComponent({
  // передаємо слот за замовчуванням, але як функцію
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`
  }
})

function MyComponent(slots) {
  const greetingMessage = 'привіт'
  return `<div>${
    // виклик функції слота за допомогою пропсів!
    slots.default({ text: greetingMessage, count: 1 })
  }</div>`
}

Фактично, це дуже близько до того, як компілюються обмежені слоти та як ви б використовували обмежені слоти у користувацьких функціях візуалізації.

Зверніть увагу, як v-slot="slotProps" відповідає сигнатурі функції слота. Як і з аргументами функції, ми можемо використовувати деструктуризацію у v-slot:

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

Іменовані обмежені слоти

Іменовані обмежені слоти працюють аналогічно – властивості слота доступні як значення директиви v-slot: v-slot:name="slotProps". При використанні скорочення це виглядає так:

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

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

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

Передача реквізитів у іменований слот:

template
<slot name="header" message="привіт"></slot>

Зауважте, що name слота не буде включено до реквізитів, оскільки його зарезервовано, тому остаточний headerProps буде { message: 'привіт' }.

Якщо ви змішуєте іменовані слоти з обмеженим слотом за промовчанням, вам потрібно використовувати явний тег <template> для слота за промовчанням. Спроба розмістити директиву v-slot безпосередньо в компоненті призведе до помилки компіляції. Це зроблено для того, щоб уникнути будь-якої двозначності щодо обсягу реквізитів слота за промовчанням. Наприклад:

template
<!-- Цей шаблон не скомпілюється -->
<template>
  <MyComponent v-slot="{ message }">
    <p>{{ message }}</p>
    <template #footer>
      <!-- повідомлення належить до слота за промовчанням і тут недоступне -->
      <p>{{ message }}</p>
    </template>
  </MyComponent>
</template>

Використання явного тегу <template> для слота за промовчанням допомагає зрозуміти, що властивість message недоступна в іншому слоті:

template
<template>
  <MyComponent>
    <!-- Використовуйте явний слот за промовчанням -->
    <template #default="{ message }">
      <p>{{ message }}</p>
    </template>

    <template #footer>
      <p>Ось трохи контактної інформації</p>
    </template>
  </MyComponent>
</template>

Приклад гарного списку

Можливо, вам цікаво, що було б гарним варіантом використання для обмежених слотів. Ось приклад: уявіть компонент <FancyList>, який рендерить список елементів - він може інкапсулювати логіку для завантаження віддалених даних, використання даних для відображення списку або навіть розширені функції, такі як розбиття на сторінки або нескінченне прокручування. Однак ми хочемо, щоб він був гнучким щодо того, як виглядає кожен елемент, і залишив стиль кожного елемента батьківському компоненту, який його використовує. Отже, бажане використання може виглядати так:

template
<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>автор: {{ username }} | {{ likes }} вподобайок</p>
    </div>
  </template>
</FancyList>

Усередині <FancyList> ми можемо кілька разів візуалізувати той самий <slot> з різними даними елемента (зверніть увагу, що ми використовуємо v-bind, щоб передати об’єкт як реквізити слота):

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

Компоненти без рендерингу

Варіант використання <FancyList>, який ми обговорювали вище, інкапсулює як багаторазову логіку (вибірка даних, розбиття на сторінки тощо), так і візуальний вихід, делегуючи частину візуального виводу споживчому компоненту через обмежені слоти.

Якщо ми просунемо цю концепцію трохи далі, ми зможемо створити компоненти, які лише інкапсулюють логіку, а самі по собі нічого не рендерять — візуальний вихід повністю делегується споживчому компоненту з обмеженими слотами. Ми називаємо цей тип компонента Компонент без рендерингу.

Прикладом компонента без рендерингу може бути компонент, який інкапсулює логіку відстеження поточної позиції миші:

template
<MouseTracker v-slot="{ x, y }">
  Координати миші: {{ x }}, {{ y }}
</MouseTracker>

Не зважаючи на цікаву модель, більшість із того, що можна досягти за допомогою компонентів без рендерингу, можна досягти більш ефективним способом за допомогою композиційного API, не зазнаючи накладних витрат на вкладення додаткових компонентів. Пізніше ми побачимо, як можна реалізувати ту саму функцію відстеження миші, що й композиційна.

Проте, обмежені слоти все ще корисні у випадках, коли нам потрібно інкапсулювати логіку, а також створити візуальне виведення, як у прикладі з <FancyList>.

Слоти has loaded