10. Слоты (Slots)
🎰 Слоты в Vue 3
Заголовок раздела «🎰 Слоты в Vue 3»Слоты — это как дырки в компоненте, куда родитель может вставить свой HTML. Представь, что ты делаешь компонент-карточку, но хочешь, чтобы содержимое карточки мог задавать тот, кто её использует. Вот тут и приходят слоты! 🚀
Зачем нужны слоты?
Заголовок раздела «Зачем нужны слоты?»Без слотов компоненты — это чёрные ящики. Хочешь кнопку внутри карточки — приходится передавать label пропом. Хочешь иконку — ещё один проп. А слоты позволяют передавать целые куски шаблона, сохраняя гибкость.
<!-- Без слотов — негибко 😢 --><Card title="Привет" content="Текст" icon="🎉" />
<!-- Со слотами — свобода! 🎉 --><Card> <template #header> <h2>Привет <span class="emoji">🎉</span></h2> </template> <p>Любой контент здесь!</p></Card>Default Slot — слот по умолчанию
Заголовок раздела «Default Slot — слот по умолчанию»Самый простой вариант. В компоненте ставишь <slot />, а родитель вставляет туда что хочет:
<template> <div class="card"> <slot /> <!-- Всё, что передаст родитель, появится здесь --> </div></template>
<style scoped>.card { border: 1px solid #42b883; border-radius: 8px; padding: 1rem;}</style><!-- Использование --><template> <BaseCard> <h2>Заголовок</h2> <p>Описание карточки</p> <button>Кликни!</button> </BaseCard></template>Весь контент между тегами <BaseCard>...</BaseCard> попадёт в <slot />. Просто и элегантно! ✨
Fallback Content — контент по умолчанию
Заголовок раздела «Fallback Content — контент по умолчанию»Что если родитель не передал никакого содержимого? Слот может иметь запасной контент — он отобразится, когда слот пуст:
<template> <button class="btn"> <slot> <!-- Это отобразится, если контент не передан --> Нажми меня </slot> </button></template><!-- С контентом — отобразит "Отправить" --><Button>Отправить</Button>
<!-- Без контента — отобразит "Нажми меня" --><Button />Удобно для кнопок, плейсхолдеров, состояний загрузки 👍
Named Slots — именованные слоты
Заголовок раздела «Named Slots — именованные слоты»Когда нужно несколько точек вставки — используй именованные слоты. Классический пример — лейаут страницы:
<template> <div class="layout"> <header class="header"> <slot name="header"> <!-- Запасной контент для шапки --> <h1>Мой сайт</h1> </slot> </header>
<main class="main"> <slot /> <!-- Дефолтный слот — для основного контента --> </main>
<aside class="sidebar"> <slot name="sidebar" /> </aside>
<footer class="footer"> <slot name="footer"> <p>© 2024 Яша учит код</p> </slot> </footer> </div></template><!-- Использование AppLayout.vue --><template> <AppLayout> <!-- Именованный слот через #имя --> <template #header> <nav> <RouterLink to="/">Главная</RouterLink> <RouterLink to="/about">О нас</RouterLink> </nav> </template>
<!-- Дефолтный слот — без template --> <article> <h2>Статья</h2> <p>Контент страницы...</p> </article>
<!-- Можно и через v-slot: --> <template v-slot:sidebar> <ul> <li>Ссылка 1</li> <li>Ссылка 2</li> </ul> </template>
<!-- footer не передаём — отобразится fallback --> </AppLayout></template>💡
#header— это сокращение отv-slot:header. Так же как@clickэто сокращение отv-on:click.
v-slot директива
Заголовок раздела «v-slot директива»v-slot — официальная директива для работы со слотами. Полный синтаксис:
<template v-slot:header>...</template>
<!-- Сокращение --><template #header>...</template>
<!-- Дефолтный слот — эти записи эквивалентны --><template v-slot:default>...</template><template #default>...</template><!-- TypeScript с проверкой типов --><script setup lang="ts">// Компонент явно объявляет, какие слоты он принимаетdefineSlots<{ default(props: {}): any header(props: { title: string }): any footer(props: {}): any}>()</script>Scoped Slots — слоты с данными
Заголовок раздела «Scoped Slots — слоты с данными»Самая мощная фича! Компонент может передавать данные обратно в слот. Родитель получает данные дочернего компонента прямо в шаблоне.
Представь список: компонент управляет данными, но стиль каждого элемента задаёт родитель:
<script setup lang="ts">interface Item { id: number name: string status: 'active' | 'inactive'}
const props = defineProps<{ items: Item[]}>()</script>
<template> <ul class="list"> <li v-for="item in items" :key="item.id"> <!-- Передаём item обратно в слот! --> <slot :item="item" :index="item.id" /> </li> </ul></template><!-- Родитель получает item через v-slot --><template> <DataList :items="users"> <template #default="{ item, index }"> <!-- item — это данные из DataList! --> <span :class="['badge', item.status]"> {{ index }}. {{ item.name }} — {{ item.status }} </span> </template> </DataList></template>Если слот только один (дефолтный), можно сократить:
<DataList :items="users" v-slot="{ item }"> <span>{{ item.name }}</span></DataList>Renderless Components — компоненты без разметки
Заголовок раздела «Renderless Components — компоненты без разметки»Паттерн renderless (без рендера) — компонент, который только предоставляет логику, а разметку полностью контролирует родитель. Идеальный пример — компонент для отслеживания мыши:
<!-- MouseTracker.vue — renderless component --><script setup lang="ts">import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)const y = ref(0)
function handleMouseMove(e: MouseEvent) { x.value = e.clientX y.value = e.clientY}
onMounted(() => { window.addEventListener('mousemove', handleMouseMove)})
onUnmounted(() => { window.removeEventListener('mousemove', handleMouseMove)})</script>
<template> <!-- Передаём координаты в слот --> <slot :x="x" :y="y" /></template><!-- Полный контроль над разметкой! --><template> <MouseTracker v-slot="{ x, y }"> <div class="tracker"> Мышь: {{ x }}, {{ y }} </div> </MouseTracker>
<!-- Используем те же данные, но другую разметку --> <MouseTracker v-slot="{ x, y }"> <canvas :data-x="x" :data-y="y" /> </MouseTracker></template>🎯 Renderless components — предшественник composables. В Vue 3 чаще используют composables (
useMouse()), но renderless всё ещё отлично подходит для случаев, где нужна гибкость шаблона.
Проверка наличия слота — $slots
Заголовок раздела «Проверка наличия слота — $slots»Иногда нужно знать, передал ли родитель контент в слот. Используем $slots:
<script setup lang="ts">import { useSlots } from 'vue'
const slots = useSlots()</script>
<template> <div class="card"> <!-- Показываем шапку только если есть контент --> <header v-if="slots.header" class="card-header"> <slot name="header" /> </header>
<div class="card-body"> <slot /> </div>
<!-- Показываем footer только если он передан --> <footer v-if="slots.footer" class="card-footer"> <slot name="footer" /> </footer> </div></template><!-- В Options API через $slots --><template> <div> <div v-if="$slots.icon" class="icon-wrapper"> <slot name="icon" /> </div> <slot /> </div></template>Динамические имена слотов
Заголовок раздела «Динамические имена слотов»Имя слота может быть динамическим — как и в случае с динамическими директивами:
<script setup lang="ts">import { ref } from 'vue'
const currentSlot = ref<'header' | 'footer'>('header')</script>
<template> <Layout> <!-- Динамическое имя слота --> <template #[currentSlot]> <p>Контент для {{ currentSlot }}</p> </template> </Layout>
<button @click="currentSlot = currentSlot === 'header' ? 'footer' : 'header'"> Переключить слот </button></template>Типизация слотов с defineSlots
Заголовок раздела «Типизация слотов с defineSlots»В Vue 3.3+ появился defineSlots для строгой типизации:
<script setup lang="ts">interface TableColumn<T> { key: keyof T label: string}
const props = defineProps<{ columns: TableColumn<any>[] rows: any[]}>()
// Явная типизация слотовconst slots = defineSlots<{ // Слот для ячейки с данными cell(props: { row: any; column: TableColumn<any>; value: any }): any // Слот для шапки header(props: { column: TableColumn<any> }): any // Слот "нет данных" empty(props: {}): any}>()</script>
<template> <table> <thead> <tr> <th v-for="col in columns" :key="String(col.key)"> <slot name="header" :column="col"> {{ col.label }} </slot> </th> </tr> </thead> <tbody> <tr v-if="rows.length === 0"> <td :colspan="columns.length"> <slot name="empty"> <p>Нет данных</p> </slot> </td> </tr> <tr v-for="(row, i) in rows" :key="i"> <td v-for="col in columns" :key="String(col.key)"> <slot name="cell" :row="row" :column="col" :value="row[col.key]"> {{ row[col.key] }} </slot> </td> </tr> </tbody> </table></template>Практические паттерны
Заголовок раздела «Практические паттерны»Compound Components через слоты
Заголовок раздела «Compound Components через слоты»<!-- Tabs.vue — родительский компонент --><script setup lang="ts">import { ref, provide } from 'vue'
const activeTab = ref(0)provide('activeTab', activeTab)provide('setActiveTab', (i: number) => activeTab.value = i)</script>
<template> <div class="tabs"> <slot name="tabs" /> <slot name="panels" /> </div></template>Slot Forwarding — прокидывание слотов
Заголовок раздела «Slot Forwarding — прокидывание слотов»Когда компонент-обёртка должен прокинуть слоты внутрь:
<template> <BaseComponent> <!-- Прокидываем все слоты с их scoped данными --> <template v-for="(_, name) in $slots" #[name]="slotProps"> <slot :name="name" v-bind="slotProps || {}" /> </template> </BaseComponent></template>Шпаргалка по слотам
Заголовок раздела «Шпаргалка по слотам»| Синтаксис | Что делает |
|---|---|
<slot /> | Дефолтный слот |
<slot name="header" /> | Именованный слот |
<slot :item="item" /> | Scoped слот (передаёт данные) |
<slot>fallback</slot> | Слот с запасным контентом |
#header | Сокращение для v-slot:header |
v-slot="{ item }" | Деструктуризация scoped данных |
$slots.header | Проверка наличия слота |
defineSlots<{...}>() | Типизация слотов (Vue 3.3+) |