Расширение функционала компонентов

Ключевой механизм расширения функциональности в 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>

Композиция позволяет:

  • повторно использовать стили и поведение
  • избегать дублирования кода
  • создавать иерархию UI-компонентов

Расширение через props и реактивность

Добавление новых возможностей часто реализуется через расширение набора 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>

Именованные слоты:

  • позволяют гибко менять структуру
  • делают компонент более универсальным
  • уменьшают необходимость создания новых компонентов

Forwarding событий (event forwarding)

Расширение поведения часто требует передачи событий наружу.

<!-- 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

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>

Преимущества:

  • отделение логики от разметки
  • повторное использование
  • минимальное вмешательство в компонент

Контексты (Context API)

Контекст используется для передачи данных между компонентами без явной передачи через 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>

Контекст полезен для:

  • темизации
  • глобальных настроек
  • управления состоянием UI

Расширение через store

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:

  • добавляет глобальное состояние
  • позволяет синхронизировать компоненты
  • упрощает управление сложными интерфейсами

Higher-Order Components (HOC) через обёртки

В 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-библиотеках для:

  • создания универсальных компонентов
  • поддержки разных HTML-ролей
  • адаптации под контекст

Миксины через утилиты и функции

Логика может выноситься в переиспользуемые функции.

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>

Расширение через адаптеры (adapters)

Адаптеры позволяют подключать сторонние библиотеки или API.

export function createFetchAdapter(fetchFn) {
  return {
    get: (url) => fetchFn(url).then(r => r.json())
  };
}

Использование:

const api = createFetchAdapter(fetch);

Паттерн Headless UI

Компонент предоставляет только логику, а разметка полностью контролируется.

<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>

Преимущества:

  • максимальная гибкость
  • полное управление UI
  • минимальные ограничения

Lazy loading и расширение производительности

Компоненты могут загружаться динамически:

<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);
  };
}

Управление доступностью (a11y) как часть расширения

Добавление ARIA-атрибутов и клавиатурной навигации:

<button
  aria-expanded={open}
  on:keydown={(e) => e.key === 'Enter' && toggle()}
>
  Меню
</button>

Комбинирование подходов

Реальные UI-библиотеки используют комбинацию техник:

  • композиция + slots
  • store + context
  • actions + директивы
  • headless + стилизация

Так достигается:

  • масштабируемость
  • гибкость API
  • высокая переиспользуемость компонентов