Polymorphic компоненты

Полиморфные компоненты в SvelteKit представляют собой универсальные компоненты, способные рендериться с разными HTML- или пользовательскими тегами в зависимости от переданных пропсов. Основная цель полиморфизма — повышение переиспользуемости кода и упрощение управления стилизацией и поведением компонентов без создания множества специализированных версий одного и того же элемента.

В JavaScript-библиотеках UI, таких как svelte-ui или shadcn-svelte, полиморфные компоненты часто используют проп as, который определяет тег или компонент, в котором будет осуществляться рендеринг.

<script lang="ts">
  export let as: string = 'div';
</script>

<svelte:element this={as}>
  <slot />
</svelte:element>

В этом примере <svelte:element> — это ключевой инструмент для создания полиморфного компонента в SvelteKit. Проп as позволяет динамически подставлять любой HTML-элемент.


Применение пропа as

Использование пропа as делает компонент гибким для разных контекстов:

<Button as="a" href="/home">Перейти на главную</Button>
<Button as="button" on:click={handleClick}>Нажми меня</Button>
  • Первый пример рендерит <a> с ссылкой.
  • Второй — обычную кнопку с обработчиком события.

Такой подход устраняет дублирование кода, когда один и тот же компонент должен вести себя как <button> или <a>.


Наследование стилей и классов

Полиморфные компоненты сохраняют унифицированный стиль, независимо от того, какой тег используется. Это достигается через объединение классов:

<script lang="ts">
  export let className: string = '';
  export let as: string = 'div';
</script>

<svelte:element this={as} class={`base-styles ${className}`}>
  <slot />
</svelte:element>

Здесь base-styles задают общий дизайн, а className позволяет добавлять специфичные стили. Такой подход особенно полезен для библиотек UI, где компоненты должны выглядеть одинаково на разных элементах.


Полиморфные компоненты и типизация

В TypeScript важно корректно типизировать проп as и сопутствующие свойства. Простейший способ — использование дженериков и условной типизации:

type PolymorphicProps<T extends keyof HTMLElementTagNameMap> = {
  as?: T;
} & Omit<svelte.JSX.HTMLAttributes<HTMLElementTagNameMap[T]>, 'as'>;

Пример использования:

<script lang="ts">
  import type { PolymorphicProps } from './types';
  
  export let as: 'a' | 'button' = 'button';
  export let href?: string;
</script>

<svelte:element this={as} href={href}>
  <slot />
</svelte:element>

Таким образом, редактор поддерживает автодополнение и проверку типов, предотвращая ошибки передачи неподходящих атрибутов для конкретного тега.


Сложные сценарии: обёртка других компонентов

Полиморфные компоненты не ограничиваются HTML-элементами. Они могут оборачивать и другие компоненты, передавая им пропсы:

<script lang="ts">
  export let as: typeof CustomCard | 'div' = 'div';
</script>

<svelte:component this={as}>
  <slot />
</svelte:component>

<svelte:component> используется для динамического рендеринга компонентов. Такой подход позволяет создавать универсальные UI-обёртки, которые могут рендерить как стандартный элемент, так и кастомный компонент библиотеки.


Передача дополнительных атрибутов

Для полноценной гибкости важно корректно передавать все атрибуты, включая обработчики событий и классы:

<script lang="ts">
  export let as: string = 'div';
  export let props: Record<string, any> = {};
</script>

<svelte:element this={as} {...props}>
  <slot />
</svelte:element>

Пример использования:

<Polymorphic as="button" props={{ onClick: handleClick, class: 'btn-primary' }}>
  Нажми меня
</Polymorphic>

Такой подход делает компонент максимально универсальным и расширяемым.


Взаимодействие с TailwindCSS или другими CSS-библиотеками

Полиморфные компоненты идеально сочетаются с утилитарными классами:

<Button as="a" class="text-white bg-blue-500 px-4 py-2 rounded">
  Перейти
</Button>
  • as="a" задаёт тег <a>.
  • Классы Tailwind автоматически применяются к выбранному элементу, сохраняя единый стиль.

Оптимизация производительности

Полиморфные компоненты могут создавать лишние уровни рендера, если использовать их слишком глубоко в дереве. Для оптимизации рекомендуется:

  1. Минимизировать количество вложенных <svelte:element> и <svelte:component>.
  2. Использовать проп as только там, где это действительно необходимо.
  3. Комбинировать с slot для передачи содержимого, а не создавать дополнительные компоненты для каждого варианта тега.

Полезные паттерны

  • Компонент-кнопка с поддержкой ссылок и событий.
  • Карточка с возможностью рендеринга как <article> или <div>.
  • Обёртка текста для <p>, <span>, <strong> с едиными стилями.

Использование этих паттернов обеспечивает масштабируемость UI-библиотеки, облегчает поддержку и делает компоненты гибкими для любых задач.

Полиморфные компоненты являются фундаментальным инструментом при создании универсальных, переиспользуемых и типизированных компонентов в SvelteKit UI-библиотеках.