@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/Component.min.js +1 -1
- package/Component.min.js.gz +0 -0
- package/Component.pico.min.js +1 -0
- package/Component.pico.min.js.gz +0 -0
- package/Component.tiny.min.js +1 -0
- package/Component.tiny.min.js.gz +0 -0
- package/README.md +654 -235
- package/package.json +16 -9
- package/src/Component.js +15 -0
- package/src/EL.js +371 -0
- package/src/SVG.js +18 -0
- package/src/Sheet.js +73 -0
- package/src/State.js +47 -0
- package/src/utils.js +94 -0
- package/test/index.html +12 -0
- package/test/script.js +434 -124
- package/webpack.config.js +29 -10
- package/webpack.dev.config.js +32 -0
- package/Component.js +0 -345
- package/test/example.html +0 -15
package/README.md
CHANGED
|
@@ -1,313 +1,732 @@
|
|
|
1
1
|
# Component.js
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
EL.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
EL.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
166
|
+
Для реактивного поведения нужно создать `State` с параметрами как:
|
|
167
|
+
|
|
95
168
|
```js
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
173
|
+
> [!WARNING]
|
|
174
|
+
> Нельзя создавать параметры с именами `subscribe`, `addStates`, `hasState` и `bind`! Они не будут работать
|
|
175
|
+
|
|
176
|
+
У объекта можно менять и читать созданные поля как
|
|
177
|
+
|
|
112
178
|
```js
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
186
|
+
let state = useState({ count: 0 });
|
|
187
|
+
|
|
188
|
+
EL.make('button', {
|
|
139
189
|
parent: document.body,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
190
|
+
text: state.bind('count'),
|
|
191
|
+
onclick: () => {
|
|
192
|
+
state.count += 1; // будет менять text
|
|
143
193
|
},
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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: '
|
|
192
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
280
|
+
EL.update(btn, {
|
|
281
|
+
text: state.bind('name'),
|
|
282
|
+
});
|
|
283
|
+
```
|
|
218
284
|
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
239
|
-
document.body
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
null, // валидно
|
|
392
|
+
parent: document.body,
|
|
393
|
+
class: 'card',
|
|
394
|
+
child: [ // может быть массивом
|
|
250
395
|
{
|
|
251
|
-
|
|
396
|
+
tag: 'span',
|
|
397
|
+
text: 'hello 1',
|
|
252
398
|
},
|
|
253
|
-
EL.make(...), // контекст будет проброшен сюда автоматически
|
|
254
|
-
foo && {...}, // добавить компонент если foo - true
|
|
255
399
|
{
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
```
|