@granularjs/core 1.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.
Files changed (70) hide show
  1. package/README.md +576 -0
  2. package/dist/granular.min.js +2 -0
  3. package/dist/granular.min.js.map +7 -0
  4. package/package.json +54 -0
  5. package/src/core/bootstrap.js +63 -0
  6. package/src/core/collections/observable-array.js +204 -0
  7. package/src/core/component/function-component.js +82 -0
  8. package/src/core/context.js +172 -0
  9. package/src/core/dom/dom.js +25 -0
  10. package/src/core/dom/element.js +725 -0
  11. package/src/core/dom/error-boundary.js +111 -0
  12. package/src/core/dom/input-format.js +82 -0
  13. package/src/core/dom/list.js +185 -0
  14. package/src/core/dom/portal.js +57 -0
  15. package/src/core/dom/tags.js +182 -0
  16. package/src/core/dom/virtual-list.js +242 -0
  17. package/src/core/dom/when.js +138 -0
  18. package/src/core/events/event-hub.js +97 -0
  19. package/src/core/forms/form.js +127 -0
  20. package/src/core/internal/symbols.js +5 -0
  21. package/src/core/network/websocket.js +165 -0
  22. package/src/core/query/query-client.js +529 -0
  23. package/src/core/reactivity/after-flush.js +20 -0
  24. package/src/core/reactivity/computed.js +51 -0
  25. package/src/core/reactivity/concat.js +89 -0
  26. package/src/core/reactivity/dirty-host.js +162 -0
  27. package/src/core/reactivity/observe.js +421 -0
  28. package/src/core/reactivity/persist.js +180 -0
  29. package/src/core/reactivity/resolve.js +8 -0
  30. package/src/core/reactivity/signal.js +97 -0
  31. package/src/core/reactivity/state.js +294 -0
  32. package/src/core/renderable/render-string.js +51 -0
  33. package/src/core/renderable/renderable.js +21 -0
  34. package/src/core/renderable/renderer.js +66 -0
  35. package/src/core/router/router.js +865 -0
  36. package/src/core/runtime.js +28 -0
  37. package/src/index.js +42 -0
  38. package/types/core/bootstrap.d.ts +11 -0
  39. package/types/core/collections/observable-array.d.ts +25 -0
  40. package/types/core/component/function-component.d.ts +14 -0
  41. package/types/core/context.d.ts +29 -0
  42. package/types/core/dom/dom.d.ts +13 -0
  43. package/types/core/dom/element.d.ts +10 -0
  44. package/types/core/dom/error-boundary.d.ts +8 -0
  45. package/types/core/dom/input-format.d.ts +6 -0
  46. package/types/core/dom/list.d.ts +8 -0
  47. package/types/core/dom/portal.d.ts +8 -0
  48. package/types/core/dom/tags.d.ts +114 -0
  49. package/types/core/dom/virtual-list.d.ts +8 -0
  50. package/types/core/dom/when.d.ts +13 -0
  51. package/types/core/events/event-hub.d.ts +48 -0
  52. package/types/core/forms/form.d.ts +9 -0
  53. package/types/core/internal/symbols.d.ts +4 -0
  54. package/types/core/network/websocket.d.ts +18 -0
  55. package/types/core/query/query-client.d.ts +73 -0
  56. package/types/core/reactivity/after-flush.d.ts +4 -0
  57. package/types/core/reactivity/computed.d.ts +1 -0
  58. package/types/core/reactivity/concat.d.ts +1 -0
  59. package/types/core/reactivity/dirty-host.d.ts +42 -0
  60. package/types/core/reactivity/observe.d.ts +10 -0
  61. package/types/core/reactivity/persist.d.ts +1 -0
  62. package/types/core/reactivity/resolve.d.ts +1 -0
  63. package/types/core/reactivity/signal.d.ts +11 -0
  64. package/types/core/reactivity/state.d.ts +14 -0
  65. package/types/core/renderable/render-string.d.ts +2 -0
  66. package/types/core/renderable/renderable.d.ts +15 -0
  67. package/types/core/renderable/renderer.d.ts +38 -0
  68. package/types/core/router/router.d.ts +57 -0
  69. package/types/core/runtime.d.ts +26 -0
  70. package/types/index.d.ts +2 -0
