Перейти к содержимому

16. Специальные элементы

Svelte имеет набор встроенных «магических» элементов — они начинаются с svelte: и дают возможности, которые нельзя получить обычным HTML или компонентами. Это как суперспособности для шаблонов 🦸


<svelte:self> — рекурсивный рендер компонента
<svelte:component> — динамический выбор компонента
<svelte:element> — динамический HTML тег
<svelte:window> — события и привязки window
<svelte:document> — события document
<svelte:body> — события и классы body
<svelte:head> — meta теги, title, CSS
<svelte:fragment> — обёртка без DOM элемента
<svelte:options> — настройки компонента

Позволяет компоненту рендерить самого себя. Идеально для деревьев:

TreeNode.svelte
<script lang="ts">
interface TreeNode {
id: number
label: string
children?: TreeNode[]
}
export let node: TreeNode
export let depth = 0
let expanded = depth < 2 // Первые 2 уровня раскрыты
</script>
<div class="tree-node" style="padding-left: {depth * 16}px">
<button
class="toggle"
on:click={() => expanded = !expanded}
class:has-children={node.children?.length}
>
{#if node.children?.length}
{expanded ? '▼' : '▶'}
{:else}
{/if}
{node.label}
</button>
{#if expanded && node.children}
{#each node.children as child (child.id)}
<!-- Рекурсия! -->
<svelte:self node={child} depth={depth + 1} />
{/each}
{/if}
</div>
<!-- Использование -->
<script>
import TreeNode from './TreeNode.svelte'
const tree = {
id: 1,
label: '📁 Проект',
children: [
{ id: 2, label: '📁 src', children: [
{ id: 3, label: '📄 App.svelte' },
{ id: 4, label: '📄 main.ts' },
{ id: 5, label: '📁 components', children: [
{ id: 6, label: '📄 Button.svelte' },
]},
]},
{ id: 7, label: '📄 package.json' },
],
}
</script>
<TreeNode node={tree} />

Рендерит разные компоненты в зависимости от значения:

<script lang="ts">
import Button from './Button.svelte'
import Link from './Link.svelte'
import IconButton from './IconButton.svelte'
type ButtonType = 'button' | 'link' | 'icon'
export let type: ButtonType = 'button'
export let label: string
const components = {
button: Button,
link: Link,
icon: IconButton,
} as const
$: component = components[type]
</script>
<!-- Динамически выбирает компонент! -->
<svelte:component this={component} {label} {...$$restProps} />
<!-- Паттерн: Page Router -->
<script>
import HomePage from './pages/HomePage.svelte'
import AboutPage from './pages/AboutPage.svelte'
import ContactPage from './pages/ContactPage.svelte'
import NotFoundPage from './pages/NotFoundPage.svelte'
const routes = {
'/': HomePage,
'/about': AboutPage,
'/contact': ContactPage,
}
let currentPath = window.location.pathname
$: currentComponent = routes[currentPath] ?? NotFoundPage
</script>
<!-- this={null} — ничего не рендерится -->
<svelte:component this={currentComponent} />
<script>
let component = null // Ничего не рендерится!
</script>
{#if condition}
<!-- Альтернатива {#if} -->
<svelte:component this={condition ? MyComponent : null} />
{/if}

Когда нужен разный HTML тег, но одинаковое содержимое:

<script lang="ts">
export let level: 1 | 2 | 3 | 4 | 5 | 6 = 1
export let text: string
$: tag = `h${level}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
</script>
<!-- Рендерит h1, h2, h3 и т.д. в зависимости от level -->
<svelte:element this={tag} class="heading">
{text}
</svelte:element>
<!-- Паттерн: Полиморфный компонент -->
<script lang="ts">
export let as: string = 'div' // HTML тег
export let href: string | undefined = undefined
// Если href есть — используем 'a'
$: tag = href ? 'a' : as
</script>
<svelte:element this={tag} {href} {...$$restProps}>
<slot />
</svelte:element>
<!-- Использование -->
<Polymorphic>Обычный div</Polymorphic>
<Polymorphic as="section">Секция</Polymorphic>
<Polymorphic href="/about">Ссылка (тег a)</Polymorphic>
<Polymorphic as="article">Статья</Polymorphic>

<script>
let scrollY = 0
let innerWidth = 0
let innerHeight = 0
let online = navigator.onLine
// Клавиши
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') closeModal()
if (event.ctrlKey && event.key === 's') saveDocument()
}
</script>
<!-- Привязки к свойствам window -->
<svelte:window
bind:scrollY
bind:innerWidth
bind:innerHeight
bind:online
on:keydown={handleKeydown}
on:resize={() => console.log('Размер изменился')}
on:beforeunload={e => { e.preventDefault(); return '' }}
/>
<p>Прокрутка: {scrollY}px</p>
<p>Ширина окна: {innerWidth}px</p>
<p>
{innerWidth < 768 ? '📱 Мобильный' : '🖥️ Десктоп'}
</p>
bind:innerWidth — ширина viewport
bind:innerHeight — высота viewport
bind:outerWidth — ширина окна с рамкой
bind:outerHeight — высота окна с рамкой
bind:scrollX — горизонтальная прокрутка
bind:scrollY — вертикальная прокрутка
bind:online — онлайн статус

<script>
let pointerX = 0
let pointerY = 0
let activeElement: Element | null = null
function handlePointerMove(event: PointerEvent) {
pointerX = event.clientX
pointerY = event.clientY
}
</script>
<svelte:document
on:pointermove={handlePointerMove}
on:visibilitychange={() => {
if (document.hidden) pauseVideo()
}}
bind:activeElement
/>
<!-- Курсор всегда отображается -->
<div
class="cursor"
style="left: {pointerX}px; top: {pointerY}px"
/>

<script>
export let theme: 'dark' | 'light' = 'dark'
export let modalOpen = false
</script>
<svelte:body
class:dark-theme={theme === 'dark'}
class:modal-open={modalOpen}
on:touchstart={handleTouchStart}
on:touchend={handleTouchEnd}
/>
<!-- При modalOpen=true на body добавляется класс modal-open -->
<!-- CSS: .modal-open { overflow: hidden; } -->

<!-- +page.svelte (SvelteKit) -->
<script>
export let data
</script>
<svelte:head>
<!-- Динамический title -->
<title>{data.post.title} | Мой блог</title>
<!-- Meta теги для SEO -->
<meta name="description" content={data.post.excerpt} />
<meta property="og:title" content={data.post.title} />
<meta property="og:image" content={data.post.coverImage} />
<meta property="og:type" content="article" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={data.post.title} />
<!-- Canonical URL -->
<link rel="canonical" href="https://myblog.com/posts/{data.post.slug}" />
<!-- Дополнительные стили для этой страницы -->
<link rel="stylesheet" href="/syntax-highlight.css" />
</svelte:head>
<article>
<h1>{data.post.title}</h1>
<!-- ... -->
</article>
Seo.svelte
<!-- Компонент для переиспользования SEO -->
<script lang="ts">
export let title: string
export let description: string
export let image: string | undefined = undefined
export let noindex = false
const siteName = 'Мой Сайт'
const siteUrl = 'https://mysite.com'
</script>
<svelte:head>
<title>{title} | {siteName}</title>
<meta name="description" content={description} />
{#if noindex}
<meta name="robots" content="noindex, nofollow" />
{/if}
{#if image}
<meta property="og:image" content={image} />
<meta name="twitter:image" content={image} />
{/if}
</svelte:head>

<!-- Когда нужно передать несколько элементов в именованный слот -->
<Layout>
<!-- БЕЗ svelte:fragment — нужен лишний div -->
<div slot="header">
<Logo />
<Nav />
<SearchBar />
</div>
<!-- С svelte:fragment — нет лишнего div в DOM! -->
<svelte:fragment slot="header">
<Logo />
<Nav />
<SearchBar />
</svelte:fragment>
</Layout>

<!-- В начале файла, до <script> -->
<svelte:options
immutable={true}
accessors={true}
namespace="svg"
customElement="my-button"
/>
<script>
export let count = 0
</script>
<!-- svelte:options immutable=true говорит Svelte:
"Данные не мутируются — только заменяются" -->
<svelte:options immutable={true} />
<script>
export let items: string[]
// Svelte теперь сравнивает items по ссылке (===)
// вместо глубокого сравнения
// БЫСТРЕЕ для больших списков!
</script>
{#each items as item}
<li>{item}</li>
{/each}
Counter.svelte
<svelte:options accessors={true} />
<script>
export let count = 0
export function increment() { count++ }
</script>
<!-- Родитель может обращаться напрямую! -->
<script>
import Counter from './Counter.svelte'
let counter: Counter
function external() {
console.log(counter.count) // Читаем
counter.increment() // Вызываем метод
}
</script>
<Counter bind:this={counter} />
<button on:click={external}>Внешнее управление</button>
<!-- Компилируется в настоящий Web Component! -->
<svelte:options customElement="my-cool-button" />
<script>
export let label = 'Нажми'
export let variant = 'primary'
</script>
<button class="btn btn--{variant}">
{label}
</button>
<!-- Теперь можно использовать без Svelte! -->
<!-- <my-cool-button label="Привет" variant="secondary"></my-cool-button> -->