tyaff 0.0.1 → 0.0.3

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,2 +1,1032 @@
1
- Tyaff - vdom javascript library
2
- ==================================================
1
+ <center>
2
+ <img src="logo.svg" alt="Логотип" width="220">
3
+ </center>
4
+
5
+ # Tyaff — VDOM библиотека для JavaScript
6
+
7
+ Легковесная альтернатива React на чистом JavaScript (ES6+) с собственным виртуальным DOM и философией минимализма. Библиотека отходит от каноничной модели React там, где она кажется избыточной: вместо иммутабельности — прямая мутация состояния, вместо Provider/Consumer — прозрачный pull-based контекст, вместо чёрного ящика мемоизации — явные массивы зависимостей с точечным контролем над рендером.
8
+
9
+ **Ключевые отличия от React:**
10
+
11
+ - **`memo()` блокирует только текущий компонент** — принимает массив зависимостей и не выполняет `render()` только для себя. Дочерние компоненты продолжают свою цепочку обновлений независимо, что делает оптимизацию предсказуемой и не ломает работу контекста.
12
+
13
+ - **Мутабельные данные из любых источников** — компонент может читать глобальный store, singleton или `window` напрямую, без props drilling. Данные живут там, где им удобно, а компонент просто их потребляет.
14
+
15
+ - **Pull-based контекст без Provider/Consumer** — любой компонент объявляет себя провайдером через объект `context: { key() { ... } }`, а дети ниже по дереву запрашивают значения через `this.context(key)` по мере необходимости. Никаких обёрток и HOC.
16
+
17
+ - **Props первым аргументом** — сигнатуры `init(props)`, `memo(props)`, `render({ title, items })` позволяют деструктурировать props прямо в определении, делая код декларативным и компактным.
18
+
19
+ - **Ключи уникальны в пределах всего render** (а не только среди братьев, как в React) — это позволяет перемещать компоненты между разными родителями с сохранением instance и state.
20
+
21
+
22
+ ## Основные возможности
23
+
24
+ ### 🎯 Компактный и производительный
25
+ - Минимальный размер API
26
+ - Собственный diff/patch алгоритм
27
+ - Кеширование refs и обработчиков
28
+ - Batching обновлений через microtask
29
+
30
+ ### 🔄 Динамическое дерево контекстов
31
+ - Иерархическая система провайдеров
32
+ - Методы `context()` и `contextSelf()`
33
+ - Автоматическая передача контекста через HTML-теги
34
+ - Защита от рекурсии
35
+
36
+ ### 🏗️ Компоненты на основе фабрик
37
+ - Единый способ создания компонентов
38
+ - Автоматический биндинг пользовательских методов
39
+ - Локальное состояние и свойства
40
+ - Гибкий lifecycle
41
+
42
+ ### 🚪 Порталы с отложенным монтированием
43
+ - Монтирование в произвольный DOM-контейнер
44
+ - Якорные текстовые узлы для стабильности
45
+ - Отложенная активация при появлении контейнера
46
+ - Автоматическая очистка при unmount
47
+
48
+ ### 🔑 Система ключей (отличие от React)
49
+ - Пользовательские ключи уникальны в пределах всего render
50
+ - Сохранение instance при перемещении между разными родителями
51
+ - Автоматические ключи на основе пути
52
+ - Экранирование специальных символов
53
+
54
+ ### 🧩 Fragment с key
55
+ - Группировка детей без обёртки (без key)
56
+ - Виртуальный instance для перемещения групп (с key)
57
+ - Сохранение состояния детей при перемещении группы
58
+
59
+ ### 📦 Защита структуры детей
60
+ - Сохранение вложенности массивов
61
+ - Иерархическая генерация ключей
62
+ - Предотвращение сдвига индексов
63
+ - Стабильная идентификация элементов
64
+
65
+
66
+ ## Установка
67
+
68
+ ```javascript
69
+ import { h, Component, createPortal, Fragment, mount, refresh } from 'tyaff';
70
+ ```
71
+
72
+
73
+ ## Публичный API
74
+
75
+ ### h(type, props, ...children)
76
+ Создание VDOM узла.
77
+
78
+ **Параметры:**
79
+ - `type` — строка (HTML-тег), функция (компонент) или Symbol (Fragment/Portal)
80
+ - `props` — объект свойств (может быть `null`)
81
+ - `children` — дочерние элементы
82
+
83
+ **Возвращает:** VDOM объект `{ tag, props, childs }`
84
+
85
+ **Пример:**
86
+ ```javascript
87
+ h('div', { className: 'container' },
88
+ h('h1', null, 'Заголовок'),
89
+ h('p', null, 'Текст')
90
+ )
91
+ ```
92
+
93
+ ### Component(definition)
94
+ Фабрика для создания компонентов.
95
+
96
+ **Параметры:**
97
+ - `definition` — объект с lifecycle методами, свойствами и пользовательскими методами
98
+
99
+ **Возвращает:** Конструктор компонента (с маркером `_definition`)
100
+
101
+ **Структура definition:**
102
+ ```javascript
103
+ {
104
+ name: 'MyComponent', // опционально, для логов ошибок
105
+
106
+ // Lifecycle методы
107
+ init(props),
108
+ render(props),
109
+ props(incoming),
110
+ memo(props),
111
+ onMounted(),
112
+ onUpdated(),
113
+ onUnmounted(),
114
+
115
+ // Объект контекста
116
+ context: {
117
+ theme() { return 'light'; }
118
+ },
119
+
120
+ // Пользовательские методы (АВТОМАТИЧЕСКИ биндятся)
121
+ increment() { this.count++; },
122
+
123
+ // Пользовательские свойства (копируются на инстанс)
124
+ count: 0,
125
+ config: { theme: 'dark' }
126
+ }
127
+ ```
128
+
129
+ ### createPortal(children, containerGetter)
130
+ Создание портала для монтирования в произвольный DOM-контейнер.
131
+
132
+ **Параметры:**
133
+ - `children` — VDOM дети
134
+ - `containerGetter` — функция, возвращающая DOM-элемент или `null`
135
+
136
+ **Возвращает:** VDOM узел с `tag: Symbol(Portal)`
137
+
138
+ **Пример:**
139
+ ```javascript
140
+ createPortal(
141
+ h('div', { className: 'modal' }, 'Содержимое'),
142
+ () => document.getElementById('modal-root')
143
+ )
144
+ ```
145
+
146
+ ### Fragment
147
+ Symbol для создания фрагментов. Поддерживает `key` prop для перемещения групп детей.
148
+
149
+ **Примеры:**
150
+ ```javascript
151
+ // Простая группировка без overhead
152
+ h(Fragment, null,
153
+ h('li', null, 'Item 1'),
154
+ h('li', null, 'Item 2')
155
+ )
156
+
157
+ // Группа с key — можно перемещать, дети сохраняют instance
158
+ h(Fragment, { key: 'group-a' },
159
+ h(Item, { key: 'i1' }),
160
+ h(Item, { key: 'i2' })
161
+ )
162
+ ```
163
+
164
+ ### mount(input, container)
165
+ **Универсальная функция** — единая точка входа для mount, update и unmount.
166
+
167
+ **Поведение:**
168
+ - **Первый вызов** с vnode → создаёт DOM и вставляет в container
169
+ - **Повторный вызов** с vnode → выполняет diff, применяет изменения к существующему DOM
170
+ - **Вызов с `null`** → размонтирует дерево (выполняется `onUnmounted`, `ref(null)`)
171
+
172
+ **Поддерживаемые типы `input`:**
173
+ - **vnode** (объект с `tag`, `props`, `childs`)
174
+ - **Конструктор компонента** — оборачивается в `h(Component, {})`
175
+ - **Массив vnode** — оборачивается в `h(Fragment, {}, ...array)`
176
+ - **Строка/число** — оборачивается в текстовый узел
177
+ - **`null`/`undefined`** — размонтирует дерево
178
+
179
+ **Примеры:**
180
+ ```javascript
181
+ // Первый mount
182
+ mount(App, container);
183
+ mount(h(App, { theme: 'dark' }), container);
184
+
185
+ // Массив
186
+ mount([h('div', null, 'A'), h('div', null, 'B')], container);
187
+
188
+ // Текст
189
+ mount('Hello World', container);
190
+
191
+ // Update
192
+ mount(h(App, { theme: 'light' }), container);
193
+
194
+ // Unmount
195
+ mount(null, container);
196
+ ```
197
+
198
+ **Ограничения:**
199
+ - Один контейнер = одно дерево
200
+ - `onMounted()` выполняется только при первом mount
201
+
202
+ ### refresh()
203
+ Глобальное асинхронное обновление всех примонтированных деревьев.
204
+
205
+ **Сигнатура:**
206
+ ```javascript
207
+ const time = await refresh(); // Promise<number> — время в миллисекундах
208
+ ```
209
+
210
+ **Поведение:**
211
+ - Находит все корневые компоненты
212
+ - Выполняет `update()` у каждого
213
+ - Возвращает `Promise`, который разрешается после завершения всех обновлений
214
+
215
+ **Use cases:**
216
+ - Измерение производительности рендера
217
+ - Интеграция с глобальным state (store, singleton)
218
+ - Async тесты
219
+ - Профилирование slow renders
220
+
221
+ **Пример:**
222
+ ```javascript
223
+ store.items = processData(bigData);
224
+ const time = await refresh();
225
+ console.log(`Render: ${time.toFixed(2)}ms`);
226
+ if (time > 16) console.warn('Slow render');
227
+ ```
228
+
229
+
230
+ ## Архитектура
231
+
232
+ ### Виртуальный DOM
233
+ - Плоские объекты с `tag`, `props`, `childs`
234
+ - Сохранение вложенной структуры массивов
235
+ - Текстовые узлы оборачиваются в объекты `{ _text: '...' }`
236
+ - `null` в VDOM игнорируется при диффе
237
+
238
+ ### Diff алгоритм
239
+ - Сравнение по `tag` и `key`
240
+ - Точечное обновление атрибутов
241
+ - Рекурсивная обработка детей
242
+ - Ключи для сохранения instance при перемещении
243
+
244
+ ### Lifecycle
245
+ - `init(props)` — инициализация (один раз при создании instance)
246
+ - `props(incoming)` — нормализация пропсов (чистая функция)
247
+ - `memo(props)` — массив зависимостей для оптимизации
248
+ - `render(props)` — возврат VDOM
249
+ - `onMounted()` — после вставки в DOM
250
+ - `onUpdated()` — после обновления DOM (только если render выполнен)
251
+ - `onUnmounted()` — перед удалением
252
+
253
+ ### Update Engine
254
+ - Batching через microtask — множественные `update()` объединяются в один render
255
+ - Защита от рекурсии и бесконечных циклов (лимит 50 итераций)
256
+ - Изоляция ошибок — падающий компонент не ломает другие
257
+ - `memo()` блокирует только текущий компонент, дети продолжают цепочку
258
+
259
+
260
+ ## Обработка атрибутов
261
+
262
+ ### HTML-атрибуты
263
+ ```javascript
264
+ // camelCase → lowercase
265
+ { className: 'box' } // → class="box"
266
+ { htmlFor: 'input' } // → for="input"
267
+ { tabIndex: 0 } // → tabindex="0"
268
+ ```
269
+
270
+ ### SVG-атрибуты
271
+ ```javascript
272
+ // camelCase остаются camelCase или конвертируются
273
+ { viewBox: '0 0 100 100' } // → viewBox
274
+ { xlinkHref: '#icon' } // → xlink:href (через setAttributeNS)
275
+ { preserveAspectRatio: 'xMidYMid' } // → preserveAspectRatio
276
+ ```
277
+
278
+ ### Контролируемые формы
279
+ ```javascript
280
+ // Используются DOM property, а не атрибуты
281
+ { value: 'text' } // → element.value
282
+ { checked: true } // → element.checked
283
+ { selected: true } // → element.selected
284
+ ```
285
+
286
+ ### Специальные атрибуты
287
+ ```javascript
288
+ {
289
+ style: { fontSize: '16px' },
290
+ dangerouslySetInnerHTML: { __html: '<b>Bold</b>' },
291
+ ref: this.refs('element')
292
+ }
293
+ ```
294
+
295
+ ### События
296
+ ```javascript
297
+ {
298
+ onClick: (e) => console.log(e),
299
+ onChange: this.handleChange
300
+ }
301
+ ```
302
+
303
+
304
+ ## Производительность
305
+
306
+ ### Оптимизации
307
+ - **Batching обновлений** — множественные `update()` в одной задаче объединяются
308
+ - **WeakMap** — автоматическая очистка памяти при удалении контейнеров
309
+ - **Кеширование refs** — одна функция на имя
310
+ - **Batch-вставка** — `prepend()` с чанками по 20 000 для больших деревьев
311
+ - **memo()** — блокировка ненужных ререндеров
312
+
313
+ ### Рекомендации
314
+ - Используйте `memo()` для оптимизации компонентов
315
+ - Включайте контекстные значения в `memo()`, если компонент их использует
316
+ - Избегайте создания объектов в `render()`
317
+ - Используйте ключи для списков
318
+ - Минимизируйте вложенность компонентов
319
+
320
+
321
+ ## Совместимость
322
+
323
+ - ES6+ (ES2015 и выше)
324
+ - Современные браузеры
325
+ - `WeakMap`, `Symbol`, `Promise`, `Array.from`
326
+ - Без внешних зависимостей
327
+
328
+
329
+ ## Лицензия
330
+
331
+ MIT
332
+
333
+
334
+ ---
335
+
336
+
337
+ # Примеры использования
338
+
339
+ ## 1. Простой компонент
340
+
341
+ ```javascript
342
+ import { h, Component, mount } from './core.js';
343
+
344
+ const HelloWorld = Component({
345
+ render() {
346
+ return h('div', { className: 'hello' },
347
+ h('h1', null, 'Привет, мир!'),
348
+ h('p', null, 'Это tyaff')
349
+ );
350
+ }
351
+ });
352
+
353
+ // Монтаж в DOM
354
+ mount(HelloWorld, document.body);
355
+ ```
356
+
357
+ ## 2. Компонент с состоянием
358
+
359
+ ```javascript
360
+ const Counter = Component({
361
+ count: 0,
362
+
363
+ increment() {
364
+ this.update({ count: this.count + 1 });
365
+ },
366
+
367
+ decrement() {
368
+ this.update({ count: this.count - 1 });
369
+ },
370
+
371
+ render() {
372
+ return h('div', null,
373
+ h('span', null, 'Счётчик: ' + this.count),
374
+ h('button', { onClick: this.decrement }, '-'),
375
+ h('button', { onClick: this.increment }, '+')
376
+ );
377
+ }
378
+ });
379
+
380
+ mount(Counter, document.getElementById('app'));
381
+ ```
382
+
383
+ ## 3. Props первым аргументом
384
+
385
+ ```javascript
386
+ const Button = Component({
387
+ // Нормализация пропсов
388
+ props(incoming) {
389
+ const { label, type = 'button', disabled = false, onClick } = incoming;
390
+ return { label, type, disabled, onClick };
391
+ },
392
+
393
+ // Деструктуризация прямо в сигнатуре
394
+ render({ label, type, disabled, onClick }) {
395
+ return h('button', { type, disabled, onClick }, label);
396
+ }
397
+ });
398
+
399
+ // Использование
400
+ mount(
401
+ h(Button, {
402
+ label: 'Отправить',
403
+ onClick: () => alert('Клик!')
404
+ }),
405
+ document.body
406
+ );
407
+ ```
408
+
409
+ ## 4. Lifecycle методы
410
+
411
+ ```javascript
412
+ const Timer = Component({
413
+ count: 0,
414
+ intervalId: null,
415
+
416
+ init(props) {
417
+ console.log('Инициализация компонента');
418
+ this.intervalId = setInterval(() => {
419
+ this.update({ count: this.count + 1 });
420
+ }, 1000);
421
+ },
422
+
423
+ onMounted() {
424
+ console.log('Компонент смонтирован, DOM доступен');
425
+ },
426
+
427
+ onUpdated() {
428
+ console.log('Компонент обновлён:', this.count);
429
+ },
430
+
431
+ onUnmounted() {
432
+ console.log('Компонент будет удалён');
433
+ clearInterval(this.intervalId);
434
+ },
435
+
436
+ render() {
437
+ return h('div', null, 'Таймер: ' + this.count);
438
+ }
439
+ });
440
+ ```
441
+
442
+ ## 5. memo() для оптимизации
443
+
444
+ ```javascript
445
+ const ExpensiveComponent = Component({
446
+ props(incoming) {
447
+ return {
448
+ data: incoming.data || [],
449
+ multiplier: incoming.multiplier || 1
450
+ };
451
+ },
452
+
453
+ // Зависимости для мемоизации
454
+ memo(props) {
455
+ return [props.data.length, props.multiplier];
456
+ },
457
+
458
+ render(props) {
459
+ console.log('render() выполняется');
460
+ const result = props.data.reduce((sum, item) =>
461
+ sum + item * props.multiplier, 0
462
+ );
463
+
464
+ return h('div', null, 'Результат: ' + result);
465
+ }
466
+ });
467
+
468
+ // render() выполняется только при изменении длины массива или multiplier
469
+ // Если зависимости совпадают — render() блокируется
470
+ ```
471
+
472
+ ## 6. Context (провайдеры)
473
+
474
+ ```javascript
475
+ // Провайдер темы
476
+ const ThemeProvider = Component({
477
+ theme: 'light',
478
+
479
+ context: {
480
+ theme() { return this.theme; },
481
+ toggleTheme() {
482
+ this.theme = this.theme === 'light' ? 'dark' : 'light';
483
+ this.update();
484
+ }
485
+ },
486
+
487
+ render() {
488
+ return h('div', { className: 'theme-provider' },
489
+ h('button',
490
+ { onClick: () => this.contextSelf('toggleTheme') },
491
+ 'Переключить тему'
492
+ ),
493
+ this.props.children
494
+ );
495
+ }
496
+ });
497
+
498
+ // Потребитель темы
499
+ const ThemedButton = Component({
500
+ memo(props) {
501
+ // ✅ Включаем контекст в зависимости
502
+ return [this.context('theme')];
503
+ },
504
+
505
+ render() {
506
+ const theme = this.context('theme');
507
+ return h('button',
508
+ { className: 'btn-' + theme },
509
+ this.props.children
510
+ );
511
+ }
512
+ });
513
+
514
+ mount(
515
+ h(ThemeProvider, null,
516
+ h(ThemedButton, null, 'Кнопка с темой')
517
+ ),
518
+ document.body
519
+ );
520
+ ```
521
+
522
+ ## 7. Вложенный Context
523
+
524
+ ```javascript
525
+ const UserProvider = Component({
526
+ context: {
527
+ user() { return this.props.user || null; },
528
+ isAdmin() {
529
+ const user = this.context('user');
530
+ return user && user.role === 'admin';
531
+ }
532
+ },
533
+
534
+ render() {
535
+ return h('div', null, this.props.children);
536
+ }
537
+ });
538
+
539
+ const AdminPanel = Component({
540
+ render() {
541
+ const isAdmin = this.contextSelf('isAdmin');
542
+
543
+ if (!isAdmin) {
544
+ return h('div', null, 'Доступ запрещён');
545
+ }
546
+
547
+ return h('div', { className: 'admin-panel' }, 'Админ-панель');
548
+ }
549
+ });
550
+
551
+ mount(
552
+ h(UserProvider, { user: { name: 'John', role: 'admin' } },
553
+ h(AdminPanel, null)
554
+ ),
555
+ document.body
556
+ );
557
+ ```
558
+
559
+ ## 8. Refs (ссылки на DOM и компоненты)
560
+
561
+ ```javascript
562
+ const InputFocus = Component({
563
+ onMounted() {
564
+ // DOM доступен после монтирования
565
+ if (this.refs.input) {
566
+ this.refs.input.focus();
567
+ }
568
+ },
569
+
570
+ handleClick() {
571
+ if (this.refs.input) {
572
+ this.refs.input.select();
573
+ }
574
+ },
575
+
576
+ render() {
577
+ return h('div', null,
578
+ h('input', {
579
+ ref: this.refs('input'),
580
+ type: 'text',
581
+ value: 'Кликни для выделения',
582
+ readOnly: true
583
+ }),
584
+ h('button', { onClick: this.handleClick }, 'Выделить')
585
+ );
586
+ }
587
+ });
588
+
589
+ // Ref на компонент
590
+ const Parent = Component({
591
+ onMounted() {
592
+ this.refs.child.someMethod(); // вызов метода дочернего компонента
593
+ },
594
+
595
+ render() {
596
+ return h(Child, { ref: this.refs('child') });
597
+ }
598
+ });
599
+ ```
600
+
601
+ ## 9. Порталы
602
+
603
+ ```javascript
604
+ const Modal = Component({
605
+ render() {
606
+ if (!this.props.visible) return null;
607
+
608
+ return createPortal(
609
+ h('div', { className: 'modal-overlay' },
610
+ h('div', { className: 'modal-content' },
611
+ h('h2', null, this.props.title),
612
+ h('p', null, this.props.children),
613
+ h('button', { onClick: this.props.onClose }, 'Закрыть')
614
+ )
615
+ ),
616
+ () => document.getElementById('modal-root')
617
+ );
618
+ }
619
+ });
620
+
621
+ const App = Component({
622
+ showModal: false,
623
+
624
+ toggleModal() {
625
+ this.update({ showModal: !this.showModal });
626
+ },
627
+
628
+ render() {
629
+ return h('div', null,
630
+ h('button', { onClick: this.toggleModal }, 'Открыть модал'),
631
+ h(Modal, {
632
+ visible: this.showModal,
633
+ title: 'Привет!',
634
+ onClose: this.toggleModal
635
+ }, 'Содержимое модального окна')
636
+ );
637
+ }
638
+ });
639
+ ```
640
+
641
+ ## 10. Списки с ключами
642
+
643
+ ```javascript
644
+ const TodoList = Component({
645
+ todos: [
646
+ { id: 1, text: 'Изучить tyaff', done: false },
647
+ { id: 2, text: 'Создать проект', done: false },
648
+ { id: 3, text: 'Написать тесты', done: false }
649
+ ],
650
+
651
+ toggleTodo(id) {
652
+ this.update({
653
+ todos: this.todos.map(todo =>
654
+ todo.id === id ? { ...todo, done: !todo.done } : todo
655
+ )
656
+ });
657
+ },
658
+
659
+ render() {
660
+ return h('ul', null,
661
+ this.todos.map(todo =>
662
+ h('li',
663
+ {
664
+ key: todo.id, // пользовательский ключ
665
+ onClick: () => this.toggleTodo(todo.id),
666
+ style: {
667
+ textDecoration: todo.done ? 'line-through' : 'none'
668
+ }
669
+ },
670
+ todo.text
671
+ )
672
+ )
673
+ );
674
+ }
675
+ });
676
+ ```
677
+
678
+ ## 11. Fragment
679
+
680
+ ```javascript
681
+ // Простая группировка без обёртки
682
+ const TableRows = Component({
683
+ render() {
684
+ return h(Fragment, null,
685
+ h('tr', null,
686
+ h('td', null, 'Ячейка 1'),
687
+ h('td', null, 'Ячейка 2')
688
+ ),
689
+ h('tr', null,
690
+ h('td', null, 'Ячейка 3'),
691
+ h('td', null, 'Ячейка 4')
692
+ )
693
+ );
694
+ }
695
+ });
696
+
697
+ // Fragment с key — можно перемещать группу
698
+ const Tabs = Component({
699
+ activeTab: 'a',
700
+
701
+ switchTab(tab) {
702
+ this.update({ activeTab: tab });
703
+ },
704
+
705
+ render() {
706
+ return h('div', null,
707
+ this.activeTab === 'a'
708
+ ? h(Fragment, { key: 'group-a' },
709
+ h(Item, { key: 'i1' }),
710
+ h(Item, { key: 'i2' })
711
+ )
712
+ : h(Fragment, { key: 'group-b' },
713
+ h(Item, { key: 'i3' }),
714
+ h(Item, { key: 'i4' })
715
+ )
716
+ );
717
+ }
718
+ });
719
+ ```
720
+
721
+ ## 12. Условный рендеринг
722
+
723
+ ```javascript
724
+ const ConditionalRender = Component({
725
+ showDetails: false,
726
+
727
+ toggle() {
728
+ this.update({ showDetails: !this.showDetails });
729
+ },
730
+
731
+ render() {
732
+ return h('div', null,
733
+ h('button', { onClick: this.toggle }, 'Показать детали'),
734
+
735
+ this.showDetails
736
+ ? h('div', { className: 'details' },
737
+ h('p', null, 'Это детализированная информация'),
738
+ h('p', null, 'Здесь больше контента')
739
+ )
740
+ : null
741
+ );
742
+ }
743
+ });
744
+ ```
745
+
746
+ ## 13. Контролируемые формы
747
+
748
+ ```javascript
749
+ const Form = Component({
750
+ formData: { name: '', email: '' },
751
+
752
+ handleChange(field, value) {
753
+ this.update({
754
+ formData: { ...this.formData, [field]: value }
755
+ });
756
+ },
757
+
758
+ handleSubmit(e) {
759
+ e.preventDefault();
760
+ console.log('Отправлено:', this.formData);
761
+ },
762
+
763
+ render() {
764
+ return h('form', { onSubmit: this.handleSubmit },
765
+ h('div', null,
766
+ h('label', null, 'Имя:'),
767
+ h('input', {
768
+ type: 'text',
769
+ value: this.formData.name,
770
+ onChange: (e) => this.handleChange('name', e.target.value)
771
+ })
772
+ ),
773
+ h('div', null,
774
+ h('label', null, 'Email:'),
775
+ h('input', {
776
+ type: 'email',
777
+ value: this.formData.email,
778
+ onChange: (e) => this.handleChange('email', e.target.value)
779
+ })
780
+ ),
781
+ h('button', { type: 'submit' }, 'Отправить')
782
+ );
783
+ }
784
+ });
785
+ ```
786
+
787
+ ## 14. SVG компоненты
788
+
789
+ ```javascript
790
+ const Icon = Component({
791
+ render({ color = 'currentColor' }) {
792
+ return h('svg',
793
+ {
794
+ viewBox: '0 0 24 24',
795
+ width: 24,
796
+ height: 24,
797
+ fill: color
798
+ },
799
+ h('path', {
800
+ d: 'M12 2L2 22h20L12 2z'
801
+ })
802
+ );
803
+ }
804
+ });
805
+
806
+ const Button = Component({
807
+ render() {
808
+ return h('button', { className: 'btn' },
809
+ h(Icon, { color: 'blue' }),
810
+ this.props.children
811
+ );
812
+ }
813
+ });
814
+ ```
815
+
816
+ ## 15. Global Store Pattern
817
+
818
+ ```javascript
819
+ // store.js
820
+ export const store = { count: 0, user: null };
821
+
822
+ // App.js
823
+ import { h, Component, mount, refresh } from './core.js';
824
+ import { store } from './store.js';
825
+
826
+ const Counter = Component({
827
+ render() {
828
+ return h('div', null, 'Count: ', store.count);
829
+ }
830
+ });
831
+
832
+ const UserProfile = Component({
833
+ render() {
834
+ return store.user
835
+ ? h('div', null, 'User: ', store.user.name)
836
+ : h('div', null, 'Not logged in');
837
+ }
838
+ });
839
+
840
+ mount(
841
+ h('div', null,
842
+ h(Counter),
843
+ h(UserProfile)
844
+ ),
845
+ document.getElementById('app')
846
+ );
847
+
848
+ // Внешнее обновление
849
+ async function updateCount() {
850
+ store.count = 55;
851
+ await refresh(); // явный trigger, все компоненты перечитают store
852
+ }
853
+ ```
854
+
855
+ ## 16. Анимации через style
856
+
857
+ ```javascript
858
+ const AnimatedBox = Component({
859
+ visible: true,
860
+
861
+ toggle() {
862
+ this.update({ visible: !this.visible });
863
+ },
864
+
865
+ render() {
866
+ return h('div', null,
867
+ h('button', { onClick: this.toggle }, 'Переключить'),
868
+ h('div', {
869
+ style: {
870
+ opacity: this.visible ? 1 : 0,
871
+ transition: 'opacity 0.3s ease',
872
+ width: '100px',
873
+ height: '100px',
874
+ backgroundColor: 'blue'
875
+ }
876
+ })
877
+ );
878
+ }
879
+ });
880
+ ```
881
+
882
+ ## 17. dangerouslySetInnerHTML
883
+
884
+ ```javascript
885
+ const HTMLContent = Component({
886
+ render() {
887
+ const htmlString = '<strong>Жирный</strong> и <em>курсив</em>';
888
+ return h('div', {
889
+ dangerouslySetInnerHTML: { __html: htmlString }
890
+ });
891
+ }
892
+ });
893
+ ```
894
+
895
+ ## 18. Измерение производительности через refresh()
896
+
897
+ ```javascript
898
+ async function loadData() {
899
+ const bigData = await fetch('/api/data').then(r => r.json());
900
+ store.items = processData(bigData);
901
+
902
+ const time = await refresh();
903
+ console.log(`Render time: ${time.toFixed(2)}ms`);
904
+
905
+ if (time > 16) {
906
+ console.warn('Slow render detected');
907
+ }
908
+ }
909
+ ```
910
+
911
+ ## 19. Тестирование с refresh()
912
+
913
+ ```javascript
914
+ test('renders user name after store update', async () => {
915
+ mount(UserProfile, container);
916
+
917
+ store.user = { name: 'Alice' };
918
+ await refresh();
919
+
920
+ expect(container.textContent).toContain('Alice');
921
+ });
922
+ ```
923
+
924
+ ## 20. Update через mount()
925
+
926
+ ```javascript
927
+ const App = Component({
928
+ count: 0,
929
+ increment() { this.update({ count: this.count + 1 }); },
930
+ render() {
931
+ return h('div', null,
932
+ h('h1', null, 'Счётчик: ' + this.count),
933
+ h('button', { onClick: this.increment }, '+')
934
+ );
935
+ }
936
+ });
937
+
938
+ // Первоначальный mount
939
+ mount(App, document.getElementById('app'));
940
+
941
+ // При повторном вызове mount() с тем же контейнером
942
+ // выполняется diff и обновление существующего дерева
943
+ mount(h(App, { initialCount: 10 }), document.getElementById('app'));
944
+
945
+ // Unmount
946
+ mount(null, document.getElementById('app'));
947
+ ```
948
+
949
+
950
+ ## Полноценный пример: Todo приложение
951
+
952
+ ```javascript
953
+ const TodoApp = Component({
954
+ todos: [],
955
+ inputValue: '',
956
+
957
+ addTodo() {
958
+ if (!this.inputValue.trim()) return;
959
+
960
+ this.update({
961
+ todos: [...this.todos, {
962
+ id: Date.now(),
963
+ text: this.inputValue,
964
+ completed: false
965
+ }],
966
+ inputValue: ''
967
+ });
968
+ },
969
+
970
+ toggleTodo(id) {
971
+ this.update({
972
+ todos: this.todos.map(todo =>
973
+ todo.id === id
974
+ ? { ...todo, completed: !todo.completed }
975
+ : todo
976
+ )
977
+ });
978
+ },
979
+
980
+ deleteTodo(id) {
981
+ this.update({
982
+ todos: this.todos.filter(todo => todo.id !== id)
983
+ });
984
+ },
985
+
986
+ render() {
987
+ return h('div', { className: 'todo-app' },
988
+ h('h1', null, 'Todo список'),
989
+
990
+ h('div', { className: 'todo-input' },
991
+ h('input', {
992
+ type: 'text',
993
+ value: this.inputValue,
994
+ onChange: (e) => this.update({ inputValue: e.target.value }),
995
+ onKeyPress: (e) => e.key === 'Enter' && this.addTodo()
996
+ }),
997
+ h('button', { onClick: this.addTodo }, 'Добавить')
998
+ ),
999
+
1000
+ h('ul', { className: 'todo-list' },
1001
+ this.todos.map(todo =>
1002
+ h('li', {
1003
+ key: todo.id,
1004
+ className: todo.completed ? 'completed' : ''
1005
+ },
1006
+ h('input', {
1007
+ type: 'checkbox',
1008
+ checked: todo.completed,
1009
+ onChange: () => this.toggleTodo(todo.id)
1010
+ }),
1011
+ h('span', null, todo.text),
1012
+ h('button',
1013
+ { onClick: () => this.deleteTodo(todo.id) },
1014
+ 'Удалить'
1015
+ )
1016
+ )
1017
+ )
1018
+ ),
1019
+
1020
+ h('div', { className: 'todo-stats' },
1021
+ 'Всего: ' + this.todos.length + ', ',
1022
+ 'Выполнено: ' + this.todos.filter(t => t.completed).length
1023
+ )
1024
+ );
1025
+ }
1026
+ });
1027
+
1028
+ mount(TodoApp, document.getElementById('app'));
1029
+ ```
1030
+
1031
+ Эти примеры демонстрируют основные возможности библиотеки tyaff и могут быть использованы как основа для ваших проектов.
1032
+