14. Slots и Snippets (Svelte 5)
🎰 Slots и Snippets: Композиция компонентов
Заголовок раздела «🎰 Slots и Snippets: Композиция компонентов»Slots — это «дырки» в компоненте, куда родитель вставляет свой контент. Представь компонент как рамку для картины: рамка одна, а картина — разная 🖼️ В Svelte 5 slots заменяются snippets, но концепция та же.
Зачем нужны Slots?
Заголовок раздела «Зачем нужны Slots?»Без slots:<Button label="Нажми меня" icon="star" />// Компонент должен знать о каждом варианте контента
С slots:<Button> ⭐ Нажми меня</Button>// Родитель решает, что внутри!Slots — это основа компонентной компоновки (composition pattern).
Default Slot — слот по умолчанию
Заголовок раздела «Default Slot — слот по умолчанию»<div class="card"> <slot /> <!-- Сюда вставится всё, что между тегами --></div><!-- Использование --><Card> <h2>Заголовок</h2> <p>Любой контент!</p> <button>Действие</button></Card>Fallback контент
Заголовок раздела «Fallback контент»Если слот не передан — показывается содержимое по умолчанию:
<button class="btn"> <slot> <!-- Fallback: показывается если слот не передан --> Нажать </slot></button><!-- Кнопка с кастомным текстом --><Button>Сохранить ✅</Button>
<!-- Кнопка с fallback текстом "Нажать" --><Button />Named Slots — именованные слоты
Заголовок раздела «Named Slots — именованные слоты»<div class="layout"> <header> <slot name="header"> <!-- Fallback заголовок --> <h1>Мой сайт</h1> </slot> </header>
<main> <slot /> <!-- Безымянный слот — основной контент --> </main>
<aside> <slot name="sidebar" /> </aside>
<footer> <slot name="footer"> <p>© 2024</p> </slot> </footer></div><!-- Использование --><Layout> <!-- Именованный слот через slot="..." --> <svelte:fragment slot="header"> <h1>Моя страница</h1> <nav>...</nav> </svelte:fragment>
<!-- Безымянный слот — просто контент без атрибута --> <article> <p>Основной контент страницы</p> </article>
<div slot="sidebar"> <p>Виджеты боковой панели</p> </div>
<!-- footer не передан — используется fallback --></Layout>svelte:fragment для групп элементов
Заголовок раздела «svelte:fragment для групп элементов»Когда нужно передать несколько элементов в именованный слот без лишней обёртки:
<!-- Без svelte:fragment — лишний div --><Layout> <div slot="header"> <!-- div попадает в DOM! --> <h1>Заголовок</h1> <p>Подзаголовок</p> </div></Layout>
<!-- С svelte:fragment — без лишнего элемента --><Layout> <svelte:fragment slot="header"> <h1>Заголовок</h1> <p>Подзаголовок</p> </svelte:fragment></Layout>Slot Props — данные из дочернего компонента
Заголовок раздела «Slot Props — данные из дочернего компонента»Slot props — это способ передать данные из компонента обратно в слот. Scoped slots в Vue, render props в React:
<!-- List.svelte — компонент списка с данными --><script> export let items: string[]</script>
<ul> {#each items as item, index} <!-- Передаём item и index как slot props --> <slot {item} {index} isFirst={index === 0} isLast={index === items.length - 1} /> {/each}</ul><!-- Использование — получаем slot props через let: --><List items={['Яблоко', 'Банан', 'Манго']} let:item let:index let:isFirst let:isLast> <li class:first={isFirst} class:last={isLast}> {index + 1}. {item} {#if isFirst}🥇{/if} {#if isLast}🏁{/if} </li></List>Продвинутый пример: DataTable с slot props
Заголовок раздела «Продвинутый пример: DataTable с slot props»<script lang="ts"> export let rows: Record<string, unknown>[] export let columns: { key: string; label: string }[]
let sortKey = '' let sortDir = 1
$: sorted = sortKey ? [...rows].sort((a, b) => { const av = a[sortKey] const bv = b[sortKey] if (typeof av === 'string' && typeof bv === 'string') { return av.localeCompare(bv) * sortDir } return ((av as number) - (bv as number)) * sortDir }) : rows
function sort(key: string) { if (sortKey === key) sortDir *= -1 else { sortKey = key; sortDir = 1 } }</script>
<table> <thead> <tr> {#each columns as col} <th on:click={() => sort(col.key)}> {col.label} {#if sortKey === col.key}{sortDir > 0 ? '↑' : '↓'}{/if} </th> {/each} </tr> </thead> <tbody> {#each sorted as row, i} <!-- Передаём row, index и isEven в слот --> <slot {row} rowIndex={i} isEven={i % 2 === 0} /> {/each} </tbody></table><!-- Кастомный рендер строк! --><DataTable {rows} {columns} let:row let:rowIndex let:isEven> <tr class:even={isEven}> <td>{rowIndex + 1}</td> <td style="font-weight: bold">{row.name}</td> <td> <span class="badge">{row.role}</span> </td> <td> <button on:click={() => editRow(row)}>✏️</button> <button on:click={() => deleteRow(row)}>🗑️</button> </td> </tr></DataTable>$$slots — проверка наличия слота
Заголовок раздела «$$slots — проверка наличия слота»<script> // $$slots — объект с именами переданных слотов // { default: true, header: true } ← пример</script>
<div class="card"> {#if $$slots.header} <div class="card-header"> <slot name="header" /> </div> {/if}
<div class="card-body"> <slot /> </div>
{#if $$slots.footer} <div class="card-footer"> <slot name="footer" /> </div> {/if}</div>Полезно для:✅ Условное отображение обёртки слота✅ Разная вёрстка в зависимости от наличия слота✅ Опциональные секции компонентаSvelte 5: {#snippet} — Переиспользуемые фрагменты 🆕
Заголовок раздела «Svelte 5: {#snippet} — Переиспользуемые фрагменты 🆕»Snippets — замена slots в Svelte 5. Они мощнее и гибче:
<!-- Svelte 5 --><script lang="ts"> let items = ['Яша', 'Петя', 'Маша']</script>
<!-- Объявление snippet — как inline компонент -->{#snippet userItem(name: string, index: number)} <div class="user"> <span class="badge">{index + 1}</span> <span class="name">{name}</span> <span class="avatar">{name[0]}</span> </div>{/snippet}
<!-- Использование с @render --><div class="user-list"> {#each items as item, i} {@render userItem(item, i)} {/each}</div>Svelte 5: children snippet — замена default slot
Заголовок раздела «Svelte 5: children snippet — замена default slot»<!-- Card.svelte — Svelte 5 --><script lang="ts"> import type { Snippet } from 'svelte'
let { title, children, // Обязательный children snippet actions, // Опциональный snippet для кнопок variant = 'default', } = $props<{ title: string children: Snippet actions?: Snippet variant?: 'default' | 'danger' | 'success' }>()</script>
<div class="card card--{variant}"> <h3 class="card-title">{title}</h3> <div class="card-body"> {@render children()} </div> {#if actions} <div class="card-actions"> {@render actions()} </div> {/if}</div><!-- Использование Card.svelte — Svelte 5 --><Card title="Мой профиль" variant="success"> <p>Имя: Яша</p> <p>Возраст: 10</p>
{#snippet actions()} <button class="btn-secondary">Отмена</button> <button class="btn-primary">Сохранить</button> {/snippet}</Card>Svelte 5: Snippets с параметрами
Заголовок раздела «Svelte 5: Snippets с параметрами»<script lang="ts"> import type { Snippet } from 'svelte'
interface Row { id: number [key: string]: unknown }
let { rows, columns, rowRenderer, emptyState, } = $props<{ rows: Row[] columns: string[] rowRenderer: Snippet<[Row, number]> // Тип: Snippet<[аргументы]> emptyState?: Snippet }>()</script>
<table> <thead> <tr>{#each columns as col}<th>{col}</th>{/each}</tr> </thead> <tbody> {#if rows.length === 0} {#if emptyState} <tr><td colspan={columns.length}>{@render emptyState()}</td></tr> {/if} {:else} {#each rows as row, i} {@render rowRenderer(row, i)} {/each} {/if} </tbody></table><!-- Использование --><Table {rows} columns={['ID', 'Имя', 'Email']}> {#snippet rowRenderer(row, i)} <tr class:even={i % 2 === 0}> <td>{row.id}</td> <td style="font-weight: bold">{String(row.name)}</td> <td style="color: #64748b">{String(row.email)}</td> </tr> {/snippet}
{#snippet emptyState()} <div style="text-align: center; padding: 2rem; color: #64748b"> 📭 Нет данных </div> {/snippet}</Table>Сравнение: Slots (Svelte 4) vs Snippets (Svelte 5)
Заголовок раздела «Сравнение: Slots (Svelte 4) vs Snippets (Svelte 5)»┌────────────────────────────┬────────────────────────────────┐│ Svelte 4 (Slots) │ Svelte 5 (Snippets) │├────────────────────────────┼────────────────────────────────┤│ <slot /> │ {@render children()} ││ <slot name="header" /> │ {@render header()} ││ export let header: ... │ let { header } = $props() ││ slot:item={value} │ Snippet<[paramType]> ││ let:item (в родителе) │ {#snippet name(param)} ... {/} ││ $$slots.header │ if (header) {@render header()} ││ <svelte:fragment slot="x"> │ {#snippet x()} ... {/snippet} │└────────────────────────────┴────────────────────────────────┘Паттерн: Compound Components
Заголовок раздела «Паттерн: Compound Components»<script lang="ts"> import type { Snippet } from 'svelte' import { setContext } from 'svelte'
let { children } = $props<{ children: Snippet }>()
let openItem = $state<string | null>(null)
setContext('accordion', { get open() { return openItem }, toggle: (id: string) => { openItem = openItem === id ? null : id } })</script>
<div class="accordion">{@render children()}</div><script lang="ts"> import type { Snippet } from 'svelte' import { getContext } from 'svelte'
let { id, title, children } = $props<{ id: string title: string children: Snippet }>()
const accordion = getContext<{ open: string | null; toggle: (id: string) => void }>('accordion') const isOpen = $derived(accordion.open === id)</script>
<div class="accordion-item"> <button class="accordion-trigger" on:click={() => accordion.toggle(id)}> {title} <span>{isOpen ? '▲' : '▼'}</span> </button> {#if isOpen} <div class="accordion-content" transition:slide> {@render children()} </div> {/if}</div>