dom.js/README.md

280 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# dom.js — декларативное создание DOM с реактивностью на сигналах
Библиотека предоставляет текучий (fluent) API для создания DOM-элементов и реактивного управления ими через собственную систему сигналов.
## Установка
```html
<script src="path/to/dom.js"></script>
```
## Быстрый старт
```javascript
// Создать кнопку с реактивным текстом
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
### Сигналы
Сигнал — реактивный контейнер для значения. При изменении значения все подписчики автоматически уведомляются.
```javascript
// Создание сигнала
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()`
Создаёт новый сигнал, значение которого автоматически обновляется при изменении родительского:
```javascript
const count = dom.signal(1);
const doubled = count.map(x => x * 2);
console.log(doubled()); // 2
count(5);
console.log(doubled()); // 10
```
#### Управление памятью `.destroy()`
Уничтожает сигнал, отписывая его от родителя и очищая всех слушателей:
```javascript
doubled.destroy();
```
#### Отладка `.chain()` и `.listenerCount()`
```javascript
console.log(doubled.chain()); // 'signal_a1b2c → signal_a1b2c_map_1'
console.log(count.listenerCount()); // количество подписчиков
```
### Создание элементов
Любой HTML-тег доступен как метод `dom`:
```javascript
dom.div() // <div></div>
dom.div('app') // <div id="app"></div>
dom.section() // <section></section>
dom.span() // <span></span>
```
CamelCase автоматически преобразуется в kebab-case:
```javascript
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()` и других методов, где значения могут быть сигналами, нужно передавать их явно:
```javascript
// Передача сигнала в атрибуты
const href = dom.signal('/home');
dom.a().atr('href', href).done();
```
#### Scoped-стили: `pcss`
Создаёт стили, привязанные только к этому элементу, с уникальным классом:
```javascript
dom.button()
.pcss(':hover', 'background: blue; color: white;')
.pcss('::after', 'content: "→";')
.done();
```
Сгенерирует `<style>` с уникальным селектором, действующим только на этот элемент.
#### Media-запросы: `mcss`
Создаёт медиа-запросы для конкретного элемента:
```javascript
dom.div()
.mcss('(max-width: 600px)', 'width: 100%;')
.mcss('(min-width: 1200px)', 'max-width: 800px; margin: 0 auto;')
.done();
```
### Условный рендеринг
```javascript
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`
```javascript
const items = dom.signal(['Apple', 'Banana', 'Cherry']);
dom.div()
.ueach(items)
.done();
```
#### Упорядоченный список: `oeach`
```javascript
const steps = dom.signal(['Install', 'Configure', 'Run']);
dom.div()
.oeach(steps)
.done();
```
При изменении массива в сигнале элементы списка обновляются реактивно — добавляются/удаляются по мере необходимости, существующие элементы переиспользуются.
### События
Все нативные события доступны через camelCase-алиасы:
```javascript
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-нотации:
```javascript
dom.div().onCustomEvent(handler) // → 'custom-event'
```
### Завершение сборки: `.done()`
Подписывается на все сигналы, выполняет рендеринг и возвращает готовый DOM-элемент:
```javascript
const element = dom.div('app')
.cls('container')
.child(dom.span().text('Hello').done())
.done();
document.body.appendChild(element);
```
**Важно:** дочерние элементы, добавляемые через `.child()`, должны быть либо готовым `HTMLElement`, либо результатом `.done()`.
## Полный пример
```javascript
// Сигналы состояния
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);
```