# Слоты

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

Изучите основы слотов в бесплатном видеоуроке на Vue School

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

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

Что позволяет создавать например такие компоненты:

<todo-button>
  Добавить todo
</todo-button>
1
2
3

Шаблон <todo-button> может выглядеть примерно так:

<!-- шаблон компонента todo-button -->
<button class="btn-primary">
  <slot></slot>
</button>
1
2
3
4

При отрисовке компонента <slot></slot> будет заменён на «Добавить todo».

<!-- отрисованный HTML -->
<button class="btn-primary">
  Добавить todo
</button>
1
2
3
4

И строки — это только начало! Слоты могут содержать код любого шаблона, даже HTML:

<todo-button>
  <!-- Добавляем иконку Font Awesome -->
  <i class="fas fa-plus"></i>
  Добавить todo
</todo-button>
1
2
3
4
5

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

<todo-button>
  <!-- Используем компонент для добавления иконки -->
  <font-awesome-icon name="plus"></font-awesome-icon>
  Добавить todo
</todo-button>
1
2
3
4
5

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

<!-- В шаблоне компонента todo-button НЕТ <slot> -->
<button class="btn-primary">
  Создать новый элемент
</button>
1
2
3
4
<todo-button>
  <!-- Этот текст НЕ БУДЕТ ОТРИСОВАН -->
  Добавить todo
</todo-button>
1
2
3
4

# Область видимости при отрисовке

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

<todo-button>
  Удалить {{ item.name }}
</todo-button>
1
2
3

То в таком слоте будет доступ к тем же свойствам экземпляра (т.е. к той же «области видимости»), как и в остальной части шаблона.

Пояснительная диаграмма для слота

У слота нет доступа к области видимости <todo-button>. Поэтому попытка обратиться к входному параметру action не сработает:

<todo-button action="delete">
  Клик вызовет действие {{ action }} для элемента
  <!--
  Значение `action` будет undefined, потому что это содержимое передаётся
  ВНУТРЬ <todo-button>, а не определяется СНАРУЖИ компонента <todo-button>.
  -->
</todo-button>
1
2
3
4
5
6
7

Обычно достаточно запомнить что:

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

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

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

<button type="submit">
  <slot></slot>
</button>
1
2
3

Удобнее указать текст по умолчанию «Отправить», который будет отображаться большую часть времени. Для этого нужно сделать «Отправить» содержимым по умолчанию, поместив его между тегами <slot>:

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

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

<submit-button></submit-button>
1

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

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

Но стоит определить его:

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

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

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

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

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

<div class="container">
  <header>
    <!-- Здесь должен быть заголовок -->
  </header>
  <main>
    <!-- Здесь основной контент -->
  </main>
  <footer>
    <!-- Здесь контент подвала -->
  </footer>
</div>
1
2
3
4
5
6
7
8
9
10
11

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

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
1
2
3
4
5
6
7
8
9
10
11

Элемент <slot> без name неявно получает имя «default».

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

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Теперь содержимое элементов <template> будет передаваться в соответствующие слоты.

Отрисованный HTML получится таким:

<div class="container">
  <header>
    <h1>Здесь мог быть заголовок страницы</h1>
  </header>
  <main>
    <p>Параграф для основного контента.</p>
    <p>И ещё один.</p>
  </main>
  <footer>
    <p>Некая контактная информация</p>
  </footer>
</div>
1
2
3
4
5
6
7
8
9
10
11
12

Запомните, v-slot можно добавлять только на теги <template>одним исключением).

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

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

Например, есть компонент со списком дел:

app.component('todo-list', {
  data() {
    return {
      items: ['Покормить кота', 'Купить молока']
    }
  },
  template: `
    <ul>
      <li v-for="(item, index) in items">
        {{ item }}
      </li>
    </ul>
  `
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Заменим {{ item }} на <slot> для управления отрисовкой в родительском компоненте:

<todo-list>
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>
1
2
3
4

К сожалению так это не сработает, потому что только у компонента <todo-list> есть доступ к item, а в этом примере содержимое слота указывается в родительском.

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

<ul>
  <li v-for="(item, index) in items">
    <slot :item="item"></slot>
  </li>
</ul>
1
2
3
4
5

Можно привязывать к slot любое количество атрибутов:

<ul>
  <li v-for="(item, index) in items">
    <slot
      :item="item"
      :index="index"
      :another-attribute="anotherAttribute"
    ></slot>
  </li>
</ul>
1
2
3
4
5
6
7
8
9

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

<todo-list>
  <template v-slot:default="slotProps">
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span>
  </template>
</todo-list>
1
2
3
4
5
6
Диаграмма слотов с ограниченной областью видимости

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

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

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

<todo-list v-slot:default="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>
</todo-list>
1
2
3
4

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

<todo-list v-slot="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>
</todo-list>
1
2
3
4

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

<!-- НЕПРАВИЛЬНО, будет выбрасываться предупреждение -->
<todo-list v-slot="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>

  <template v-slot:other="otherSlotProps">
    slotProps здесь НЕДОСТУПНЫ
  </template>
</todo-list>
1
2
3
4
5
6
7
8
9

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

<todo-list>
  <template v-slot:default="slotProps">
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span>
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</todo-list>
1
2
3
4
5
6
7
8
9
10

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

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

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

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

<todo-list v-slot="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>
1
2
3
4

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

<todo-list v-slot="{ item: todo }">
  <i class="fas fa-check"></i>
  <span class="green">{{ todo }}</span>
</todo-list>
1
2
3
4

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

<todo-list v-slot="{ item = 'Нет информации' }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>
1
2
3
4

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

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

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>
1
2
3
4
5

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

Кроме v-on и v-bind, есть сокращённая запись и у v-slot, которая заменяет всё перед аргументом (v-slot:) на символ #, а v-slot:header можно сократить до #header:

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

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

  <template #footer>
    <p>Некая контактная информация</p>
  </template>
</base-layout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

<!-- НЕ ЗАРАБОТАЕТ и выкинет предупреждение -->
<todo-list #="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>
1
2
3
4
5

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

<todo-list #default="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>
1
2
3
4