Слоты

Подразумевается, что вы уже изучили и разобрались с разделом Основы компонентов. Если нет — прочитайте его сначала.

В версии 2.6.0 был представлен новый единый синтаксис (директива v-slot) для именованных слотов и слотов с ограниченной областью видимости. Он заменяет атрибуты slot и slot-scope, которые в настоящий момент объявлены устаревшими, но _не_ удалены и документированы здесь. Обоснование введения нового синтаксиса описано в этом RFC.

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

Vue реализует API распределения контента, вдохновлённое текущим черновиком спецификации веб-компонентов, используя элемент <slot> в качестве точек распространения контента.

Например, это позволит составлять такие компоненты:

<navigation-link url="/profile">
  Ваш профиль
</navigation-link>

Для этого шаблон <navigation-link> должен быть например таким:

<a
  v-bind:href="url"
  class="nav-link"
>
  <slot></slot>
</a>

При отрисовке компонента <slot></slot> будет заменён на «Ваш профиль». Слоты могут содержать любой код шаблона, в том числе HTML:

<navigation-link url="/profile">
  <!-- Добавляем иконку из набора Font Awesome -->
  <span class="fa fa-user"></span>
  Ваш профиль
</navigation-link>

Или даже другие компоненты:

<navigation-link url="/profile">
  <!-- Используем компонент для добавления иконки -->
  <font-awesome-icon name="user"></font-awesome-icon>
  Ваш профиль
</navigation-link>

Если шаблон <navigation-link> не содержит элемент <slot>, любой переданный контент будет просто проигнорирован.

Область видимости при компиляции

Если необходимо использовать данные внутри слота, например:

<navigation-link url="/profile">
  Вы вошли как {{ user.name }}
</navigation-link>

То этот слот имеет доступ к тем же свойствам экземпляра (т.е. к той же «области видимости»), что и остальная часть шаблона. Слот не имеет доступа к области видимости <navigation-link>. Поэтому попытка получить url не сработает:

<navigation-link url="/profile">
  Кликните для перехода сюда: {{ url }}
  <!--
  Значение `url` будет неопределено (undefined), потому что этот
  контент передаётся _на_ <navigation-link>, а не определяется
  _внутри_ компонента <navigation-link>.
  -->
</navigation-link>

Как правило, достаточно запомнить что:

Всё в родительском шаблоне компилируется в области видимости родительского компонента; всё в дочернем шаблоне компилируется в области видимости дочернего компонента.

Содержимое слота по умолчанию

Бывает полезным указать запасное содержимое слота (т.е. по умолчанию), которое будет отображаться только тогда, когда ничего не передавалось в слот. Например, в компоненте <submit-button>:

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

Было бы удобно если текст «Отправить» отображался внутри <button> большую часть времени. Чтобы сделать «Отправить» в качестве содержимого по умолчанию, необходимо поместить его между тегами <slot>:

<button type="submit">
  <slot>Отправить</slot>
</button>

Теперь, при использовании <submit-button> в родительском компоненте и не указывая содержимое для слота:

<submit-button></submit-button>

отобразится содержимое по умолчанию, «Отправить»:

<button type="submit">
  Отправить
</button>

Но если указать содержимое:

<submit-button>
  Сохранить
</submit-button>

Тогда оно будет использовано для отображения:

<button type="submit">
  Сохранить
</button>

Именованные слоты

Обновлено в версии 2.6.0+. Устаревший синтаксис с использованием атрибута slot можно посмотреть здесь.

Зачастую удобно иметь несколько слотов. К примеру, для компонента <base-layout> со следующим шаблоном:

<div class="container">
  <header>
    <!-- Мы хотим отобразить контент заголовка здесь -->
  </header>
  <main>
    <!-- Мы хотим отобразить основной контент здесь -->
  </main>
  <footer>
    <!-- Мы хотим отобразить контент подвала здесь -->
  </footer>
</div>

В таких случаев элементу <slot> можно указать специальный атрибут name, который используется для определения дополнительных слотов:

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

Обычный <slot> без name неявно имеет имя «default».

Чтобы указать содержимое для именованного слота, нужно использовать директиву v-slot на <template>, передав имя слота аргументом v-slot:

<base-layout>
  <template v-slot:header>
    <h1>Здесь мог быть заголовок страницы</h1>
  </template>

  <p>Параграф для основного контента.</p>
  <p>И ещё один.</p>

  <template v-slot:footer>
    <p>Некая контактная информация</p>
  </template>
</base-layout>

Теперь всё внутри элементов <template> будет передаваться в соответствующие слоты. Предполагается, что любое содержимое, не обёрнутое в <template> с использованием v-slot, предназначается для слота по умолчанию.

Однако, можно и явно обернуть в <template> содержимое слота по умолчанию:

<base-layout>
  <template v-slot:header>
    <h1>Здесь мог быть заголовок страницы</h1>
  </template>

  <template v-slot:default>
    <p>Параграф для основного контента.</p>
    <p>И ещё один.</p>
  </template>

  <template v-slot:footer>
    <p>Некая контактная информация</p>
  </template>
</base-layout>

