Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке слоти у Vue?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Slots (слоти)** дозволяють батьківському компоненту вставляти контент у іменовані місця шаблону дочірнього компонента. Named slots адресують конкретні зони, scoped slots дають дочірньому компоненту можливість передавати власні дані вгору через `v-slot="{ item }"`. ```vue <Card> <template #header><h1>Заголовок</h1></template> <p>Основний контент</p> <template #footer><button>Дія</button></template> </Card> ``` **Ключове:** слоти для контролю структури з боку батька, props для передачі даних.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Slots (слоти)** дозволяють батьківському компоненту вставляти довільний контент у заздалегідь визначені місця в шаблоні дочірнього компонента. ## Теорія ### TL;DR - Слоти - це як USB-порти: дочірній компонент визначає порти, батьківський підключає будь-який контент - Три типи: default (безіменний), named (з іменем), scoped (дочірній передає дані батьківському) - Слоти передають **контент** (HTML, компоненти); props передають **дані** (рядки, об'єкти) - Правило вибору: батько контролює структуру UI? Слот. Тільки дані? Props. - Синтаксис Vue 3: `#name` або `v-slot:name` (стара форма `slot="name"` є застарілою) ### Швидкий приклад ```vue <!-- Alert.vue --> <template> <div class="alert"> <slot name="icon">⚠️</slot> <!-- named slot з fallback --> <slot>Повідомлення за замовчуванням</slot> <!-- default slot --> </div> </template> ``` ```vue <!-- Батько --> <Alert> <template #icon>🚨</template> <p>Термінове повідомлення!</p> </Alert> <!-- Результат: 🚨 Термінове повідомлення! --> ``` Якщо батько не надає `#icon`, показується fallback `⚠️` з дочірнього компонента. Контент всередині `<slot>` - це і є той fallback. ### Слоти проти props Props переносять дані: рядки, числа, об'єкти. Слоти переносять **структуру**: HTML, інші компоненти, все що можна написати в шаблоні. Передати `<CustomButton>` або стилізований абзац через prop неможливо без того, щоб це перетворилось на екранований рядок. Слоти вирішують саме цю проблему. Контент слота живе у скоупі батька. Батько його пише, тому має доступ до своїх змінних і методів. Scoped slots додають зворотний зв'язок: дочірній компонент ділиться власними даними з батьківським. ### Default, named і scoped слоти **Default slot** - безіменний слот. Будь-який контент, розміщений безпосередньо між тегами компонента, потрапляє в нього. **Named slots** дозволяють заповнювати кілька місць окремо. Компонент макету може мати слоти `header`, `default` і `footer`, кожен із яких батько контролює незалежно. ```vue <!-- Layout.vue --> <template> <header><slot name="header" /></header> <main><slot /></main> <footer><slot name="footer" /></footer> </template> ``` ```vue <!-- Використання --> <Layout> <template #header><h1>Заголовок сторінки</h1></template> <p>Основний контент</p> <template #footer><p>© 2025</p></template> </Layout> ``` **Scoped slots** - найгнучкіший тип. Дочірній компонент передає дані через слот, а батьківський вирішує, як їх відобразити. Класичний приклад: список, який сам зберігає дані, але делегує рендеринг батьку. ```vue <!-- UserList.vue --> <template> <ul> <li v-for="user in users" :key="user.id"> <slot :item="user" /> </li> </ul> </template> <script setup> import { ref } from 'vue' const users = ref([{ id: 1, name: 'Аліса' }, { id: 2, name: 'Боб' }]) </script> ``` ```vue <!-- Dashboard.vue --> <UserList> <template #default="{ item }"> <strong>{{ item.name }}</strong> <button @click="editUser(item)">Редагувати</button> </template> </UserList> ``` Список відповідає за ітерацію і дані. Батько контролює відображення. У більшості дашбордів, з якими доводилось працювати, цей патерн зустрічається в кожній таблиці даних - саме завдяки йому бібліотеки Vuetify та Element Plus такі гнучкі без необхідності їх форкати. ### Коли використовувати - Загальний контейнер (картка, модалка, панель) → default slot - Кілька різних областей (header, body, footer) → named slots - Дочірній компонент зберігає дані, батько контролює рендеринг (таблиці, списки, дропдауни) → scoped slots - Передаються тільки рядки або примітиви → достатньо [props](/questions/what-are-props-in-vue) - Глибоке передавання даних без prop drilling → [provide/inject](/questions/provide-inject-in-vue) ### Як Vue обробляє слоти всередині Компілятор шаблонів Vue сканує теги `<slot>` і створює точки вставки в render-функції дочірнього компонента. Під час рендерингу контент батьківського компонента компілюється в окрему render-функцію, яка підставляється в ці точки. Scoped slots працюють так само, але дочірній компонент передає об'єкт з даними при виклику функції слота. У Vue 3 реактивні дані слота передаються через Proxy, тому зміни в дочірньому компоненті автоматично оновлюють контент слота без ре-рендеру всього дерева. Перевірити наявність слота можна через `$slots.name` у шаблоні або `useSlots()` у `<script setup>`: ```vue <script setup> import { useSlots } from 'vue' const slots = useSlots() </script> <template> <slot name="tab2" /> <p v-if="!slots.tab2">Контент не надано</p> </template> ``` ### Типові помилки **Передача HTML через prop:** ```vue <!-- Неправильно --> <Child :content="'<p>Привіт</p>'" /> <!-- Рендериться як екранований рядок, не HTML --> ``` ```vue <!-- Правильно --> <Child><p>Привіт</p></Child> ``` Props серіалізують значення. HTML переданий як рядок відрендериться як `<p>Привіт</p>`. **Використання застарілого синтаксису Vue 2 у Vue 3:** ```vue <!-- Неправильно для Vue 3 --> <Child slot="header">Контент</Child> ``` ```vue <!-- Правильно --> <Child v-slot:header>Контент</Child> <!-- або скорочено --> <Child #header>Контент</Child> ``` Атрибут `slot` є застарілим. Компілятор Vue 3 очікує `v-slot:name` або `#name`. **Спроба змінити дані scoped slot:** ```vue <!-- Неправильно: мутація не збережеться --> <List v-slot="{ item }"> {{ item.name = 'Змінено' }} </List> ``` Slot props доступні тільки для читання в шаблоні батька. Вони передаються через Proxy, який ігнорує мутації. Щоб оновити дані, потрібно emit-нути подію і обробити її в батьку: ```vue <List @update="handleUpdate" v-slot="{ item }"> <button @click="$emit('update', item)">Оновити</button> </List> ``` **Безіменні вкладені слоти:** Коли компоненти вкладені і жоден слот не названий, кілька `<slot />` конкурують за один default. Завжди називай слоти, якщо компонент має більше однієї контентної зони. ### Де використовується - Vuetify: `<v-card><template #text>Custom content</template></v-card>` - Element Plus: `<el-table><template #default="{ row }">...</template></el-table>` - Nuxt UI: компоненти макетів використовують `<slot name="hero" />` для секцій сторінки - PrimeVue: `<DataTable><template #body="slotProps">...</template></DataTable>` Scoped slots - це основа компонентів відображення даних у кожній великій UI-бібліотеці для Vue. ### Follow-up питання **Q:** Яка різниця між default і named слотом? **A:** Default заповнює безіменний `<slot />`. Named слоти відповідають конкретному `<slot name="x" />` через `#x` або `v-slot:x`. В одному компоненті можуть бути обидва типи одночасно. **Q:** Чи може контент слота звертатись до даних дочірнього компонента? **A:** Не напряму. Контент слота живе у скоупі батька і бачить тільки батьківські змінні. Scoped slots вирішують це: `<slot :item="user" />` робить `user` доступним у батьку через `v-slot="{ item }"`. **Q:** Як перевірити, чи батько заповнив слот? **A:** У шаблоні - через `$slots.name`. У `<script setup>` - через `useSlots()` з Composition API, який повертає реактивний об'єкт із наявними слотами. **Q:** Що змінилось у синтаксисі слотів між Vue 2 і Vue 3? **A:** Vue 2 використовував `slot="name"` на елементах і `slot-scope="data"` для scoped слотів. Vue 3 замінив обидва на `v-slot:name="data"` (скорочено `#name="data"`). Старий синтаксис ще працює, але є застарілим і може бути видалений. **Q:** Як Vue оптимізує scoped slots при використанні з Suspense або async-компонентами? **A:** У Vue 3 контент слота може призупинятись незалежно від батька. Дочірній компонент може огорнути слот у `<Suspense>` для показу fallback поки async-контент завантажується. Реактивні дані слота проходять через Proxy, тому при зміні даних в дочірньому компоненті оновлюється тільки відповідний слот, а не все дерево компонентів. ## Приклади ### Базовий: Компонент картки з named slots і fallback ```vue <!-- Card.vue --> <template> <div class="card"> <div class="card-header"> <slot name="header">Заголовок за замовчуванням</slot> </div> <div class="card-body"> <slot /> </div> <!-- footer div існує в DOM тільки якщо батько надав контент --> <div class="card-footer" v-if="$slots.footer"> <slot name="footer" /> </div> </div> </template> ``` ```vue <!-- ProductPage.vue --> <Card> <template #header> <h2>MacBook Pro</h2> </template> <p>Найкращий ноутбук для розробників.</p> <p>Ціна: $2,499</p> <template #footer> <button @click="addToCart">Додати в кошик</button> </template> </Card> ``` `v-if="$slots.footer"` прибирає весь `div` з DOM, якщо батько не надав контент для footer. Не порожній блок, а повна відсутність елемента. Це важливо, коли CSS grid або flex правила адресують прямих дітей. ### Середній: Таблиця даних зі scoped slots Поширений патерн для дашбордів: таблиця відповідає за структуру та ітерацію, рендеринг кожного стовпця делегується батьку. ```vue <!-- DataTable.vue --> <template> <table> <thead> <tr> <th v-for="col in columns" :key="col.key">{{ col.label }}</th> </tr> </thead> <tbody> <tr v-for="row in rows" :key="row.id"> <td v-for="col in columns" :key="col.key"> <!-- scoped slot передає і весь рядок, і значення клітинки --> <slot :name="col.key" :row="row" :value="row[col.key]"> {{ row[col.key] }} <!-- за замовчуванням: просто текст --> </slot> </td> </tr> </tbody> </table> </template> <script setup> defineProps({ columns: Array, rows: Array }) </script> ``` ```vue <!-- UsersDashboard.vue --> <DataTable :columns="cols" :rows="users"> <!-- кастомний рендер для стовпця статусу --> <template #status="{ value }"> <span :class="value === 'active' ? 'badge-green' : 'badge-red'"> {{ value }} </span> </template> <!-- кастомний рендер для стовпця дій --> <template #actions="{ row }"> <button @click="editUser(row)">Редагувати</button> <button @click="deleteUser(row.id)">Видалити</button> </template> </DataTable> ``` Стовпці без кастомного слота автоматично показують звичайний текст. Саме так влаштований `<el-table>` в Element Plus. ### Senior: Реактивне визначення слотів через useSlots ```vue <!-- DashboardWidget.vue --> <template> <div class="widget"> <div class="widget-header"> <slot name="title"> <span>{{ defaultTitle }}</span> </slot> <!-- панель дій монтується тільки якщо батько її заповнив --> <div v-if="slots.actions" class="widget-actions"> <slot name="actions" /> </div> </div> <div class="widget-body"> <slot /> </div> </div> </template> <script setup> import { useSlots } from 'vue' defineProps({ defaultTitle: { type: String, default: 'Віджет' } }) const slots = useSlots() </script> ``` ```vue <!-- AdminPanel.vue --> <DashboardWidget default-title="Трафік"> <template #actions> <button @click="refresh">Оновити</button> <button @click="exportData">Експорт</button> </template> <TrafficChart :data="chartData" /> </DashboardWidget> ``` `useSlots()` повертає реактивний об'єкт. Якщо батько динамічно додає або прибирає `#actions`, віджет реагує без додаткової логіки. Цей підхід прибирає порожні wrapper div з HTML, що важливо для CSS grid макетів, які адресують прямих дітей.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.