Ключевой механизм расширения функциональности в SvelteKit UI-библиотеках — композиция. Вместо изменения базового компонента создаётся обёртка, которая добавляет поведение, не нарушая внутреннюю реализацию.
<!-- Button.svelte -->
<script>
export let variant = 'primary';
</script>
<button class={`btn ${variant}`}>
<slot />
</button>
<!-- IconButton.svelte -->
<script>
import Button from './Button.svelte';
export let icon;
</script>
<Button>
<span class="icon">{icon}</span>
<slot />
</Button>
Композиция позволяет:
Добавление новых возможностей часто реализуется через расширение
набора props и использование реактивных выражений.
<script>
export let loading = false;
export let disabled = false;
$: isDisabled = disabled || loading;
</script>
<button disabled={isDisabled}>
{#if loading}
<span class="spinner" />
{/if}
<slot />
</button>
Реактивность Svelte позволяет автоматически пересчитывать состояние без дополнительного кода управления.
Слоты — основной инструмент для внедрения кастомного контента и расширения структуры компонента.
<!-- Card.svelte -->
<div class="card">
<header>
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</div>
Использование:
<Card>
<h1 slot="header">Заголовок</h1>
<p>Контент</p>
<button slot="footer">ОК</button>
</Card>
Именованные слоты:
Расширение поведения часто требует передачи событий наружу.
<!-- Input.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function handleInput(e) {
dispatch('input', e.target.value);
}
</script>
<input on:input={handleInput} />
<!-- FormField.svelte -->
<script>
import Input from './Input.svelte';
export let value;
</script>
<Input on:input={(e) => value = e.detail} />
Forwarding позволяет:
Actions — мощный инструмент для добавления поведения на уровне DOM без изменения компонента.
// tooltip.js
export function tooltip(node, text) {
function show() {
const el = document.createElement('div');
el.textContent = text;
el.className = 'tooltip';
document.body.appendChild(el);
}
node.addEventListener('mouseenter', show);
return {
destroy() {
node.removeEventListener('mouseenter', show);
}
};
}
<button use:tooltip={'Подсказка'}>
Наведи курсор
</button>
Преимущества:
Контекст используется для передачи данных между компонентами без явной передачи через props.
// context.js
import { setContext, getContext } from 'svelte';
export const KEY = {};
export function setTheme(theme) {
setContext(KEY, theme);
}
export function getTheme() {
return getContext(KEY);
}
<!-- Provider.svelte -->
<script>
import { setTheme } from './context';
setTheme('dark');
</script>
<slot />
<!-- Child.svelte -->
<script>
import { getTheme } from './context';
const theme = getTheme();
</script>
<div class={theme}>...</div>
Контекст полезен для:
Stores позволяют внедрять реактивное состояние в компоненты.
// store.js
import { writable } from 'svelte/store';
export const modalStore = writable(false);
<script>
import { modalStore } from './store';
</script>
{#if $modalStore}
<div class="modal">...</div>
{/if}
Расширение через store:
В Svelte нет классических HOC, но их можно реализовать через композицию.
<!-- withLoading.svelte -->
<script>
export let component;
export let loading = false;
</script>
{#if loading}
<div>Загрузка...</div>
{:else}
<svelte:component this={component} />
{/if}
Использование:
<WithLoading component={UserList} loading={true} />
Svelte поддерживает директивы (use:, bind:,
on:), которые можно комбинировать для расширения
поведения.
<input
bind:value
on:focus
use:tooltip={'Введите текст'}
/>
Комбинирование директив:
Позволяют выбирать компонент во время выполнения.
<script>
import Button from './Button.svelte';
import Link from './Link.svelte';
export let as = 'button';
const components = {
button: Button,
link: Link
};
</script>
<svelte:component this={components[as]}>
<slot />
</svelte:component>
Такой подход используется в UI-библиотеках для:
Логика может выноситься в переиспользуемые функции.
export function createToggle(initial = false) {
let value = initial;
function toggle() {
value = !value;
}
return {
get value() {
return value;
},
toggle
};
}
Использование:
<script>
import { createToggle } from './toggle';
const toggle = createToggle();
</script>
<button on:click={toggle.toggle}>
{toggle.value ? 'ON' : 'OFF'}
</button>
UI-библиотеки часто позволяют расширять стили через CSS-переменные или классы.
:root {
--btn-bg: blue;
}
.btn {
background: var(--btn-bg);
}
Переопределение:
.custom-theme {
--btn-bg: red;
}
<div class="custom-theme">
<Button>Кнопка</Button>
</div>
Адаптеры позволяют подключать сторонние библиотеки или API.
export function createFetchAdapter(fetchFn) {
return {
get: (url) => fetchFn(url).then(r => r.json())
};
}
Использование:
const api = createFetchAdapter(fetch);
Компонент предоставляет только логику, а разметка полностью контролируется.
<script>
export let open = false;
export let toggle;
</script>
<slot {open} {toggle} />
Использование:
<Dropdown let:open let:toggle>
<button on:click={toggle}>Меню</button>
{#if open}
<div>Содержимое</div>
{/if}
</Dropdown>
Преимущества:
Компоненты могут загружаться динамически:
<script>
let Component;
async function load() {
const module = await import('./HeavyComponent.svelte');
Component = module.default;
}
</script>
<button on:click={load}>Загрузить</button>
{#if Component}
<svelte:component this={Component} />
{/if}
Некоторые UI-библиотеки поддерживают систему плагинов.
export function withAnalytics(component) {
return function(props) {
console.log('render');
return component(props);
};
}
Добавление ARIA-атрибутов и клавиатурной навигации:
<button
aria-expanded={open}
on:keydown={(e) => e.key === 'Enter' && toggle()}
>
Меню
</button>
Реальные UI-библиотеки используют комбинацию техник:
Так достигается: