Builders в экосистеме SvelteKit UI-библиотек представляют собой функциональные конструкции, позволяющие декларативно и модульно расширять поведение компонентов без усложнения их внутренней реализации. Основная идея заключается в композиции поведения через функции, которые возвращают набор атрибутов, событий и реактивных состояний, предназначенных для привязки к DOM-элементам.
Builders широко используются в библиотеках, ориентированных на headless-подход: логика отделена от представления, а конечный разработчик сам определяет разметку и стили.
Типичная builder-функция возвращает объект со следующими элементами:
Пример упрощённой структуры:
function createButtonBuilder(options) {
let disabled = options.disabled ?? false;
function onClick(event) {
if (disabled) return;
options.onClick?.(event);
}
return {
props: {
role: 'button',
tabindex: disabled ? -1 : 0,
'aria-disabled': disabled
},
events: {
click: onClick
},
state: {
disabled
}
};
}
Использование в компоненте:
<script>
const button = createButtonBuilder({
onClick: () => console.log('clicked')
});
</script>
<button {...button.props} on:click={button.events.click}>
Click
</button>
Ключевое преимущество builders — возможность комбинировать несколько источников поведения в одном элементе.
function hoverBuilder() {
let isHovered = false;
return {
props: {},
events: {
mouseenter: () => isHovered = true,
mouseleave: () => isHovered = false
},
state: { isHovered }
};
}
function focusBuilder() {
let isFocused = false;
return {
props: {},
events: {
focus: () => isFocused = true,
blur: () => isFocused = false
},
state: { isFocused }
};
}
Композиция:
function composeBuilders(...builders) {
return builders.reduce((acc, builder) => {
return {
props: { ...acc.props, ...builder.props },
events: { ...acc.events, ...builder.events },
state: { ...acc.state, ...builder.state }
};
}, { props: {}, events: {}, state: {} });
}
Использование:
const combined = composeBuilders(
hoverBuilder(),
focusBuilder()
);
При композиции возможны конфликты:
click, keydown)tabindex, role)Решения:
function chainHandlers(...handlers) {
return (event) => {
for (const handler of handlers) {
handler?.(event);
}
};
}
Применение:
events: {
click: chainHandlers(builderA.click, builderB.click)
}
Позволяет явно задавать порядок:
composeBuilders(highPriorityBuilder, lowPriorityBuilder)
Разделение state:
state: {
hover: { isHovered },
focus: { isFocused }
}
Builders часто возвращают actions, которые инкапсулируют работу с DOM:
function tooltipBuilder(content) {
function action(node) {
function show() {
// логика отображения tooltip
}
node.addEventListener('mouseenter', show);
return {
destroy() {
node.removeEventListener('mouseenter', show);
}
};
}
return {
actions: {
tooltip: action
}
};
}
Использование:
<div use:tooltipBuilder("Info").actions.tooltip>
Hover me
</div>
Builders могут использовать Svelte store для управления состоянием:
import { writable } from 'svelte/store';
function toggleBuilder() {
const active = writable(false);
return {
props: {},
events: {
click: () => active.update(v => !v)
},
state: {
active
}
};
}
Использование:
<script>
const toggle = toggleBuilder();
</script>
<button on:click={toggle.events.click}>
{$toggle.state.active ? 'ON' : 'OFF'}
</button>
Builders являются фундаментом headless UI:
Пример: dropdown builder
function dropdownBuilder() {
let open = false;
return {
state: { open },
events: {
toggle: () => open = !open,
close: () => open = false
}
};
}
Возможные реализации:
<!-- Вариант 1 -->
<button on:click={dropdown.events.toggle}>Menu</button>
<!-- Вариант 2 -->
<div class="custom-trigger" on:click={dropdown.events.toggle}>
Open
</div>
Builders хорошо сочетаются с slot props:
<script>
const dropdown = dropdownBuilder();
</script>
<slot {dropdown} />
Использование:
<MyDropdown let:dropdown>
<button on:click={dropdown.events.toggle}>Open</button>
</MyDropdown>
Builder можно расширять через обёртки:
function withLogging(builder) {
return {
...builder,
events: Object.fromEntries(
Object.entries(builder.events).map(([key, fn]) => [
key,
(e) => {
console.log(key);
fn(e);
}
])
)
};
}
type Builder = {
props?: Record<string, any>;
events?: Record<string, (e: Event) => void>;
state?: Record<string, any>;
};
function composeBuilders(...builders: Builder[]): Builder {
// типобезопасная композиция
}
Улучшение через generics:
function createBuilder<TState>(state: TState): {
state: TState;
}
Builders минимизируют избыточные перерендеры:
Оптимизации:
function accessibleButton(options) {
let disabled = options.disabled ?? false;
function handleKeydown(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
options.onClick?.(e);
}
}
return {
props: {
role: 'button',
tabindex: disabled ? -1 : 0,
'aria-disabled': disabled
},
events: {
click: options.onClick,
keydown: handleKeydown
}
};
}
| Подход | Особенности |
|---|---|
| Компоненты | Жёсткая структура |
| HOC | Сложность вложенности |
| Hooks (React) | Привязка к framework |
| Builders | Гибкость + композиция |
Builders органично вписываются в:
Они позволяют создавать UI-логику, независимую от среды выполнения, что особенно важно для SSR и hydration.