8.6 KiB
dom.js — декларативное создание DOM с реактивностью на сигналах
Библиотека предоставляет текучий (fluent) API для создания DOM-элементов и реактивного управления ими через собственную систему сигналов.
Установка
<script src="path/to/dom.js"></script>
Быстрый старт
// Создать кнопку с реактивным текстом
const count = dom.signal(0);
const button = dom.button('counter')
.cls('btn btn-primary')
.text(count)
.on('click', () => count(count() + 1))
.done();
document.body.appendChild(button);
API
Сигналы
Сигнал — реактивный контейнер для значения. При изменении значения все подписчики автоматически уведомляются.
// Создание сигнала
const name = dom.signal('John');
// Чтение значения
console.log(name()); // 'John'
console.log(name.value); // 'John'
// Изменение значения
name('Jane');
name.value = 'Jane';
// Подписка на изменения
const unsubscribe = name.on((newVal, oldVal) => {
console.log(`${oldVal} → ${newVal}`);
});
// Отписка
unsubscribe();
Производные сигналы .map()
Создаёт новый сигнал, значение которого автоматически обновляется при изменении родительского:
const count = dom.signal(1);
const doubled = count.map(x => x * 2);
console.log(doubled()); // 2
count(5);
console.log(doubled()); // 10
Управление памятью .destroy()
Уничтожает сигнал, отписывая его от родителя и очищая всех слушателей:
doubled.destroy();
Отладка .chain() и .listenerCount()
console.log(doubled.chain()); // 'signal_a1b2c → signal_a1b2c_map_1'
console.log(count.listenerCount()); // количество подписчиков
Создание элементов
Любой HTML-тег доступен как метод dom:
dom.div() // <div></div>
dom.div('app') // <div id="app"></div>
dom.section() // <section></section>
dom.span() // <span></span>
CamelCase автоматически преобразуется в kebab-case:
dom.viewBox() // <view-box></view-box>
Инструкции
После создания элемента конфигурация задаётся цепочкой инструкций. Все инструкции возвращают прокси элемента для продолжения цепочки.
| Инструкция | Описание | Пример |
|---|---|---|
.id(id) |
Установить id | .id('main') |
.cls(classes) |
Добавить CSS-классы | .cls('btn primary') |
.atr(name, value) |
Установить атрибут | .atr('href', '/home') |
.atrs({...}) |
Установить несколько атрибутов | .atrs({ href: '/', title: 'Home' }) |
.css(styles) |
Добавить инлайн-стили | .css('color: red;') |
.pcss(selector, styles) |
Scoped-стили | .pcss(':hover', 'color: blue;') |
.mcss(query, styles) |
Media-запросы | .mcss('(max-width: 768px)', 'font-size: 14px;') |
.text(content) |
Установить текстовое содержимое | .text('Hello') |
.child(element) |
Добавить дочерний элемент | .child(dom.span().done()) |
.children([...]) |
Добавить массив дочерних | .children([el1, el2]) |
Важно: внутри .atrs(), .children() и других методов, где значения могут быть сигналами, нужно передавать их явно:
// Передача сигнала в атрибуты
const href = dom.signal('/home');
dom.a().atr('href', href).done();
Scoped-стили: pcss
Создаёт стили, привязанные только к этому элементу, с уникальным классом:
dom.button()
.pcss(':hover', 'background: blue; color: white;')
.pcss('::after', 'content: "→";')
.done();
Сгенерирует <style> с уникальным селектором, действующим только на этот элемент.
Media-запросы: mcss
Создаёт медиа-запросы для конкретного элемента:
dom.div()
.mcss('(max-width: 600px)', 'width: 100%;')
.mcss('(min-width: 1200px)', 'max-width: 800px; margin: 0 auto;')
.done();
Условный рендеринг
const isLoggedIn = dom.signal(false);
dom.div()
.if(isLoggedIn)
.text('Welcome back!')
.elif(dom.signal(false))
.text('Session expired')
.else()
.text('Please log in')
.endif()
.done();
При изменении сигнала DOM перерендеривается автоматически.
Циклы
Неупорядоченный список: ueach
const items = dom.signal(['Apple', 'Banana', 'Cherry']);
dom.div()
.ueach(items)
.done();
Упорядоченный список: oeach
const steps = dom.signal(['Install', 'Configure', 'Run']);
dom.div()
.oeach(steps)
.done();
При изменении массива в сигнале элементы списка обновляются реактивно — добавляются/удаляются по мере необходимости, существующие элементы переиспользуются.
События
Все нативные события доступны через camelCase-алиасы:
dom.button()
.onClick(() => console.log('clicked'))
.onMouseEnter(() => console.log('hover'))
.onKeyDown(e => console.log(e.key))
.done();
Поддерживаемые алиасы событий:
| Категория | Алиасы |
|---|---|
| Keyboard | keyDown, keyUp, keyPress |
| Mouse | mouseEnter, mouseLeave, mouseDown, mouseUp, mouseMove, mouseOver, mouseOut |
| Touch | touchStart, touchEnd, touchMove, touchCancel |
| Focus | focusIn, focusOut |
| Animation | animationStart, animationEnd, animationIteration |
| Transition | transitionEnd |
| Drag | dragStart, dragEnd, dragEnter, dragLeave, dragOver |
| Pointer | pointerDown, pointerUp, pointerMove, pointerEnter, pointerLeave, pointerOver, pointerOut, pointerCancel |
| Wheel | wheelStart, wheelEnd |
| Composition | compositionStart, compositionEnd, compositionUpdate |
| Other | contextMenu, formData, formChange, formInput |
Для событий без алиаса используйте kebab-case в camelCase-нотации:
dom.div().onCustomEvent(handler) // → 'custom-event'
Завершение сборки: .done()
Подписывается на все сигналы, выполняет рендеринг и возвращает готовый DOM-элемент:
const element = dom.div('app')
.cls('container')
.child(dom.span().text('Hello').done())
.done();
document.body.appendChild(element);
Важно: дочерние элементы, добавляемые через .child(), должны быть либо готовым HTMLElement, либо результатом .done().
Полный пример
// Сигналы состояния
const count = dom.signal(0);
const theme = dom.signal('light');
// Приложение
const app = dom.div('app')
.cls('app-container')
.pcss(':hover', 'box-shadow: 0 0 10px rgba(0,0,0,0.1);')
.child(
dom.h1()
.text('Counter')
.done()
)
.child(
dom.p('counter-value')
.text(count.map(x => `Count: ${x}`))
.css('font-size: 24px;')
.done()
)
.child(
dom.div()
.cls('buttons')
.child(
dom.button('increment')
.text('+')
.onClick(() => count(count() + 1))
.done()
)
.child(
dom.button('decrement')
.text('-')
.onClick(() => count(count() - 1))
.done()
)
.done()
)
.done();
document.body.appendChild(app);