# Слоты

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

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

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 -->
<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

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

Slot explanation diagram

Слот не имеет доступа к области видимости <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

Было бы удобно если текст «Отправить» отображался внутри <button> большую часть времени. Чтобы сделать «Отправить» в качестве содержимого по умолчанию, необходимо поместить его между тегами <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: ['Feed a cat', 'Buy milk']
    }
  },
  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
Scoped slot diagram

В этом примере мы выбрали имя объекта 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 = 'Placeholder' }">
  <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

Deployed on Netlify.
Последнее обновление: 2021-01-10, 20:01:14 UTC