@@ -0,0 +1,725 @@
1
+ import { Renderable } from '../renderable/renderable.js';
2
+ import { Renderer } from '../renderable/renderer.js';
3
+ import { isObservableArray } from '../collections/observable-array.js';
4
+ import { createComment, clearBetween } from './dom.js';
5
+ import { normalizeInputFormat, applyInputFormat } from './input-format.js';
6
+ import { isWhen, readWhenValue, subscribeWhenValue } from './when.js';
7
+ import { isSignal, readSignal, subscribeSignal, getMappedArrayMeta } from '../reactivity/signal.js';
8
+ import { isState, isStatePath, isComputed, readState, subscribeState, getMappedMeta, readStateMeta, subscribeStateMeta } from '../reactivity/state.js';
9
+ import { list } from './list.js';
10
+
11
+ const voidElements = new Set([
12
+ 'area',
13
+ 'base',
14
+ 'br',
15
+ 'col',
16
+ 'embed',
17
+ 'hr',
18
+ 'img',
19
+ 'input',
20
+ 'link',
21
+ 'meta',
22
+ 'param',
23
+ 'source',
24
+ 'track',
25
+ 'wbr',
26
+ ]);
27
+
28
+ export class ElementNode extends Renderable {
29
+ tagName;
30
+ props;
31
+ children;
32
+ #el = null;
33
+ #unsubs = [];
34
+ #styleUnsubs = [];
35
+ #mounted = false;
36
+
37
+ constructor(tagName, props = {}, children = []) {
38
+ super();
39
+ this.tagName = tagName;
40
+ this.props = props || {};
41
+ this.children = Array.isArray(children) ? children : [children];
42
+ if (voidElements.has(tagName.toLowerCase())) this.children = [];
43
+ }
44
+
45
+ mountInto(parent, beforeNode) {
46
+ if (this.#mounted) return;
47
+ this.#mounted = true;
48
+ const el = document.createElement(this.tagName);
49
+ this.#el = el;
50
+ this.#applyProps(el);
51
+ this.#appendChildren(el);
52
+ parent.insertBefore(el, beforeNode);
53
+ }
54
+
55
+ unmount() {
56
+ if (!this.#mounted) return;
57
+ this.#mounted = false;
58
+ for (const unsub of this.#unsubs) unsub();
59
+ this.#unsubs = [];
60
+ this.#cleanupChildren();
61
+ this.#el?.remove();
62
+ this.#el = null;
63
+ }
64
+
65
+ renderToString(render) {
66
+ const tag = this.tagName;
67
+ const props = this.props || {};
68
+ const lower = tag.toLowerCase();
69
+ const attrParts = [];
70
+ let innerHtml = null;
71
+ let textContent = null;
72
+
73
+ for (const [key, rawValue] of Object.entries(props)) {
74
+ if (key === 'node') continue;
75
+ if (key === 'children' || key === 'content') continue;
76
+ if (key === 'format') continue;
77
+ if (key.startsWith('on') && typeof rawValue === 'function') continue;
78
+ let value = rawValue;
79
+ if (isWhen(value)) value = readWhenValue(value);
80
+ if (isSignal(value)) value = readSignal(value);
81
+ if (isState(value) || isStatePath(value)) value = readState(value);
82
+
83
+ if (key === 'style') {
84
+ if (value && typeof value === 'object') {
85
+ const styles = [];
86
+ for (const [k, v] of Object.entries(value)) {
87
+ if (v == null || v === false) continue;
88
+ const name = k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
89
+ styles.push(`${name}:${v}`);
90
+ }
91
+ if (styles.length) attrParts.push(`style="${styles.join(';')}"`);
92
+ } else if (typeof value === 'string') {
93
+ attrParts.push(`style="${render.escape(value)}"`);
94
+ }
95
+ continue;
96
+ }
97
+ if (key === 'className' || key === 'class') {
98
+ if (value != null && value !== false) attrParts.push(`class="${render.escape(String(value))}"`);
99
+ continue;
100
+ }
101
+ if (key === 'htmlFor') {
102
+ if (value != null && value !== false) attrParts.push(`for="${render.escape(String(value))}"`);
103
+ continue;
104
+ }
105
+ if (key === 'value' && lower === 'input' && props.format != null) {
106
+ const resolvedFormat = isSignal(props.format)
107
+ ? readSignal(props.format)
108
+ : isState(props.format) || isStatePath(props.format)
109
+ ? readState(props.format)
110
+ : props.format;
111
+ const formatConfig = normalizeInputFormat(resolvedFormat);
112
+ const formatMode = formatConfig?.mode ?? 'both';
113
+ const formatted = applyInputFormat(value ?? '', formatConfig);
114
+ value =
115
+ formatMode === 'value-only'
116
+ ? (formatted.raw ?? formatted.value ?? '')
117
+ : (formatted.visual ?? formatted.value ?? '');
118
+ }
119
+ if (key === 'textContent') {
120
+ textContent = value == null ? '' : String(value);
121
+ continue;
122
+ }
123
+ if (key === 'innerHTML') {
124
+ innerHtml = value == null ? '' : String(value);
125
+ continue;
126
+ }
127
+ if (value === true) {
128
+ attrParts.push(`${key}`);
129
+ continue;
130
+ }
131
+ if (value === false || value == null) {
132
+ continue;
133
+ }
134
+ attrParts.push(`${key}="${render.escape(String(value))}"`);
135
+ }
136
+
137
+ const attrs = attrParts.length ? ` ${attrParts.join(' ')}` : '';
138
+ if (voidElements.has(lower)) {
139
+ return `<${tag}${attrs}>`;
140
+ }
141
+ if (innerHtml != null) {
142
+ return `<${tag}${attrs}>${innerHtml}</${tag}>`;
143
+ }
144
+ if (textContent != null) {
145
+ return `<${tag}${attrs}>${render.escape(textContent)}</${tag}>`;
146
+ }
147
+ const children = Array.isArray(this.children) ? this.children : [this.children];
148
+ const html = children.map((child) => render(child)).join('');
149
+ return `<${tag}${attrs}>${html}</${tag}>`;
150
+ }
151
+
152
+ #cleanupChildren() {
153
+ if (!this.#el) return;
154
+ for (const child of Array.from(this.#el.childNodes)) child.remove();
155
+ }
156
+
157
+ #applyProps(el) {
158
+ const props = this.props || {};
159
+ const tagName = this.tagName.toLowerCase();
160
+ let formatBound = false;
161
+ let valueBound = false;
162
+
163
+ const { formatConfig } = this.#getFormatConfig();
164
+
165
+ for (const [key, rawValue] of Object.entries(props)) {
166
+ if (key === 'value') valueBound = true;
167
+
168
+ if (key === 'node') continue;
169
+ if (key === 'children' || key === 'content') continue;
170
+ if (key === 'format') continue;
171
+ if (key === 'style') {
172
+ this.#applyStyle(el, rawValue);
173
+ continue;
174
+ }
175
+ const props = { el, key, rawValue, formatConfig };
176
+ if (isWhen(rawValue)) {
177
+ this.#applyPropAsWhen(props);
178
+ continue;
179
+ }
180
+ if (isSignal(rawValue)) {
181
+ if (key === 'value' && formatConfig) formatBound = true;
182
+ this.#applyPropAsSignal(props);
183
+ continue;
184
+ }
185
+ if (isState(rawValue) || isStatePath(rawValue)) {
186
+ if (key === 'value' && formatConfig) formatBound = true;
187
+ this.#applyPropAsState(props)
188
+ continue;
189
+ }
190
+ if (key === 'value' && formatConfig) {
191
+ const { visualValue } = this.#formatValue(rawValue);
192
+ this.#setProp(el, key, visualValue);
193
+ formatBound = true;
194
+ continue;
195
+ }
196
+ if ((key === 'onInput' || key === 'onChange') && typeof rawValue === 'function' && formatConfig) {
197
+ const handler = (ev) => {
198
+ rawValue?.(ev, ev?.target?.rawValue);
199
+ };
200
+ this.#setProp(el, key, handler);
201
+ continue;
202
+ }
203
+ if (key === 'onInput' && !formatBound) {
204
+ const onInput = (ev) => {
205
+ if (formatConfig) {
206
+ this.#applyPropsBaseOnInputFormatted(ev);
207
+ }
208
+ rawValue?.(ev);
209
+ };
210
+ this.#setProp(el, key, onInput);
211
+ continue;
212
+ }
213
+ this.#setProp(el, key, rawValue);
214
+ }
215
+
216
+ if (!valueBound && formatConfig) {
217
+ const onInput = (ev) => {
218
+ const { visualValue } = this.#applyPropsBaseOnInputFormatted({ target: el });
219
+ this.#setProp(el, 'value', visualValue);
220
+ }
221
+ onInput()
222
+ this.#applyPropsAddInputEventListeners(el, onInput, true);
223
+ formatBound = true;
224
+ }
225
+
226
+ if (props.node && (isState(props.node) || isStatePath(props.node))) {
227
+ props.node.set(this.#el);
228
+ }
229
+
230
+ if (formatConfig && !formatBound) {
231
+ const onInput = (ev) => {
232
+ this.#applyPropsBaseOnInputFormatted(ev);
233
+ };
234
+ this.#applyPropsAddInputEventListeners(el, onInput, true);
235
+ }
236
+ }
237
+ #getFormatConfig() {
238
+ const props = this.props || {};
239
+ const tagName = this.tagName.toLowerCase();
240
+ const resolveFormat = (value) => {
241
+ if (isSignal(value)) return readSignal(value);
242
+ if (isState(value) || isStatePath(value)) return readState(value);
243
+ return value;
244
+ };
245
+ const formatConfig = tagName === 'input' ? normalizeInputFormat(resolveFormat(props.format)) : null;
246
+ const formatMode = formatConfig?.mode ?? 'both';
247
+ return { formatConfig, formatMode };
248
+ }
249
+ #formatValue(next) {
250
+ const { formatConfig, formatMode } = this.#getFormatConfig();
251
+ const formatted = applyInputFormat(next ?? '', formatConfig);
252
+ const visualValue =
253
+ formatMode === 'value-only'
254
+ ? (formatted.raw ?? formatted.value ?? '')
255
+ : (formatted.visual ?? formatted.value ?? '');
256
+ const stateValue =
257
+ formatMode === 'visual-only'
258
+ ? (formatted.raw ?? formatted.value ?? '')
259
+ : (formatted.value ?? formatted.visual ?? '');
260
+ return { formatted, visualValue, stateValue };
261
+ };
262
+
263
+ #applyPropsBaseOnInputFormatted(ev) {
264
+ const { formatted, visualValue, stateValue } = this.#formatValue(ev.target.value ?? '');
265
+ const rawValue = formatted?.raw ?? stateValue;
266
+ ev.target.value = visualValue;
267
+ ev.target.rawValue = rawValue;
268
+ return { visualValue, stateValue, rawValue };
269
+ }
270
+ #applyPropsAddInputEventListeners(el, onInput, capture) {
271
+ el.addEventListener('input', onInput, capture);
272
+ el.addEventListener('change', onInput, capture);
273
+ this.#unsubs.push(() => {
274
+ el.removeEventListener('input', onInput, capture);
275
+ el.removeEventListener('change', onInput, capture);
276
+ });
277
+ }
278
+
279
+ #applyPropsSubscribeUpdate({ key, el, rawValue, read, subscribe, formatConfig }) {
280
+ const update = () => {
281
+ const nextValue = read(rawValue);
282
+ if (key === 'value' && formatConfig) {
283
+ const { visualValue } = this.#formatValue(nextValue);
284
+ this.#setProp(el, key, visualValue);
285
+ return;
286
+ }
287
+ this.#setProp(el, key, nextValue);
288
+ };
289
+ update();
290
+ const unsub = subscribe(rawValue, update);
291
+ if (unsub) this.#unsubs.push(unsub);
292
+ return update;
293
+ }
294
+
295
+ #applyPropAsWhen(props) {
296
+ this.#applyPropsSubscribeUpdate({ ...props, read: readWhenValue, subscribe: subscribeWhenValue });
297
+ }
298
+ #applyPropAsSignal({ el, key, rawValue, formatConfig }) {
299
+ const update = this.#applyPropsSubscribeUpdate({ key, el, rawValue, formatConfig, read: readSignal, subscribe: subscribeSignal });
300
+ if (key === 'value') {
301
+ if (formatConfig) {
302
+ const onInput = (ev) => {
303
+ const { stateValue } = this.#applyPropsBaseOnInputFormatted(ev);
304
+ if (isComputed(rawValue)) return;
305
+ const ok = rawValue.set?.(stateValue);
306
+ if (ok === false) update();
307
+ };
308
+ this.#applyPropsAddInputEventListeners(el, onInput, true);
309
+ } else {
310
+ const onInput = (ev) => {
311
+ if (isComputed(rawValue)) return;
312
+ const ok = rawValue.set?.(ev.target?.value ?? '');
313
+ if (ok === false) update();
314
+ };
315
+ this.#applyPropsAddInputEventListeners(el, onInput);
316
+ }
317
+ }
318
+ if (key === 'checked') {
319
+ const onChange = (ev) => {
320
+ if (isComputed(rawValue)) return;
321
+ const ok = rawValue.set?.(!!ev.target?.checked);
322
+ if (ok === false) update();
323
+ };
324
+ el.addEventListener('change', onChange);
325
+ this.#unsubs.push(() => el.removeEventListener('change', onChange));
326
+ }
327
+ }
328
+ #applyPropAsState({ el, key, rawValue, formatConfig }) {
329
+ const update = this.#applyPropsSubscribeUpdate({ key, el, rawValue, formatConfig, read: readState, subscribe: subscribeState });
330
+ if (key === 'value') {
331
+ if (formatConfig) {
332
+ const onInput = (ev) => {
333
+ const { stateValue } = this.#applyPropsBaseOnInputFormatted(ev);
334
+ if (isComputed(rawValue)) return;
335
+ const ok = rawValue.set?.(stateValue);
336
+ if (ok === false) update();
337
+ };
338
+ this.#applyPropsAddInputEventListeners(el, onInput, true);
339
+ } else {
340
+ const onInput = (ev) => {
341
+ if (isComputed(rawValue)) return;
342
+ const ok = rawValue.set?.(ev.target?.value ?? '');
343
+ if (ok === false) update();
344
+ };
345
+ this.#applyPropsAddInputEventListeners(el, onInput);
346
+ }
347
+ }
348
+ if (key === 'checked') {
349
+ const onChange = (ev) => {
350
+ if (isComputed(rawValue)) return;
351
+ const ok = rawValue.set?.(!!ev.target?.checked);
352
+ if (ok === false) update();
353
+ };
354
+ el.addEventListener('change', onChange);
355
+ this.#unsubs.push(() => el.removeEventListener('change', onChange));
356
+ }
357
+ }
358
+
359
+ #setProp(el, key, value) {
360
+ if (isWhen(value)) value = readWhenValue(value);
361
+ if (isSignal(value)) value = readSignal(value);
362
+ if (isState(value) || isStatePath(value)) value = readState(value);
363
+ if (key === 'style') {
364
+ if (value && typeof value === 'object') {
365
+ Object.assign(el.style, value);
366
+ return;
367
+ }
368
+ if (typeof value === 'string') {
369
+ el.style.cssText = value;
370
+ return;
371
+ }
372
+ }
373
+ if (key.startsWith('on') && typeof value === 'function') {
374
+ const eventName = key.substring(2).toLowerCase();
375
+ el.addEventListener(eventName, value);
376
+ this.#unsubs.push(() => el.removeEventListener(eventName, value));
377
+ return;
378
+ }
379
+ if (key === 'className' || key === 'class') {
380
+ el.className = value ?? '';
381
+ return;
382
+ }
383
+ if (key === 'htmlFor') {
384
+ el.setAttribute('for', value ?? '');
385
+ return;
386
+ }
387
+ if (key === 'value') {
388
+ try {
389
+ el.value = value ?? '';
390
+ } catch { }
391
+ return;
392
+ }
393
+ if (key === 'checked') {
394
+ try {
395
+ el.checked = !!value;
396
+ } catch { }
397
+ return;
398
+ }
399
+ if (key === 'contentEditable') {
400
+ try {
401
+ el.contentEditable = value ? 'true' : 'false';
402
+ } catch { }
403
+ return;
404
+ }
405
+ if (key === 'textContent') {
406
+ el.textContent = value ?? '';
407
+ return;
408
+ }
409
+ if (key === 'innerHTML') {
410
+ el.innerHTML = value ?? '';
411
+ return;
412
+ }
413
+ if (value === false || value == null) {
414
+ el.removeAttribute(key);
415
+ if (key in el) {
416
+ try {
417
+ el[key] = false;
418
+ } catch { }
419
+ }
420
+ return;
421
+ }
422
+ if (value === true) {
423
+ el.setAttribute(key, '');
424
+ if (key in el) {
425
+ try {
426
+ el[key] = true;
427
+ } catch { }
428
+ }
429
+ return;
430
+ }
431
+ el.setAttribute(key, value);
432
+ if (key in el) {
433
+ try {
434
+ el[key] = value;
435
+ } catch { }
436
+ }
437
+ }
438
+
439
+ #applyStyle(el, styleValue) {
440
+ const cleanupStyleSubs = () => {
441
+ for (const unsub of this.#styleUnsubs) unsub();
442
+ this.#styleUnsubs = [];
443
+ };
444
+
445
+ const applyValue = (value) => {
446
+ if (typeof value === 'string') {
447
+ cleanupStyleSubs();
448
+ el.style.cssText = value;
449
+ return;
450
+ }
451
+ if (value && typeof value === 'object') {
452
+ cleanupStyleSubs();
453
+ applyObject(value);
454
+ }
455
+ };
456
+
457
+ const applyObject = (styleObj) => {
458
+ if (!styleObj || typeof styleObj !== 'object') return;
459
+ for (const [k, v] of Object.entries(styleObj)) {
460
+ if (typeof v === 'function') {
461
+ try {
462
+ el.style[k] = v();
463
+ } catch {
464
+ el.style[k] = '';
465
+ }
466
+ continue;
467
+ }
468
+ if (isSignal(v)) {
469
+ const update = () => {
470
+ try {
471
+ el.style[k] = readSignal(v) ?? '';
472
+ } catch {
473
+ el.style[k] = '';
474
+ }
475
+ };
476
+ update();
477
+ const unsub = subscribeSignal(v, update);
478
+ if (unsub) this.#styleUnsubs.push(unsub);
479
+ continue;
480
+ }
481
+ if (isState(v) || isStatePath(v)) {
482
+ const update = () => {
483
+ try {
484
+ el.style[k] = readState(v) ?? '';
485
+ } catch {
486
+ el.style[k] = '';
487
+ }
488
+ };
489
+ update();
490
+ const unsub = subscribeState(v, update);
491
+ if (unsub) this.#styleUnsubs.push(unsub);
492
+ continue;
493
+ } else {
494
+ el.style[k] = v ?? '';
495
+ }
496
+ }
497
+ };
498
+
499
+ cleanupStyleSubs();
500
+ if (isSignal(styleValue)) {
501
+ const update = () => applyValue(readSignal(styleValue));
502
+ update();
503
+ const unsub = subscribeSignal(styleValue, update);
504
+ if (unsub) this.#unsubs.push(unsub);
505
+ return;
506
+ }
507
+
508
+ if (isState(styleValue) || isStatePath(styleValue)) {
509
+ const update = () => applyValue(readState(styleValue));
510
+ update();
511
+ const unsub = subscribeState(styleValue, update);
512
+ if (unsub) this.#unsubs.push(unsub);
513
+ return;
514
+ }
515
+
516
+
517
+ if (typeof styleValue === 'function') {
518
+ try {
519
+ applyValue(styleValue());
520
+ } catch {
521
+ return;
522
+ }
523
+ return;
524
+ }
525
+
526
+ applyValue(styleValue);
527
+ }
528
+
529
+ #appendChildren(el) {
530
+ const content = Object.prototype.hasOwnProperty.call(this.props, 'content')
531
+ ? this.props.content
532
+ : null;
533
+ const children = this.children.length ? this.children : content != null ? [content] : [];
534
+ for (const child of children) this.#mountChild(el, child, null);
535
+ }
536
+
537
+ #mountChild(parent, child, beforeNode) {
538
+ if (child == null || child === false) return;
539
+ const mapped = getMappedArrayMeta(child) || getMappedMeta(child);
540
+ if (mapped) {
541
+ const start = createComment('zb:bind:start', 'map');
542
+ const end = createComment('zb:bind:end', 'map');
543
+ parent.insertBefore(start, beforeNode);
544
+ parent.insertBefore(end, beforeNode);
545
+ const state = { kind: 'static', values: [] };
546
+ const update = () => {
547
+ const src = mapped.path ? readStateMeta(mapped) : readSignal(mapped.signal);
548
+ const list = Array.isArray(src) ? src.map(mapped.mapFn) : [];
549
+ this.#renderDynamic(list, start, end, state);
550
+ };
551
+ update();
552
+ const unsub = mapped.path ? subscribeStateMeta(mapped, update) : subscribeSignal(mapped.signal, update);
553
+ if (unsub) this.#unsubs.push(() => {
554
+ unsub();
555
+ this.#cleanupDynamic(state, start, end);
556
+ start.remove();
557
+ end.remove();
558
+ });
559
+ return;
560
+ }
561
+ if (isSignal(child)) {
562
+ const start = createComment('zb:bind:start', 'signal');
563
+ const end = createComment('zb:bind:end', 'signal');
564
+ parent.insertBefore(start, beforeNode);
565
+ parent.insertBefore(end, beforeNode);
566
+ const state = { kind: 'static', values: [] };
567
+ const update = () => this.#renderDynamic(readSignal(child), start, end, state);
568
+ update();
569
+ const unsub = subscribeSignal(child, update);
570
+ if (unsub) this.#unsubs.push(() => {
571
+ unsub();
572
+ this.#cleanupDynamic(state, start, end);
573
+ start.remove();
574
+ end.remove();
575
+ });
576
+ return;
577
+ }
578
+
579
+ if (isState(child) || isStatePath(child)) {
580
+ const start = createComment('zb:bind:start', 'state');
581
+ const end = createComment('zb:bind:end', 'state');
582
+ parent.insertBefore(start, beforeNode);
583
+ parent.insertBefore(end, beforeNode);
584
+ const state = { kind: 'static', values: [] };
585
+ const update = () => this.#renderDynamic(readState(child), start, end, state);
586
+ update();
587
+ const unsub = subscribeState(child, update);
588
+ if (unsub) this.#unsubs.push(() => {
589
+ unsub();
590
+ this.#cleanupDynamic(state, start, end);
591
+ start.remove();
592
+ end.remove();
593
+ });
594
+ return;
595
+ }
596
+
597
+ if (isObservableArray(child)) {
598
+ const start = createComment('zb:bind:start', 'list');
599
+ const end = createComment('zb:bind:end', 'list');
600
+ parent.insertBefore(start, beforeNode);
601
+ parent.insertBefore(end, beforeNode);
602
+ const state = { kind: 'list', items: [], unsub: null, source: child };
603
+ this.#renderDynamic(child, start, end, state);
604
+ this.#unsubs.push(() => {
605
+ this.#cleanupDynamic(state, start, end);
606
+ start.remove();
607
+ end.remove();
608
+ });
609
+ return;
610
+ }
611
+ if (Array.isArray(child)) {
612
+ for (const item of child) this.#mountChild(parent, item, beforeNode);
613
+ return;
614
+ }
615
+ if (Renderer.isRenderable(child)) {
616
+ child.mountInto(parent, beforeNode);
617
+ this.#unsubs.push(() => child.unmount());
618
+ return;
619
+ }
620
+ if (Renderer.isDomNode(child)) {
621
+ parent.insertBefore(child, beforeNode);
622
+ return;
623
+ }
624
+ parent.insertBefore(document.createTextNode(Renderer.toText(child)), beforeNode);
625
+ }
626
+
627
+ #cleanupDynamic(state, start, end) {
628
+ if (state.kind === 'static') {
629
+ for (const r of state.values) Renderer.unmount(r);
630
+ state.values = [];
631
+ if (start && end) clearBetween(start, end);
632
+ return;
633
+ }
634
+ if (state.kind === 'list') {
635
+ state.unsub?.();
636
+ for (const it of state.items) {
637
+ for (const r of it.values) Renderer.unmount(r);
638
+ clearBetween(it.start, it.end);
639
+ it.start.remove();
640
+ it.end.remove();
641
+ }
642
+ state.items = [];
643
+ }
644
+ }
645
+
646
+ #renderDynamic(value, start, end, state) {
647
+ if (isObservableArray(value)) {
648
+ if (state.kind === 'list' && state.source === value) return;
649
+ this.#cleanupDynamic(state, start, end);
650
+ state.kind = 'list';
651
+ state.source = value;
652
+ const parent = end.parentNode;
653
+ const items = [];
654
+ const makeItemMount = (idx, rawItem) => {
655
+ const refNode = idx < items.length ? items[idx].start : end;
656
+ const itemStart = createComment('zb:item:start', 'item');
657
+ const itemEnd = createComment('zb:item:end', 'item');
658
+ parent.insertBefore(itemStart, refNode);
659
+ parent.insertBefore(itemEnd, refNode);
660
+ const values = Renderer.normalize(rawItem);
661
+ for (const r of values) this.#mountRenderable(parent, r, itemEnd);
662
+ items.splice(idx, 0, { start: itemStart, end: itemEnd, values });
663
+ };
664
+ const removeItemMount = (idx, count) => {
665
+ const removed = items.splice(idx, count);
666
+ for (const it of removed) {
667
+ for (const r of it.values) Renderer.unmount(r);
668
+ clearBetween(it.start, it.end);
669
+ it.start.remove();
670
+ it.end.remove();
671
+ }
672
+ };
673
+ const setItemMount = (idx, rawItem) => {
674
+ removeItemMount(idx, 1);
675
+ makeItemMount(idx, rawItem);
676
+ };
677
+ for (let i = 0; i < value.length; i++) makeItemMount(i, value[i]);
678
+ const unsub = value.subscribe((patch) => {
679
+ if (!this.#mounted) return;
680
+ if (patch.type === 'reset') {
681
+ removeItemMount(0, items.length);
682
+ for (let i = 0; i < patch.items.length; i++) makeItemMount(i, patch.items[i]);
683
+ return;
684
+ }
685
+ if (patch.type === 'insert') {
686
+ for (let i = 0; i < patch.items.length; i++) makeItemMount(patch.index + i, patch.items[i]);
687
+ return;
688
+ }
689
+ if (patch.type === 'remove') {
690
+ removeItemMount(patch.index, patch.count);
691
+ return;
692
+ }
693
+ if (patch.type === 'set') setItemMount(patch.index, patch.value);
694
+ });
695
+ state.items = items;
696
+ state.unsub = unsub;
697
+ return;
698
+ }
699
+
700
+ if (Array.isArray(value)) {
701
+ this.#cleanupDynamic(state, start, end);
702
+ state.kind = 'static';
703
+ const next = Renderer.normalize(value);
704
+ state.values = next;
705
+ for (const r of next) this.#mountRenderable(end.parentNode, r, end);
706
+ return;
707
+ }
708
+
709
+ this.#cleanupDynamic(state, start, end);
710
+ state.kind = 'static';
711
+ const next = Renderer.normalize(value);
712
+ state.values = next;
713
+ for (const r of next) this.#mountRenderable(end.parentNode, r, end);
714
+ }
715
+
716
+ #mountRenderable(parent, renderable, beforeNode) {
717
+ if (Renderer.isRenderable(renderable)) {
718
+ renderable.mountInto(parent, beforeNode);
719
+ return;
720
+ }
721
+ if (Renderer.isDomNode(renderable)) {
722
+ parent.insertBefore(renderable, beforeNode);
723
+ }
724
+ }
725
+ }