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> Web Components, но с дополнительными возможностями, которые мы увидим позже.

Область видимости рендеринга

Содержимое слота имеет доступ к области видимости данных родительского компонента, поскольку оно определяется в родительском компоненте. Например:

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>
    <!-- содержимое для слота 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>И ещё один.</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>`
}

Условные слоты

Иногда нужно отображать что-то в зависимости от того, было ли передано содержимое в слот.

В примере ниже мы определяем компонент Card с тремя условными слотами: header, footer и default. Когда содержимое для header / footer / default присутствует, мы хотим обернуть его для дополнительной стилизации:

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>

Попробовать в песочнице

Динамические имена слотов

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
<!-- Этот шаблон не скомпилируется -->
<MyComponent v-slot="{ message }">
  <p>{{ message }}</p>
  <template #footer>
    <!-- message принадлежит слою по умолчанию и здесь недоступен -->
    <p>{{ message }}</p>
  </template>
</MyComponent>

Использование явного тега <template> для слота по умолчанию помогает прояснить, что входной параметр message недоступен внутри других слотов:

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

  <template #footer>
    <p>Здесь контактная информация</p>
  </template>
</MyComponent>

Пример красивого списка

Возможно, вам интересно, каким может быть хороший вариант использования для слотов с ограниченной областью видимости. Вот пример: представьте компонент <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>