Слоты
Подразумевается, что вы уже изучили и разобрались с разделом Основы компонентов. Если нет — прочитайте его сначала.
Содержимое слота и его вывод
Мы узнали, что компоненты могут принимать входные параметры, которые могут быть значениями 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>
веб-компонента, но с дополнительными возможностями, которые мы увидим позже.
Область видимости при отрисовке
Содержимое слота имеет доступ к области видимости данных родительского компонента, поскольку он определен в родительском компоненте. Например:
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
, который используется для присвоения уникального ID различным слотам, чтобы определить, где какое содержимое необходимо отобразить:
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>
, нам нужен способ передачи нескольких фрагментов содержимого слотов, каждый из которых предназначен для отдельного выхода слота. Именно здесь на помощь приходят именованные слоты.
Для указания содержимого именованного слота, нужно использовать директиву v-slot
на элементе <template>
, передавая имя слота аргументом 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> template -->
<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 = 'hello'
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="hello"></slot>
Обратите внимание, что name
слота не будет включено в входной параметр, поскольку оно зарезервировано - таким образом, результирующий headerProps
будет { message: 'hello' }
.
Если вы смешиваете именованные слоты со слотами с ограниченной областью видимости по умолчанию, вам необходимо использовать явный тег <template>
для слота по умолчанию. Попытка разместить директиву v-slot
непосредственно на компоненте приведет к ошибке компиляции. Это сделано для того, чтобы избежать двусмысленности относительно области видимости входного параметра слота по умолчанию. Например:
template
<!-- <MyComponent> template -->
<div>
<slot :message="hello"></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>Here's some contact info</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 }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
Хотя это интересный паттерн, большинство из того, что можно достичь с помощью компонентов без рендеринга, может быть достигнуто более эффективным способом с помощью Composition API, без накладных расходов на дополнительную вложенность компонентов. Позже мы увидим, как можно реализовать ту же функциональность отслеживания мыши с помощью Composables.
Тем не менее слоты с ограниченной областью видимости по-прежнему полезны в тех случаях, когда нам нужно как инкапсулировать логику, так и составить визуальный вывод, как в примере <FancyList>
.