Перейти к содержанию

Слоты

Эта страница предполагает, что вы уже прочитали Основы компонентов.

Содержимое и выход слота

Мы узнали, что компоненты могут принимать параметры, которые могут быть JavaScript-значениями любого типа. Но как насчёт содержания шаблонов? В некоторых случаях мы можем захотеть передать фрагмент шаблона дочернему компоненту и позволить ему отрисовать этот фрагмент в своем собственном шаблоне.

Например, у нас может быть компонент <FancyButton>, который поддерживает следующее использование:

template
<FancyButton>
  Нажмите на меня! <!-- содержимое слота -->
</FancyButton>

Шаблон <FancyButton> выглядит следующим образом:

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

Элемент <slot> — это выход слота, который указывает, куда должно быть выведено содержимое слота, предоставленное родителем.

схема слота

И финальный рендер DOM:

html
<button class="fancy-btn">Нажмите на меня!</button>

С помощью слотов <FancyButton> отвечает за отрисовку внешней <кнопки> (и её причудливой стилизации), в то время как внутреннее содержимое предоставляется родительским компонентом.

Другой способ понять слоты — сравнить их с функциями 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>

We might want the text "Submit" to be rendered inside the <button> if the parent didn't provide any slot content. To make "Submit" the fallback content, we can place it in between the <slot> tags: Возможно, нам нужен текст «Отправить» для отображения внутри <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>И ещё один.</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>`
}

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

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

Для этого можно использовать свойство $slots в сочетании с v-if.

В приведённом ниже примере мы определяем компонент 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>

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

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

Динамические аргументы также работают с 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>

Обратите внимание, что имя слота не будет включено в параметр, потому что оно зарезервировано — таким образом, результирующий headerProps будет выглядеть как { message: 'привет' }.

Если вы смешиваете именованные слоты со слотом с ограниченной областью видимости по умолчанию, вам нужно использовать явный тег <template> для слота по умолчанию. Попытка разместить директиву v-slot непосредственно на компоненте приведёт к ошибке компиляции. Это необходимо для того, чтобы избежать двусмысленности в отношении области видимости параметра слота по умолчанию. Например:

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

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

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

Слоты