В обоих случаях, итоговый HTML будет таким:

<div class="container">
  <header>
    <h1>Здесь мог быть заголовок страницы</h1>
  </header>
  <main>
    <p>Параграф для основного контента.</p>
    <p>И ещё один.</p>
  </main>
  <footer>
    <p>Некая контактная информация</p>
  </footer>
</div>

Обратите внимание, что v-slot можно добавлять только на <template> (за одним исключением), в отличие от устаревшего атрибута slot.

Слоты с ограниченной областью видимости

Обновлено в версии 2.6.0+. Устаревший синтаксис, с использованием атрибута slot-scope можно посмотреть здесь.

Иногда для содержимого слота полезно иметь возможность использовать данные, доступные только в дочернем компоненте. Например, представьте компонент <current-user> со следующим шаблоном:

<span>
  <slot>{{ user.lastName }}</slot>
</span>

Может потребоваться заменить это содержимое по умолчанию, например, чтобы отобразить имя пользователя, а не фамилию:

<current-user>
  {{ user.firstName }}
</current-user>

Однако это не сработает, потому что только компонент <current-user> имеет доступ к user, а новое содержимое слота отрисовывается в родительском.

Чтобы сделать user доступным для содержимого слота в родительском компоненте, необходимо добавить привязку user в качестве атрибута на элементе <slot>:

<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>

Атрибуты, привязанные к элементу <slot>, называются входными параметрами слота. Теперь, в родительской области видимости, можно использовать v-slot со значением, чтобы указать имя для предоставленных слоту входных параметров:

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>

В этом примере мы выбрали имя объекта slotProps, содержащего все входные параметры слота, но можно использовать любое другое, которое нравится.

Сокращённый синтаксис для одиночного слота по умолчанию

В случаях, когда только слоту по умолчанию предоставляется содержимое, тег компонента можно использовать в качестве шаблона слота. Это позволяет использовать v-slot непосредственно на компоненте:

<current-user v-slot:default="slotProps">
  {{ slotProps.user.firstName }}
</current-user>

Запись можно сократить ещё сильнее. Как предполагается, что неуказанное явно содержимое относится к слоту по умолчанию, так и v-slot без аргумента означает слот по умолчанию:

<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
</current-user>

Обратите внимание, что такой сокращённый синтаксис для слота по умолчанию нельзя смешивать с именованными слотами, потому что это приведёт к неоднозначности области видимости:

<!-- НЕПРАВИЛЬНО, будет выкидывать предупреждение -->
<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
  <template v-slot:other="otherSlotProps">
    slotProps НЕДОСТУПНЫ здесь
  </template>
</current-user>

При наличии нескольких слотов лучше используйте полный синтаксис на основе <template> для всех слотов:

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</current-user>

Деструктурирование входных параметров слота

Слоты с ограниченной областью видимости, под капотом работают заключая содержимое слота в функцию, и передавая входные параметры одним аргументом:

function (slotProps) {
  // ... содержимое слота ...
}

Это значит, что значение v-slot может принимать любое допустимое выражение JavaScript, которое может появиться в позиции аргумента определения функции. Поэтому в поддерживаемых окружениях (однофайловых компонентах или современных браузерах), можно также использовать деструктурирование ES2015 чтобы извлекать определённые входные параметры слотов, например вот так:

<current-user v-slot="{ user }">
  {{ user.firstName }}
</current-user>

Такой подход сделает шаблон намного чище, особенно когда слот предоставляет множество входных параметров. Это также открывает другие возможности, такие как переименование входных параметров, например user в person:

<current-user v-slot="{ user: person }">>
  {{ person.firstName }}
</current-user>

Можно даже определять значения по умолчанию, которые будут использоваться в случае, если входной параметр слота не определён:

<current-user v-slot="{ user = { firstName: 'Guest' } }">>
  {{ user.firstName }}
</current-user>

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

Добавлено в версии 2.6.0+

Динамические аргументы директивы также работают с v-slot, что позволяет указать динамическое имя слота:

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

Сокращённая запись для именованных слотов

Добавлено в версии 2.6.0+

Аналогично v-on и v-bind, у v-slot есть собственное сокращение, заменяющее всё перед аргументом (v-slot:) специальным символом #. Например, v-slot:header можно записать как #header:

<base-layout>
  <template #header>
    <h1>Здесь мог быть заголовок страницы</h1>
  </template>

  <p>Параграф для основного контента.</p>
  <p>И ещё один.</p>

  <template #footer>
    <p>Некая контактная информация</p>
  </template>
</base-layout>

Однако, как и в случае с другими директивами, сокращение доступно только при наличии аргумента. Это означает, что следующий синтаксис недопустим:

<!-- Это выкинет предупреждение -->
<current-user #="{ user }">
  {{ user.firstName }}
</current-user>

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

<current-user #default="{ user }">
  {{ user.firstName }}
</current-user>

Другие примеры

Входные параметры слотов позволяют превратить их в переиспользуемые шаблоны, которые могут отображать различное содержимое, основываясь на входных параметрах. Это очень полезно при разработке переиспользуемых компонентов, которые инкапсулируют логику данных, позволяя родительскому компоненту настраивать часть своего шаблона.

