@alexgyver/component 1.4.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,313 +1,732 @@
1
1
  # Component.js
2
- Библиотека для создания и настройки DOM/SVG элементов как JS объектов
3
-
4
- > npm i @alexgyver/component
5
-
6
- ## Дока
7
- ### EL
8
- Параметры для конфига:
9
-
10
- - `context` - объект, в контексте которого будут созданы некоторые параметры
11
- - `$` - текст, добавляет созданный элемент в контекст с именем $значение
12
- - `events` - объект с событиями вида `{eventName: handlerFunc}`, привязываются к контексту, если указан
13
- - `click`, `change`, `input`, `mousewheel` - события, указывается обработчик. Короткая форма подключения этих событий
14
- - `also` - функция, будет вызвана с созданным элементом, в контексте, если он указан
15
- - `text` - текст, добавится в textContent, нулевые значения - очистить
16
- - `html` - текст, добавится в innerHTML, нулевые значения - очистить
17
- - `tag` - HTML тег (для child-объектов)
18
- - `push` - массив, добавляет созданный элемент в указанный массив
19
- - `parent` - Node элемент, к которому добавить созданный элемент как child
20
- - `attrs` - объект с аттрибутами для установки через setAttribute
21
- - `props` - объект со свойствами, будут добавлены просто как el[prop] = val
22
- - `data` - объект с датасетами вида `{name: value}`, будут добавлены как аттрибуты data-name = value
23
- - `child`, `child_r` - объект конфига, добавится как ребёнок к созданному элементу. Нужно указать tag, без указания будет div
24
- - `children`, `children_r` - массив объектов конфига, добавятся как дети к созданному элементу. Нужно указать tag, без указания будет div
25
- - `animate` - объект стилей `{styleName: value}`, может содержать параметры анимации duration и easing
26
- - `style`, `style_r` - стили. Принимает:
27
- - Строки CSS стилей
28
- - Объект вида `{styleName: value}`
29
- - `class`, `class_r` - установка классов в classList, версия с `_r` - заменить классы. Принимает:
30
- - Строки вида `newClass active`
31
- - Массивы вида `['newClass', 'active']`, причём можно по условию: `['newClass', isActive && 'active']`
32
- - Объекты вида `{newClass: true, active: false}` - true значение добавляет класс
33
- - `onConfig` - функция, вызовется при настройке через функцию config
34
- - `onMount` - функция, вызовется при присоединении к DOM
35
- - Другие значения будут добавлены как свойства
2
+ Реактивный микро-фреймворк для создания и обновления HTML элементов как JS объектов
3
+
4
+ - Ванильный JS, прямая работа из браузера без компиляции, поддержка рантайм eval-плагинов
5
+ - 2.5 кБ gzip против 6 кБ Preact (минимальный идентичный пример), возможна компиляция в пико-версию 1.5 кБ
6
+ - Без Virtual DOM - в 1.5 раза быстрее Preact
7
+ - Установка и удаление свойств, атрибутов, датасетов, классов, стилей, анимаций, обработчиков событий
8
+ - Обновление параметров
9
+ - Поддержка Shadow DOM
10
+ - Нормализация текстовых и численных полей
11
+ - Поддержка реактивного State
12
+ - Жизненный цикл: коллбэки на монтирование, рендер, обновление и удаление элемента
13
+ - Автоотключение обработчиков и биндов при удалении элемента
14
+ - Поддержка SVG элементов + набор готовых инструментов
15
+ - Создание шаблонов компонентов
16
+ - Проекты на базе: [UI.js](https://github.com/GyverLibs/UI.js), [SVPlot.js](https://github.com/GyverLibs/SVPlot.js), [Settings](https://github.com/GyverLibs/Settings), [Bitmaper2](https://github.com/AlexGyver/Bitmaper2), [ota-projects](https://github.com/AlexGyver/ota-projects)
17
+
18
+ [demo](https://gyverlibs.github.io/Component.js/test/)
19
+
20
+ > NPM: `npm i @alexgyver/component`
21
+ >
22
+ > Browser: https://gyverlibs.github.io/Component.js/Component.min.js
23
+ >
24
+ > Browser (tiny): https://gyverlibs.github.io/Component.js/Component.tiny.min.js
25
+ >
26
+ > Browser (pico): https://gyverlibs.github.io/Component.js/Component.pico.min.js
27
+
28
+ ## EL
29
+ ### Конфиг
30
+ Параметры для конфига `cfg`, с которым вызывается `make`/`update`:
31
+
32
+ | Параметр | Принимает | Описание |
33
+ |--------------|--------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
34
+ | `parent` | `Node` | Монтировать созданный элемент в указанный |
35
+ | `tag` | `Строка` | HTML тег для child-объектов |
36
+ | `child` | `Конфиг\|Массив [Конфиг\|HTML\|Node\|Null\|Массив\|State]` | Создать детей и добавить к элементу. Без указания `tag` будет добавлен `div`. Null игнорируются |
37
+ | `child_r` | Как у `child` | Заменить детей |
38
+ | `props` | `Объект {name: Any\|Функция\|State}` | Добавить свойства как `el[name] = val` |
39
+ | `attrs` | `Объект {name: Any\|Функция\|State}` | Добавить аттрибуты как `setAttribute(name, val)`. `null` - удалить аттрибут |
40
+ | `data` | `Объект {name: Any\|Функция\|State}` | Добавить датасеты как `data-name = val`. `null` - удалить датасет |
41
+ | `style` | | Добавить стили в style |
42
+ | | `Строка\|Функция\|State` | CSS стили вида `color: red; padding: 0` |
43
+ | | `Объект {styleName: Строка\|Функция\|State}` | При `styleName`==`_raw` прибавляет стили в виде строки. Поддерживает `'--property'` |
44
+ | `style_r` | Как у `style` | Заменить все стили |
45
+ | `class` | | Добавить классы в classList |
46
+ | | `Строка\|Функция\|State` | Строка вида `newClass active foo bar` |
47
+ | | `Объект {className: Bool\|Функция\|State}` | true значение добавляет класс, false убирает. При `className`==`_raw` прибавляет классы в виде строки |
48
+ | | `Массив [Строка\|Null]` | Массив строк классов, null игнорируются, например `['foo', false && 'bar']` |
49
+ | `class_r` | Как у `class` | Заменить все классы |
50
+ | `transition` | `Объект {styleName: Any, ... }` | Добавить CSS переход `duration: 300, easing: 'ease', delay: 0, onEnd: func`, время в мс, можно задать обработчик однократного окончания |
51
+ | `events` | `Объект {eventName: func}` | Подключить события, где `eventName` вида click, change... |
52
+ | | `Функция` | Функция-обработчик |
53
+ | | `Объект {handler: Функция, opts...}` | Можно добавить опции для addEventListener, например `once: true, passive: false` |
54
+ | `events_r` | Как у `events` | Заменить все обработчики событий |
55
+ | `on<>` | `Функция\|Объект` | Если параметр начинается с `on` - добавить обработчик события (перехват стандартных onclick/onpress). Вид объекта как у `events` |
56
+ | `also` | `Функция` | Вызовется в конце текущего `make`/`update` |
57
+ | `onMount` | `Функция` | Вызовется при присоединении к DOM |
58
+ | `onRender` | `Функция` | Вызовется при отрисовке (момент появления размеров) |
59
+ | `onUpdate` | `Функция` | Вызовется при обновлении (через `update` или `State`) |
60
+ | `onResize` | `Функция` | Вызовется при изменении размера |
61
+ | `onDestroy` | `Функция` | Вызовется при удалении через `EL.remove(el)`, `EL.clear(el)` или `el.remove()` |
62
+ | `context` | `Объект` | Привязать контекст, он сам пробрасывается в child. При обновлении контекста все вызовы будут происходить с ним, а не со старым |
63
+ | `$` | `Строка` | Добавить созданный элемент в context с именем `$значение` |
64
+ | `push` | `Массив` | Добавить созданный элемент в указанный массив |
65
+ | `Другие` | `Строка\|Функция\|State` | Будут добавлены как `props` |
66
+
67
+ > [!TIP]
68
+ > Короткие имена параметров: `text` == `textContent`, `html` == `innerHTML`, `ctx` == `context`
69
+
70
+ > [!NOTE]
71
+ > При обновлении `update` всё что указано как **добавить** - добавится, а **заменить** - заменит старое. Обработчики событий и `State`-бинды **не заменяют** старые, но корректно очищаются при уничтожении элемента. Обработчики жизненного цикла (onMount, onRender...) подключаются только при создании элемента, в update их обновить нельзя
72
+
73
+ > [!NOTE]
74
+ > Во все обработчики передаётся объект `{el, ctx}`, где el - сам элемент, ctx - его контекст. Также во все обработчики прокидывается контекст в `this` (если обработчик - function()). В обработчики событий элементов (click, input...) и onEnd анимаций добавляется сам `Event`, т.е. свойства `e.target` и прочие
75
+
76
+ ### Функции
77
+ Статические методы EL:
36
78
 
37
79
  ```js
38
- /**
39
- * Создать компонент и поместить его в переменную $root
40
- * @param {string} tag html tag элемента
41
- * @param {object} data параметры
42
- */
43
- EL(tag, data = {}, svg = false);
44
-
45
- /**
46
- * Создать компонент
47
- * @param {string} tag html tag элемента
48
- * @param {object} data параметры
49
- * @returns {Node}
50
- */
51
- EL.make(tag, data = {});
52
-
53
- /**
54
- * Создать теневой компонент от указанного tag, дети подключатся к нему в shadowRoot
55
- * @param {string|Node} host html tag теневого элемента или Node
56
- * @param {object} data параметры внешнего элемента
57
- * @param {string} sheet css стили
58
- * @returns {Node} host
59
- */
60
- EL.makeShadow(host, data = {}, sheet = null);
61
-
62
- /**
63
- * Настроить элемент
64
- * @param {Node | Array} el элемент или массив элементов
65
- * @param {object} data параметры
66
- * @returns {Node}
67
- */
68
- EL.config(el, data);
69
-
70
- /**
71
- * Создать массив компонентов из массива объектов конфигурации
72
- * @param {array} arr массив объектов конфигурации
73
- * @returns {array} of Elements
74
- */
75
- EL.makeArray(arr);
80
+ // Создать элемент
81
+ EL.make(tag, cfg = {}, svg = false)
82
+
83
+ // Обновить элемент
84
+ EL.update(el, cfg);
85
+
86
+ // Подключить к родителю, вернёт Promise
87
+ EL.mount(el, parent, waitRender = false, tries = 100);
88
+
89
+ // Заменить ребёнка old на нового el, old удалить, у el запустить монтаж с вызовом обработчиков. Вернёт el
90
+ EL.replace(old, el);
91
+
92
+ // Удалить всех детей
93
+ EL.clear(el, recursive = true);
94
+
95
+ // Удалить элемент
96
+ EL.remove(el, recursive = true);
97
+
98
+ // Отключить on-обработчики
99
+ EL.release(el);
100
+
101
+ // Отключить state-бинды
102
+ EL.unbind(el);
103
+
104
+ // Создать теневой элемент от указанного тега/Node host, дети подключатся к нему в shadowRoot, стили запишутся в $style
105
+ EL.makeShadow(host, cfg = {}, css = '');
106
+
107
+ // Определить глобальный шаблон, fn - функция, возвращающая cfg-конфиг
108
+ EL.setTemplate(name, tag, fn);
109
+
110
+ // Вызвать шаблон
111
+ EL.useTemplate(name, ...args);
76
112
  ```
77
113
 
78
- ### SVG
114
+ > [!NOTE]
115
+ > Методы `update`, `mount`, `clear`, `remove` добавляются к созданному элементу и передают его первым аргументом, то есть можно вызывать `el.update(cfg)`, `el.mount(parent)` и т.д.
116
+
117
+ ### Компиляция
118
+ При установке **ifdef-loader** (`npm install ifdef-loader --save-dev`) флагов `TINY_COMPONENT` и `PICO_COMPONENT`:
119
+
120
+ ```
121
+ module: {
122
+ rules: [
123
+ {
124
+ test: /\.js$/,
125
+ loader: 'ifdef-loader',
126
+ options: {
127
+ TINY_COMPONENT: true,
128
+ PICO_COMPONENT: true,
129
+ }
130
+ }
131
+ ]
132
+ },
133
+ ```
134
+
135
+ Только `TINY`:
136
+
137
+ - Не создаются методы на элемент
138
+ - Нет lifecycle и обработчиков onMount, onUpdate...
139
+ - Удаление и очистка элемента не рекурсивные
140
+ - Нет отключения обработчиков событий в remove
141
+ - Нет функций replace, release, unbind, makeShadow, setTemplate, useTemplate
142
+ - Нет поддержки State и функций в значениях параметров
143
+
144
+ `TINY` + `PICO`:
145
+
146
+ - Нет SVG
147
+ - Нет функций addCSS, removeCSS, watchMount, watchResize
148
+
149
+ ### State
79
150
  ```js
80
- SVG.make(tag, data);
81
- SVG.config(el, data);
82
- SVG.makeArray(arr);
83
-
84
- SVG.svg(attrs = {}, props = {});
85
- SVG.rect(x, y, w, h, rx, ry, attrs = {}, props = {});
86
- SVG.circle(x, y, r, attrs = {}, props = {});
87
- SVG.line(x1, y1, x2, y2, attrs = {}, props = {});
88
- SVG.polyline(points, attrs = {}, props = {});
89
- SVG.polygon(points, attrs = {}, props = {});
90
- SVG.path(d, attrs = {}, props = {});
91
- SVG.text(text, x, y, attrs = {}, props = {});
151
+ constructor(init = {});
152
+
153
+ // добавить состояния
154
+ addStates(obj);
155
+
156
+ // имеет состояние
157
+ hasState(name);
158
+
159
+ // подключить состояние
160
+ bind(name, map = (e) => e.value);
161
+
162
+ // подписаться, функция вида fn(name, val)
163
+ subscribe(name, fn);
92
164
  ```
93
165
 
94
- ### Sheet
166
+ Для реактивного поведения нужно создать `State` с параметрами как:
167
+
95
168
  ```js
96
- /**
97
- * Добавить стиль с уникальным id в head. ext - стиль можно будет удалить по id
98
- * @param {string|array} style стили в виде css
99
- * @param {string|this} id уникальный id стиля. При передаче this будет именем класса
100
- * @param {boolean} ext внешний стиль - может быть удалён по id
101
- */
102
- Sheet.addStyle(style, id, ext = false);
103
-
104
- /**
105
- * Удалить ext стиль по его id
106
- * @param {string} id id стиля
107
- */
108
- Sheet.removeStyle(id);
169
+ let state = new State({foo: 1});
170
+ let state2 = useState({foo: 1}); // React-стиль
109
171
  ```
110
172
 
111
- ### StyledComponent
173
+ > [!WARNING]
174
+ > Нельзя создавать параметры с именами `subscribe`, `addStates`, `hasState` и `bind`! Они не будут работать
175
+
176
+ У объекта можно менять и читать созданные поля как
177
+
112
178
  ```js
113
- /**
114
- * Создать компонент и поместить его в переменную $root
115
- * @param {string} tag html tag элемента
116
- * @param {object} data параметры
117
- * @param {string|array} style стили в виде css строки
118
- * @param {string|this} id уникальный id стиля. При передаче this будет именем класса
119
- * @param {boolean} ext внешний стиль - может быть удалён по id
120
- */
121
- StyledComponent(tag, data = {}, style = null, id = null, ext = false);
122
-
123
- /**
124
- * Создать компонент
125
- * @param {string} tag html tag элемента
126
- * @param {object} data параметры
127
- * @param {string|array} style стили в виде css строки
128
- * @param {string|this} id уникальный id стиля. При передаче this будет именем класса
129
- * @param {boolean} ext внешний стиль - может быть удалён по id
130
- */
131
- StyledComponent.make(tag, data = {}, style = null, id = null, ext = false);
179
+ state.foo = 123;
180
+ console.log(state.foo);
132
181
  ```
133
182
 
134
- ## Примеры
135
- Создаст контейнер с двумя вложенными блоками текста и прикрепит к body
183
+ Для подключения стейта к элементу используется метод `bind(имя)` или `bind(имя, map)`, например:
136
184
 
137
185
  ```js
138
- EL.make('div', {
186
+ let state = useState({ count: 0 });
187
+
188
+ EL.make('button', {
139
189
  parent: document.body,
140
- class: 'my-div',
141
- style: {
142
- background: 'red',
190
+ text: state.bind('count'),
191
+ onclick: () => {
192
+ state.count += 1; // будет менять text
143
193
  },
144
- events: {
145
- click: () => console.log('click'),
194
+ });
195
+ ```
196
+ > [!NOTE]
197
+ > Подключать `State` и массив с ними можно только в параметры, где это поддерживается
198
+
199
+ Кастомизированный вывод:
200
+
201
+ ```js
202
+ EL.make('button', {
203
+ parent: document.body,
204
+ text: state.bind('count', e => 'Count: ' + e.value),
205
+ onclick: () => {
206
+ state.count += 1; // будет менять text
146
207
  },
147
- children: [
148
- {
149
- tag: 'span',
150
- text: 'hello',
151
- },
152
- {
153
- tag: 'span',
154
- text: 'world',
155
- }
156
- ]
157
208
  });
158
209
  ```
159
210
 
160
- Простой элемент с текстом и классами
211
+ Также можно передать массив стейтов. Начальное значение будет взято из **первого**, реакция будет на все:
161
212
 
162
213
  ```js
163
- const box = new EL('div', {
164
- text: 'Привет, мир!',
165
- class: ['box', 'highlight'],
166
- style: { padding: '10px', color: 'white', backgroundColor: 'blue' },
167
- click() { console.log('Box кликнут!'); },
168
- $: 'myBox' // создаёт ctx.$myBox
214
+ let state = useState({ count: 0, name: 'test' });
215
+
216
+ EL.make('button', {
217
+ parent: document.body,
218
+ class: 'btn',
219
+ text: [
220
+ state.bind('count', e => 'Count: ' + e.value),
221
+ state.bind('name', e => 'Name: ' + e.value),
222
+ ]
169
223
  });
170
224
 
171
- document.body.appendChild(box.$root);
172
- console.log(box.$root); // сам DOM-элемент
173
- console.log(EL.context.$myBox); // доступ через контекст
225
+ state.count = 10; // изменит текст на 'Count: 10'
226
+ state.name = 'hello'; // изменит текст на 'Name: hello'
174
227
  ```
175
228
 
176
- Полная замена классов и стилей
229
+ Стейты поддерживаются везде, где это указано в списке параметров выше, то есть у классов и стилей тоже:
177
230
 
178
231
  ```js
179
- EL.config(someElement, {
180
- text: 'Обновлённый блок',
181
- class_r: ['newClass', 'active'], // полностью заменяет классы
182
- style_r: { color: 'red', fontWeight: 'bold' } // сброс и новые стили
232
+ let state = useState({ foo: false, bar: true, display: 'block' });
233
+
234
+ EL.make('button', {
235
+ parent: document.body,
236
+ // class: state.bind('foo'),
237
+ class: {
238
+ // Для удобного задания дефолтных значений в объекте можно прописать их в параметр _raw
239
+ _raw: 'my-button my-class',
240
+ foo: state.bind('foo'), // bool
241
+ bar: state.bind('bar'), // bool
242
+ },
243
+ style: {
244
+ display: state.bind('display'), // string
245
+ }
183
246
  });
184
247
  ```
185
248
 
186
- Вложенные дети и массив children
249
+ Стейт поддерживает любые типы данных, то есть можно прибиндиться к `child_r` и реактивно заменять детей объектами конфига:
250
+
251
+ ```js
252
+ let state = useState({ children: [] });
187
253
 
188
- ```cpp
189
254
  EL.make('div', {
190
255
  parent: document.body,
191
- class: 'parent',
192
- children: [
193
- { tag: 'p', text: 'Первый ребёнок', class: 'child' },
194
- { tag: 'p', text: 'Второй ребёнок', class: ['child', 'second'] },
195
- 'Просто текстовый узел'
196
- ]
256
+ class: 'card',
257
+ child_r: state.bind('children'),
197
258
  });
259
+
260
+ state.children = [
261
+ {
262
+ tag: 'span',
263
+ text: 'hello',
264
+ },
265
+ {
266
+ tag: 'span',
267
+ text: 'world',
268
+ }
269
+ ];
198
270
  ```
199
271
 
200
- Гораздо интереснее использовать в классе и передавать контекст. Параметр `$` создаст переменную с элементом с указанным именем + префикс `$`:
272
+ Повторный стейт (на один и тот же параметр) будет заменён, т.е. при обновлении останется последний:
201
273
 
202
274
  ```js
203
- class Button {
204
- constructor(text) {
205
- EL.make('button', {
206
- context: this,
207
- $: 'button',
208
- text: text,
209
- class: 'btn',
210
- events: {
211
- click: console.log(this.$button),
212
- },
213
- });
275
+ let btn = EL.make('button', {
276
+ parent: document.body,
277
+ text: state.bind('name'),
278
+ });
214
279
 
215
- // тут уже существует this.$button
216
- }
217
- }
280
+ EL.update(btn, {
281
+ text: state.bind('name'),
282
+ });
283
+ ```
218
284
 
219
- let btn = new Button('kek');
220
- btn.$button; // элемент кнопки
285
+ На стейт можно подписаться самому как:
286
+
287
+ ```js
288
+ state.subscribe('foo', (key, val) => console.log(key, val));
221
289
  ```
222
290
 
223
- Использование контекста для $ и событий
291
+ ## SVG
292
+ Набор инструментов для создания SVG компонентов:
293
+
294
+ ```js
295
+ SVG.make(tag, cfg);
296
+ SVG.update(el, cfg);
297
+
298
+ // вернёт конфиг
299
+ SVG.svg(attrs, cfg);
300
+ SVG.rect(x, y, w, h, rx, ry, attrs, cfg);
301
+ SVG.circle(x, y, r, attrs, cfg);
302
+ SVG.line(x1, y1, x2, y2, attrs, cfg);
303
+ SVG.polyline(points, attrs, cfg);
304
+ SVG.polygon(points, attrs, cfg);
305
+ SVG.path(d, attrs, cfg);
306
+ SVG.text(text, x, y, attrs, cfg);
307
+
308
+ // создаст и вернёт элемент
309
+ SVG.make_svg(attrs, cfg);
310
+ SVG.make_rect(x, y, w, h, rx, ry, attrs, cfg);
311
+ SVG.make_circle(x, y, r, attrs, cfg);
312
+ SVG.make_line(x1, y1, x2, y2, attrs, cfg);
313
+ SVG.make_polyline(points, attrs, cfg);
314
+ SVG.make_polygon(points, attrs, cfg);
315
+ SVG.make_path(d, attrs, cfg);
316
+ SVG.make_text(text, x, y, attrs, cfg);
317
+ ```
224
318
 
319
+ ## Прочее
225
320
  ```js
226
- class MyComponent extends EL {
227
- constructor() {
228
- super('div', {
229
- class: 'comp',
230
- $: 'root',
231
- children: [
232
- { tag: 'button', text: 'Нажми меня', click() { console.log(this.$root); } }
233
- ]
234
- });
321
+ // Добавить стили уникально. Без ID будет вычислен хэш
322
+ function addCSS(css, id = null);
323
+
324
+ // Удалить стили. Без ID будет вычислен хэш
325
+ function removeCSS(css, id = null);
326
+
327
+ // следить за обновлениями размера элемента
328
+ function watchResize(el, onResize);
329
+
330
+ // проверить статус подключения в DOM и рендера
331
+ function watchMount(el, waitRender = false);
332
+ ```
333
+
334
+ ## Примеры
335
+ ### Минимальный пример
336
+ ```js
337
+ let div1 = EL.make('div', { // создать div
338
+ parent: document.body, // прикрепить к body
339
+ text: 'hello 1', // вывести текст
340
+ class: 'card bordered', // класс строкой
341
+ style: 'color: red', // стиль строкой
342
+ });
343
+
344
+ // стили и классы можно задавать объектом
345
+
346
+ let div2 = EL.make('div', { // создать div
347
+ text: 'hello 2', // вывести текст
348
+ class: { // класс объектом + условно
349
+ card: true,
350
+ bordered: false,
351
+ },
352
+ style: { // стиль объектом
353
+ color: 'green',
354
+ 'font-size': '20px',
235
355
  }
236
- }
356
+ });
237
357
 
238
- const comp = new MyComponent();
239
- document.body.appendChild(comp.$root);
358
+ // можно прикрепить вручную
359
+ div2.mount(document.body);
240
360
  ```
241
361
 
242
- Некоторые трюки
362
+ ### Обновления
363
+ ```js
364
+ // (переменные div1 и div2 из прошлого примера)
365
+ // через 1 сек поменять текст, стиль и убрать класс bordered
366
+
367
+ setTimeout(() => {
368
+ div1.update({
369
+ text: 'hello world!',
370
+ class: {
371
+ bordered: false,
372
+ },
373
+ style: {
374
+ color: 'unset',
375
+ }
376
+ });
377
+ }, 1000);
378
+
379
+ // можно очистить/перезаписать стили и классы через _r
243
380
 
381
+ setTimeout(() => {
382
+ div2.update({
383
+ class_r: '',
384
+ style_r: '',
385
+ });
386
+ }, 2000);
387
+ ```
388
+
389
+ ### Вложенные элементы
244
390
  ```js
245
391
  EL.make('div', {
246
- context: this,
247
- children: [
248
- {}, // валидно
249
- null, // валидно
392
+ parent: document.body,
393
+ class: 'card',
394
+ child: [ // может быть массивом
250
395
  {
251
- // без тега - div
396
+ tag: 'span',
397
+ text: 'hello 1',
252
398
  },
253
- EL.make(...), // контекст будет проброшен сюда автоматически
254
- foo && {...}, // добавить компонент если foo - true
255
399
  {
256
- tag: 'svg', // автоматически запустится режим SVG
257
- children: [
258
- // и будет проброшен сюда
259
- SVG.circle(10, 10, 5),
260
- {
261
- tag: 'line',
262
- attrs: {}
263
- },
264
- ],
265
- },
266
- ],
267
- class: ['some', 'class', foo && 'plus_me'], // добавить plus_me если foo - true
400
+ // без указания тега будет div
401
+ class: 'card',
402
+ child: { // может быть объектом (1 элемент)
403
+ tag: 'span',
404
+ text: 'hello 2',
405
+ }
406
+ }
407
+ ]
408
+ });
409
+
410
+ // трюки
411
+
412
+ EL.make('div', {
413
+ parent: document.body,
414
+ class: 'card',
415
+ child: [
416
+ {}, // валидно, пустой div
417
+ null, // валидно, ничего не добавится
418
+ undefined, // валидно, ничего не добавится
419
+ true && { // можно добавлять детей по условию
420
+ tag: 'span',
421
+ text: 'hello 1',
422
+ }
423
+ ]
268
424
  });
269
425
  ```
270
426
 
427
+ ### События и обработчики
428
+ ```js
429
+ EL.make('button', {
430
+ parent: document.body,
431
+ text: 'press me',
432
+ class: 'btn',
433
+
434
+ // обработчик клика
435
+ onclick: (e) => {
436
+ console.log('click!', e, e.el, e.ctx);
437
+ // e - Event
438
+ // e.el - сам элемент (кнопка)
439
+ // e.ctx - контекст (о нём ниже)
440
+ },
271
441
 
272
- Создание SVG
442
+ // можно добавлять options для событий, обработчик указывается в handler
443
+ onmousedown: {
444
+ handler: () => console.log('press'),
445
+ once: true
446
+ },
447
+
448
+ // можно подключить ещё вот так
449
+ events: {
450
+ mousemove: () => { },
451
+
452
+ input: {
453
+ handler: () => { },
454
+ passive: false
455
+ }
456
+ }
457
+ });
458
+ ```
459
+
460
+ ### Жизненный цикл и его обработчики
461
+ ```js
462
+ // кнопка меняет счётчик, после 5 кликов кнопка удаляется
463
+
464
+ let count = 0;
465
+
466
+ EL.make('button', {
467
+ parent: document.body,
468
+ class: 'btn',
469
+ text: 'press',
470
+ onclick: e => {
471
+ e.el.update({ text: 'update ' + count });
472
+ if (++count == 5) e.el.remove();
473
+ },
474
+ also: () => {
475
+ console.log('div also'); // вызовется после make
476
+ },
477
+ onMount: () => {
478
+ console.log('div mount'); // вызовется после добавления в body
479
+ },
480
+ onRender: () => {
481
+ console.log('div render'); // вызовется после фактического рендера
482
+ },
483
+ onUpdate: () => {
484
+ console.log('div update'); // вызовется после обновления параметров
485
+ },
486
+ onDestroy: () => {
487
+ console.log('div destroy'); // вызовется после удаления
488
+ },
489
+ });
490
+ ```
273
491
 
492
+ ### Стейты и реактивность
274
493
  ```js
275
- const svg = SVG.svg({ width: 200, height: 200 }, {
276
- children: [
277
- SVG.rect(10, 10, 50, 50, 5, 5, { fill: 'green' }),
278
- SVG.circle(100, 100, 40, { fill: 'red' }),
279
- SVG.line(0, 0, 200, 200, { stroke: 'blue', 'stroke-width': 2 })
494
+ // создаём стейт
495
+ let state = new State({ count: 0, name: 'Alex' });
496
+
497
+ // пример 1 (без стейта)
498
+ EL.make('button', {
499
+ parent: document.body,
500
+ class: 'btn',
501
+ text: 0,
502
+ onclick: (e) => e.el.update({ text: Number(e.el.textContent) + 1 }),
503
+ });
504
+
505
+ // пример 2
506
+ EL.make('button', {
507
+ parent: document.body,
508
+ class: 'btn',
509
+ text: state.bind('count'), // привязываем к count
510
+ onclick: () => {
511
+ state.count += 1; // будет менять text
512
+ },
513
+ });
514
+
515
+ // пример 3
516
+ EL.make('button', {
517
+ parent: document.body,
518
+ class: 'btn',
519
+ text: state.bind('count', e => 'Count: ' + e.value), // кастомный вывод
520
+ onclick: () => {
521
+ state.count += 1; // будет менять text
522
+ },
523
+ });
524
+
525
+ // пример 4
526
+ EL.make('div', {
527
+ parent: document.body,
528
+ class: 'card',
529
+ child: [
530
+ {
531
+ tag: 'input',
532
+ type: 'text',
533
+ size: 10,
534
+ value: state.name, // значение по умолчанию
535
+ oninput: e => state.name = e.el.value, // меняем
536
+ },
537
+ {
538
+ tag: 'span',
539
+ text: state.bind('name'), // и меняется тут
540
+ }
280
541
  ]
281
542
  });
282
543
  ```
283
544
 
284
- Shadow DOM со стилями
545
+ ### Контекст и экспорт
546
+ ```js
547
+ let obj = {}; // контекст
548
+ let arr = []; // массив
549
+
550
+ EL.make('div', { // создать div
551
+ push: arr, // добавить div в массив (переменная arr выше)
552
+ ctx: obj, // контекст для $ и обработчиков (переменная obj выше)
553
+ $: 'myDiv', // создать $myDiv в контексте
554
+ parent: document.body, // прикрепить к body
555
+ child: [ // добавить вложенные
556
+ {
557
+ tag: 'span', // элемент span
558
+ $: 'mySpan', // контекст прокидывается в детей, создать $mySpan
559
+ text: 'text', // с текстом 'text'
560
+ },
561
+ {
562
+ tag: 'button',
563
+ class: 'btn',
564
+ text: 'say hello',
565
+ onclick: (e) => {
566
+ // e.ctx - контекст
567
+ // обновим текст и цвет mySpan
568
+ e.ctx.$mySpan.update({
569
+ text: 'hello!',
570
+ style: 'color: red',
571
+ });
572
+ }
573
+ },
574
+ {
575
+ tag: 'button',
576
+ class: 'btn',
577
+ text: 'remove',
578
+ onclick: (e) => {
579
+ e.ctx.$myDiv.remove(); // удалить весь контейнер div
580
+ },
581
+ },
582
+ ],
583
+ });
584
+
585
+ console.log(obj); // {$myDiv: div.card, $mySpan: span, $counter: span}
586
+ console.log(arr); // [div.card]
587
+
588
+ // добавим поле для счётчика. Используется контекст родителя
589
+ obj.$myDiv.update({
590
+ child: {
591
+ tag: 'span',
592
+ $: 'counter', // создать $counter в obj
593
+ text: 0,
594
+ }
595
+ });
285
596
 
597
+ // будем менять счётчик по таймеру
598
+ let count = 0;
599
+ setInterval(() => {
600
+ obj.$counter.update({
601
+ text: count++,
602
+ });
603
+ }, 1000);
604
+
605
+ // через 3 сек заменим mySpan на новую кнопку и сохраним в контексте
606
+ setTimeout(() => {
607
+ obj.$mySpan = EL.replace(obj.$mySpan, EL.make('button', {
608
+ class: 'btn',
609
+ text: obj.$mySpan.textContent,
610
+ }));
611
+ }, 3000);
612
+ ```
613
+
614
+ ### Анимации
615
+ ```js
616
+ EL.make('div', {
617
+ // анимируем и удаляем после завершения
618
+ parent: document.body,
619
+ style: { width: '50px', height: '50px', backgroundColor: 'orange' },
620
+ transition: {
621
+ width: '150px',
622
+ height: '150px',
623
+ duration: 1500,
624
+ onEnd: (e) => e.el.remove()
625
+ },
626
+ });
627
+ ```
628
+
629
+ ### Шаблоны компонентов
286
630
  ```js
287
- const shadowHost = EL.makeShadow('div', {
288
- child: { tag: 'p', text: 'Текст внутри Shadow DOM', class: 'shadow-text' }
289
- }, `
290
- .shadow-text { color: purple; font-weight: bold; }
291
- `);
631
+ // через глобальный шаблон EL
632
+ EL.setTemplate('userCard', 'div', (name, lastname, birthdate) => ({
633
+ class: 'card',
634
+ child: [
635
+ { tag: 'h3', text: `${name} ${lastname}` },
636
+ { tag: 'p', text: `Birthdate: ${birthdate}` }
637
+ ]
638
+ }));
639
+
640
+ EL.useTemplate('userCard', 'Alice', 'Smith', '1995-06-12').mount(document.body);
641
+ EL.useTemplate('userCard', 'Bob', 'Johnson', '1990-01-01').mount(document.body);
642
+
643
+ // вручную + родитель
644
+ const myTemplate = (name, lastname, birthdate, parent) => (EL.make('div', {
645
+ class: 'card',
646
+ parent,
647
+ child: [
648
+ { tag: 'h3', text: `${name} ${lastname}` },
649
+ { tag: 'p', text: `Birthdate: ${birthdate}` }
650
+ ]
651
+ }));
652
+
653
+ myTemplate('Alice', 'Smith', '1995-06-12', document.body);
654
+ myTemplate('Bob', 'Johnson', '1990-01-01', document.body);
292
655
 
293
- document.body.appendChild(shadowHost);
656
+ // фабрика конфигурации
657
+
658
+ const myButton = (text, color) => ({
659
+ tag: 'button',
660
+ text: text,
661
+ style: {
662
+ _raw: 'padding: 5px 10px; color: white; border: none; border-radius: 4px; margin: 0 5px;',
663
+ backgroundColor: color,
664
+ }
665
+ });
666
+
667
+ EL.make('div', {
668
+ class: 'card',
669
+ parent: document.body,
670
+ child: [
671
+ myButton('hello', 'red'),
672
+ myButton('world', 'blue'),
673
+ myButton('kek', 'green'),
674
+ ]
675
+ });
294
676
  ```
295
677
 
678
+ ### Shadow DOM
679
+ ```js
680
+ // элемент со своими изолированными стилями
296
681
 
297
- Прочее
682
+ EL.makeShadow('div', {
683
+ parent: document.body,
684
+ child: [
685
+ {
686
+ class: 'myclass',
687
+ child: {
688
+ text: 'I am shadow!',
689
+ }
690
+ }
691
+ ]
692
+ },
693
+ '.myclass{color:red;}'
694
+ );
695
+ ```
298
696
 
697
+ ### SVG
299
698
  ```js
300
- const state = new State({ count: 0 });
301
-
302
- let d = EL.make('div', {
303
- text: state.get('count'),
304
- also: el => state.subscribe(d => el.textContent = d.count),
305
- style: { width: '100px', height: '50px', background: 'red' },
306
- animate: { width: '200px', background: 'blue', duration: 500 },
307
- onConfig: el => console.log('update'),
308
- onMount: el => console.log('mount'),
699
+ let circ = SVG.make_circle(100, 100, 30, { fill: 'red' });
700
+
701
+ SVG.make_svg({ width: 200, height: 200 }, {
702
+ parent: document.body,
703
+ style: 'border: 1px solid #ccc',
704
+ child: [
705
+ // вручную
706
+ {
707
+ tag: 'rect',
708
+ attrs: {
709
+ x: 10,
710
+ y: 130,
711
+ width: 50,
712
+ height: 50,
713
+ fill: 'green',
714
+ }
715
+ },
716
+
717
+ // внешний
718
+ circ,
719
+
720
+ // билдеры
721
+ SVG.rect(10, 10, 50, 50, 5, 5, { fill: 'blue' }),
722
+ SVG.line(0, 0, 200, 200, { stroke: 'black', 'stroke-width': 2 })
723
+ ],
309
724
  });
310
725
 
311
- setInterval(() => state.set('count', state.get('count') + 1), 1000);
312
- setTimeout(() => { document.body.appendChild(d) }, 2000);
726
+ EL.make('hr', { parent: document.body });
727
+
728
+ // двигаем кружок
729
+ setInterval(() => {
730
+ circ.update({ attrs: { cx: Math.random() * 200, cy: Math.random() * 200 } })
731
+ }, 300);
313
732
  ```