2026-05-18 16:07:00 +05:00
2026-05-18 15:58:18 +05:00
2026-05-18 16:10:42 +05:00

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);
Description
Библиотека для быстрого и удобного построения динамических DOM элементов
Readme MIT 38 KiB
Languages
JavaScript 100%