Например, реализуем компонент <todo-list>, который содержит шаблон и логику фильтрации для списка задач:

<ul>
  <li
    v-for="todo in filteredTodos"
    v-bind:key="todo.id"
  >
    {{ todo.text }}
  </li>
</ul>

Вместо жёсткого кодирования содержимого каждой задачи списка, мы можем позволить родительскому компоненту взять на себя управление отображением с помощью слота, а затем привязать todo в качестве входного параметра слота:

<ul>
  <li
    v-for="todo in filteredTodos"
    v-bind:key="todo.id"
  >
    <!--
    Указываем слот для каждой задачи, передавая
    объект `todo` в качестве входного параметра.
    -->
    <slot name="todo" v-bind:todo="todo">
      <!-- Содержимое по умолчанию -->
      {{ todo.text }}
    </slot>
  </li>
</ul>

Теперь, при использовании <todo-list>, можно опционально переопределить <template> для элементов списка, но сохранив доступ к данным из дочернего компонента:

<todo-list v-bind:todos="todos">
  <template v-slot:todo="{ todo }">
    <span v-if="todo.isComplete"></span>
    {{ todo.text }}
  </template>
</todo-list>

Однако, это едва ли вершина айсберга на что способы слоты с ограниченной областью видимости. Несколько реальных примеров использования слотов с ограниченной областью видимости можно посмотреть в библиотеках Vue Virtual Scroller, Vue Promised, и Portal Vue.

Устаревший синтаксис

В версии Vue 2.6.0 была представлена директива v-slot, предлагающая улучшенный альтернативный API для всё ещё поддерживаемых атрибутов slot и slot-scope. Полное обоснование добавления v-slot описано в RFC. Атрибуты slot и slot-scope будут поддерживаться в новых версиях 2.x, но официально они считаются устаревшими и будут удалены во Vue 3.

Именованные атрибуты с помощью атрибута slot

Устаревшее с 2.6.0+. Новый рекомендованный синтаксис можно посмотреть здесь.

Для передачи содержимого в именованные слоты из родительского компонента используйте специальный атрибут slot на теге <template> (в качестве примера здесь используется компонент <base-layout>, описанный выше):

<base-layout>
  <template slot="header">
    <h1>Здесь мог быть заголовок страницы</h1>
  </template>

  <p>Параграф для основного контента.</p>
  <p>И ещё один.</p>

  <template slot="footer">
    <p>Некая контактная информация</p>
  </template>
</base-layout>

Также атрибут slot можно использовать непосредственно на обычном элементе:

<base-layout>
  <h1 slot="header">Здесь мог быть заголовок страницы</h1>

  <p>Параграф для основного контента.</p>
  <p>И ещё один.</p>

  <p slot="footer">Некая контактная информация</p>
</base-layout>

Может быть только один слот без имени, который является слотом по умолчанию и служит для отображения оставшегося содержимого. В обоих случаях итоговый HTML будет таким:

<div class="container">
  <header>
    <h1>Здесь мог быть заголовок страницы</h1>
  </header>
  <main>
    <p>Параграф для основного контента.</p>
    <p>И ещё один.</p>
  </main>
  <footer>
    <p>Некая контактная информация</p>
  </footer>
</div>

Слоты с ограниченной областью видимости с помощью атрибута slot-scope

Устаревшее с 2.6.0+. Новый рекомендованный синтаксис можно посмотреть здесь.

Для получения входных параметров, переданных в слот, родительский компонент может использовать <template> с атрибутом slot-scope (в качестве примера используется <slot-example>, описанный выше):

<slot-example>
  <template slot="default" slot-scope="slotProps">
    {{ slotProps.msg }}
  </template>
</slot-example>

Здесь slot-scope объявляет объект с полученными входными параметрами как переменную slotProps, и делает его доступным внутри области видимости <template>. Можно назвать slotProps как угодно, придерживаясь именования аргументов для функций в JavaScript.

В примере slot="default" можно опустить, так как это подразумевается:

<slot-example>
  <template slot-scope="slotProps">
    {{ slotProps.msg }}
  </template>
</slot-example>

Атрибут slot-scope также можно использовать непосредственно не только на элементах <template> (включая компоненты):

<slot-example>
  <span slot-scope="slotProps">
    {{ slotProps.msg }}
  </span>
</slot-example>

Значение slot-scope может принимать любое допустимое выражение JavaScript, которое может использоваться в позиции аргумента определения функции. Это означает, что в поддерживаемых средах (однофайловых компонентах или современных браузерах) также можно использовать деструктурирование ES2015 в выражении, например так:

<slot-example>
  <span slot-scope="{ msg }">
    {{ msg }}
  </span>
</slot-example>

Используя <todo-list> описанный выше в качестве примера, вот эквивалентная запись с использованием slot-scope:

<todo-list v-bind:todos="todos">
  <template slot="todo" slot-scope="{ todo }">
    <span v-if="todo.isComplete"></span>
    {{ todo.text }}
  </template>
</todo-list>