@esportsplus/template 0.16.14 → 0.17.1

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 (55) hide show
  1. package/README.md +33 -1
  2. package/bench/runtime.bench.ts +207 -0
  3. package/build/attributes.js +4 -1
  4. package/build/compiler/plugins/vite.d.ts +8 -4
  5. package/build/compiler/plugins/vite.js +37 -2
  6. package/build/hmr.d.ts +10 -0
  7. package/build/hmr.js +42 -0
  8. package/build/index.d.ts +1 -0
  9. package/build/index.js +1 -0
  10. package/build/slot/array.js +69 -4
  11. package/build/slot/effect.d.ts +3 -3
  12. package/build/slot/effect.js +36 -17
  13. package/build/slot/render.js +1 -2
  14. package/build/utilities.d.ts +2 -1
  15. package/build/utilities.js +2 -1
  16. package/llm.txt +63 -4
  17. package/package.json +2 -1
  18. package/src/attributes.ts +4 -1
  19. package/src/compiler/plugins/vite.ts +74 -5
  20. package/src/hmr.ts +70 -0
  21. package/src/index.ts +1 -0
  22. package/src/slot/array.ts +104 -4
  23. package/src/slot/effect.ts +46 -20
  24. package/src/slot/render.ts +1 -4
  25. package/src/utilities.ts +3 -1
  26. package/{test/attributes.test.ts → tests/attributes.ts} +3 -2
  27. package/tests/compiler/codegen.ts +292 -0
  28. package/tests/compiler/integration.ts +252 -0
  29. package/tests/compiler/ts-parser.ts +160 -0
  30. package/tests/compiler/vite-hmr.ts +126 -0
  31. package/{test/constants.test.ts → tests/constants.ts} +5 -1
  32. package/tests/event/onconnect.ts +147 -0
  33. package/tests/event/onresize.ts +187 -0
  34. package/tests/event/ontick.ts +273 -0
  35. package/tests/hmr.ts +146 -0
  36. package/{test/slot/array.test.ts → tests/slot/array.ts} +475 -0
  37. package/tests/slot/async.ts +389 -0
  38. package/vitest.bench.config.ts +18 -0
  39. package/vitest.config.ts +1 -1
  40. package/storage/compiler-architecture-2026-01-13.md +0 -420
  41. /package/{test → examples}/index.ts +0 -0
  42. /package/{test → examples}/vite.config.ts +0 -0
  43. /package/{test/compiler/parser.test.ts → tests/compiler/parser.ts} +0 -0
  44. /package/{test/compiler/ts-analyzer.test.ts → tests/compiler/ts-analyzer.ts} +0 -0
  45. /package/{test → tests}/dist/test.js +0 -0
  46. /package/{test → tests}/dist/test.js.map +0 -0
  47. /package/{test/event/index.test.ts → tests/event/index.ts} +0 -0
  48. /package/{test/html.test.ts → tests/html.ts} +0 -0
  49. /package/{test/render.test.ts → tests/render.ts} +0 -0
  50. /package/{test/slot/cleanup.test.ts → tests/slot/cleanup.ts} +0 -0
  51. /package/{test/slot/effect.test.ts → tests/slot/effect.ts} +0 -0
  52. /package/{test/slot/index.test.ts → tests/slot/index.ts} +0 -0
  53. /package/{test/slot/render.test.ts → tests/slot/render.ts} +0 -0
  54. /package/{test/svg.test.ts → tests/svg.ts} +0 -0
  55. /package/{test/utilities.test.ts → tests/utilities.ts} +0 -0
package/llm.txt CHANGED
@@ -30,9 +30,16 @@ A compile-time template system that transforms `html` tagged template literals i
30
30
  │ RUNTIME │
31
31
  ├─────────────────────────────────────────────────────────────────┤
32
32
  │ utilities.ts → template(), clone(), fragment() │
33
+ │ hmr.ts → createHotTemplate(), accept() (dev only) │
33
34
  │ attributes.ts → setList(), setProperty(), reactive bindings │
34
35
  │ slot/ → ArraySlot, EffectSlot, static rendering │
35
36
  │ event/ → Event delegation, lifecycle hooks │
37
+ ├─────────────────────────────────────────────────────────────────┤
38
+ │ VITE PLUGIN (DEV) │
39
+ ├─────────────────────────────────────────────────────────────────┤
40
+ │ plugins/vite.ts → Wraps base transform + injects HMR code │
41
+ │ In dev: template() → createHotTemplate() │
42
+ │ Appends import.meta.hot.accept() │
36
43
  └─────────────────────────────────────────────────────────────────┘
37
44
  ```
38
45
 
@@ -182,11 +189,17 @@ Caches parsed HTML, returns cloned fragments on each call.
182
189
  - Wraps reactive function in `effect()`
183
190
  - Updates content on dependency changes
184
191
  - Batches updates via `requestAnimationFrame`
192
+ - **Async function support**: detects async functions via `isAsyncFunction()` from `@esportsplus/utilities`
193
+ - Async functions receive a `fallback` callback for loading state, resolved value replaces content
194
+ - Async signature: `async (fallback: (content: Renderable) => void) => Promise<Renderable>`
185
195
 
186
196
  3. **ArraySlot** (`slot/array.ts`)
187
197
  - Handles reactive arrays with fine-grained updates
188
- - Listens to array mutation events (push, pop, splice, etc.)
198
+ - Listens to array mutation events (push, pop, splice, sort, reverse, etc.)
189
199
  - Maintains DOM node groups per array item
200
+ - **`moveBefore` DOM API**: sort/reverse use `moveBefore` when available for non-destructive reordering (preserves focus, animations, iframe state)
201
+ - Falls back to `insertBefore` in browsers without `moveBefore`
202
+ - **LIS algorithm**: sort uses longest increasing subsequence to minimize DOM moves
190
203
 
191
204
  **SlotGroup Structure**:
192
205
 
@@ -291,6 +304,41 @@ const remove = (...groups) => {
291
304
  };
292
305
  ```
293
306
 
307
+ ### HMR Module: `hmr.ts`
308
+
309
+ **Purpose**: Dev-only runtime registry for hot module replacement of templates.
310
+
311
+ **Key Concepts**:
312
+ - In production, templates use `utilities.template()` which caches a parsed fragment and returns clones
313
+ - In development, the Vite plugin replaces `template()` calls with `createHotTemplate()` which registers factories in a module→template registry
314
+ - On file save, Vite triggers HMR accept which calls `accept(moduleId)` to invalidate cached fragments
315
+ - Next factory call re-parses the updated HTML, producing fresh DOM without page reload
316
+
317
+ **API**:
318
+
319
+ ```typescript
320
+ // Register or update a template factory for HMR tracking
321
+ createHotTemplate(moduleId: string, templateId: string, html: string): () => DocumentFragment
322
+
323
+ // Invalidate all cached templates for a module (called from import.meta.hot.accept)
324
+ accept(moduleId: string): void
325
+ ```
326
+
327
+ **Registry Structure**: `Map<moduleId, Map<templateId, HotTemplate>>` where HotTemplate holds the html string, cached fragment, and factory closure.
328
+
329
+ ### Vite Plugin: `compiler/plugins/vite.ts`
330
+
331
+ **Purpose**: Wraps the base `plugin.vite()` transform with HMR injection.
332
+
333
+ **How it works**:
334
+ 1. `configResolved` captures dev mode (`command === 'serve'` or `mode === 'development'`)
335
+ 2. In dev mode, after the base transform runs, `injectHMR()` post-processes the output:
336
+ - Replaces `NAMESPACE.template(\`...\`)` → `NAMESPACE.createHotTemplate(moduleId, templateId, \`...\`)`
337
+ - Appends `import.meta.hot.accept(() => { NAMESPACE.accept(moduleId); })`
338
+ 3. In production, no HMR code is injected — identical to previous behavior
339
+
340
+ **Regex**: `TEMPLATE_CALL_REGEX` matches `const varName = NAMESPACE.template(` to capture the variable name as the templateId.
341
+
294
342
  ## Constants Reference
295
343
 
296
344
  ### `constants.ts` (Runtime)
@@ -330,6 +378,11 @@ const remove = (...groups) => {
330
378
  - Add to `DIRECT_ATTACH_EVENTS` set
331
379
  - Uses existing `on()` function
332
380
 
381
+ 4. **Modifying HMR behavior**
382
+ - `hmr.ts` is standalone with no internal deps — safe to modify
383
+ - `injectHMR()` in `plugins/vite.ts` only runs in dev mode
384
+ - Production builds are unaffected by HMR changes
385
+
333
386
  ### Dangerous Changes
334
387
 
335
388
  1. **Modifying `parser.ts` level tracking**
@@ -345,6 +398,11 @@ const remove = (...groups) => {
345
398
  - Slots must be processed in document order
346
399
  - Expression indices must match slot order
347
400
 
401
+ 4. **Modifying `slot/array.ts` sort/sync**
402
+ - Uses `moveBefore` API with `insertBefore` fallback — both paths must stay in sync
403
+ - LIS algorithm in `lis()` determines which nodes to keep in place; incorrect LIS = unnecessary moves
404
+ - `sync()` and `sort()` both have moveBefore/insertBefore branches
405
+
348
406
  ### Testing Recommendations
349
407
 
350
408
  After modifying parser/codegen:
@@ -373,6 +431,7 @@ After modifying parser/codegen:
373
431
  index.ts (runtime entry)
374
432
  ├── constants.ts
375
433
  ├── attributes.ts ← constants, types, utilities, event
434
+ ├── hmr.ts (standalone, no internal deps)
376
435
  ├── html.ts ← types, slot (stub, replaced at compile)
377
436
  ├── render.ts ← types, utilities, slot
378
437
  ├── svg.ts (same as html.ts)
@@ -381,7 +440,7 @@ index.ts (runtime entry)
381
440
  ├── slot/
382
441
  │ ├── index.ts ← types, effect, render
383
442
  │ ├── array.ts ← reactivity, constants, types, utilities, cleanup, html
384
- │ ├── effect.ts ← reactivity, types, utilities, cleanup, render
443
+ │ ├── effect.ts ← reactivity, utilities (isAsyncFunction), types, utilities, cleanup, render
385
444
  │ ├── cleanup.ts ← constants, types
386
445
  │ └── render.ts ← utilities, constants, types, array
387
446
  └── event/
@@ -398,6 +457,6 @@ compiler/
398
457
  ├── ts-parser.ts ← typescript, typescript/compiler, constants
399
458
  ├── ts-analyzer.ts ← typescript, constants
400
459
  └── plugins/
401
- ├── vite.ts ← typescript/compiler, constants, reactivity/compiler, ..
402
- └── tsc.ts (similar)
460
+ ├── vite.ts ← typescript/compiler, constants, reactivity/compiler, .. (wraps base plugin, adds HMR injection)
461
+ └── tsc.ts (similar, no HMR)
403
462
  ```
package/package.json CHANGED
@@ -38,8 +38,9 @@
38
38
  },
39
39
  "type": "module",
40
40
  "types": "./build/index.d.ts",
41
- "version": "0.16.14",
41
+ "version": "0.17.1",
42
42
  "scripts": {
43
+ "bench:run": "vitest bench --config vitest.bench.config.ts",
43
44
  "build": "tsc",
44
45
  "test": "vitest run",
45
46
  "test:watch": "vitest",
package/src/attributes.ts CHANGED
@@ -29,7 +29,10 @@ function apply(element: Element, name: string, value: unknown) {
29
29
  else if (name === 'class') {
30
30
  element.className = value as string;
31
31
  }
32
- else if (name === 'style' || (name[0] === 'd' && name.startsWith('data-')) || element['ownerSVGElement']) {
32
+ else if (name === 'style') {
33
+ element.style.cssText = value as string;
34
+ }
35
+ else if ((name[0] === 'd' && name.startsWith('data-')) || element['ownerSVGElement']) {
33
36
  element.setAttribute(name, value as string);
34
37
  }
35
38
  else {
@@ -1,10 +1,79 @@
1
+ import { NAMESPACE, PACKAGE_NAME } from '../constants';
1
2
  import { plugin } from '@esportsplus/typescript/compiler';
2
- import { PACKAGE_NAME } from '../constants';
3
+
3
4
  import reactivity from '@esportsplus/reactivity/compiler';
4
5
  import template from '..';
5
6
 
6
7
 
7
- export default plugin.vite({
8
- name: PACKAGE_NAME,
9
- plugins: [reactivity, template]
10
- });
8
+ type VitePlugin = {
9
+ configResolved: (config: any) => void;
10
+ enforce: 'pre';
11
+ handleHotUpdate?: (ctx: { file: string; modules: any[] }) => void;
12
+ name: string;
13
+ transform: (code: string, id: string) => { code: string; map: null } | null;
14
+ watchChange: (id: string) => void;
15
+ };
16
+
17
+
18
+ const TEMPLATE_SEARCH = NAMESPACE + '.template(';
19
+
20
+ const TEMPLATE_CALL_REGEX = new RegExp(
21
+ '(const\\s+(\\w+)\\s*=\\s*' + NAMESPACE + '\\.template\\()(`)',
22
+ 'g'
23
+ );
24
+
25
+
26
+ let base = plugin.vite({
27
+ name: PACKAGE_NAME,
28
+ plugins: [reactivity, template]
29
+ });
30
+
31
+
32
+ function injectHMR(code: string, id: string): string {
33
+ let hmrId = id.replace(/\\/g, '/'),
34
+ hotReplace = NAMESPACE + '.createHotTemplate("' + hmrId + '", "',
35
+ injected = code.replace(TEMPLATE_CALL_REGEX, function(_match: string, prefix: string, varName: string, backtick: string) {
36
+ return prefix.replace(TEMPLATE_SEARCH, hotReplace + varName + '", ') + backtick;
37
+ });
38
+
39
+ if (injected === code) {
40
+ return code;
41
+ }
42
+
43
+ injected += '\nif (import.meta.hot) { import.meta.hot.accept(() => { ' + NAMESPACE + '.accept("' + hmrId + '"); }); }';
44
+
45
+ return injected;
46
+ }
47
+
48
+
49
+ export default ({ root }: { root?: string } = {}) => {
50
+ let isDev = false,
51
+ vitePlugin = base({ root });
52
+
53
+ return {
54
+ ...vitePlugin,
55
+ configResolved(config: any) {
56
+ vitePlugin.configResolved(config);
57
+ isDev = config?.command === 'serve' || config?.mode === 'development';
58
+ },
59
+ handleHotUpdate(_ctx: { file: string; modules: any[] }) {
60
+ // Let Vite handle the default HMR flow
61
+ },
62
+ transform(code: string, id: string) {
63
+ let result = vitePlugin.transform(code, id);
64
+
65
+ if (!result || !isDev) {
66
+ return result;
67
+ }
68
+
69
+ let injected = injectHMR(result.code, id);
70
+
71
+ if (injected === result.code) {
72
+ return result;
73
+ }
74
+
75
+ return { code: injected, map: null };
76
+ }
77
+ } satisfies VitePlugin;
78
+ };
79
+
package/src/hmr.ts ADDED
@@ -0,0 +1,70 @@
1
+ let clone = <T extends DocumentFragment | Node>(node: T, deep: boolean = true) => node.cloneNode(deep) as T,
2
+ modules = new Map<string, Map<string, HotTemplate>>(),
3
+ tmpl = typeof document !== 'undefined' ? document.createElement('template') : null;
4
+
5
+
6
+ type HotTemplate = {
7
+ cached: DocumentFragment | undefined;
8
+ factory: () => DocumentFragment;
9
+ html: string;
10
+ };
11
+
12
+
13
+ function invalidate(moduleId: string): void {
14
+ let templates = modules.get(moduleId);
15
+
16
+ if (!templates) {
17
+ return;
18
+ }
19
+
20
+ for (let [, entry] of templates) {
21
+ entry.cached = undefined;
22
+ }
23
+ }
24
+
25
+ function register(moduleId: string, templateId: string, html: string): () => DocumentFragment {
26
+ let entry: HotTemplate = {
27
+ cached: undefined,
28
+ factory: () => {
29
+ if (!entry.cached) {
30
+ let element = tmpl!.cloneNode() as HTMLTemplateElement;
31
+
32
+ element.innerHTML = entry.html;
33
+ entry.cached = element.content;
34
+ }
35
+
36
+ return clone(entry.cached!, true) as DocumentFragment;
37
+ },
38
+ html
39
+ };
40
+
41
+ (modules.get(moduleId) ?? (modules.set(moduleId, new Map()), modules.get(moduleId)!)).set(templateId, entry);
42
+
43
+ return entry.factory;
44
+ }
45
+
46
+
47
+ const accept = (moduleId: string): void => {
48
+ invalidate(moduleId);
49
+ };
50
+
51
+ const createHotTemplate = (moduleId: string, templateId: string, html: string): () => DocumentFragment => {
52
+ let existing = modules.get(moduleId)?.get(templateId);
53
+
54
+ if (existing) {
55
+ existing.cached = undefined;
56
+ existing.html = html;
57
+
58
+ return existing.factory;
59
+ }
60
+
61
+ return register(moduleId, templateId, html);
62
+ };
63
+
64
+ // Test-only: reset state
65
+ const hmrReset = (): void => {
66
+ modules.clear();
67
+ };
68
+
69
+
70
+ export { accept, createHotTemplate, hmrReset, modules };
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ if (typeof Node !== 'undefined') {
10
10
 
11
11
  export * from './attributes';
12
12
  export * from './event';
13
+ export * from './hmr';
13
14
  export * from './utilities';
14
15
 
15
16
  export { default as html } from './html';
package/src/slot/array.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { read, root, signal, write, Reactive } from '@esportsplus/reactivity';
2
2
  import { ARRAY_SLOT } from '../constants';
3
3
  import { Element, SlotGroup } from '../types';
4
- import { clone, fragment, marker, raf } from '../utilities';
4
+ import { clone, EMPTY_FRAGMENT, marker, raf } from '../utilities';
5
5
  import { ondisconnect, remove } from './cleanup';
6
+
6
7
  import html from '../html';
7
8
 
8
9
 
@@ -19,7 +20,51 @@ type ArraySlotOp<T> =
19
20
  | { op: 'sort'; order: number[] };
20
21
 
21
22
 
22
- const EMPTY_FRAGMENT = fragment('');
23
+ function lis(arr: number[]): Set<number> {
24
+ let n = arr.length;
25
+
26
+ if (n === 0) {
27
+ return new Set();
28
+ }
29
+
30
+ let ends = new Int32Array(n),
31
+ predecessors = new Int32Array(n),
32
+ len = 0;
33
+
34
+ for (let i = 0; i < n; i++) {
35
+ let lo = 0,
36
+ hi = len,
37
+ val = arr[i];
38
+
39
+ while (lo < hi) {
40
+ let mid = (lo + hi) >> 1;
41
+
42
+ if (arr[ends[mid]] < val) {
43
+ lo = mid + 1;
44
+ }
45
+ else {
46
+ hi = mid;
47
+ }
48
+ }
49
+
50
+ ends[lo] = i;
51
+ predecessors[i] = lo > 0 ? ends[lo - 1] : -1;
52
+
53
+ if (lo >= len) {
54
+ len = lo + 1;
55
+ }
56
+ }
57
+
58
+ let idx = ends[len - 1],
59
+ result = new Set<number>();
60
+
61
+ for (let i = len - 1; i >= 0; i--) {
62
+ result.add(idx);
63
+ idx = predecessors[idx];
64
+ }
65
+
66
+ return result;
67
+ }
23
68
 
24
69
 
25
70
  class ArraySlot<T> {
@@ -204,14 +249,48 @@ class ArraySlot<T> {
204
249
  return;
205
250
  }
206
251
 
207
- let sorted = new Array(n) as SlotGroup[];
252
+ let end: Node | null = n > 0 ? nodes[n - 1].tail.nextSibling : null,
253
+ keep = lis(order),
254
+ parent = this.marker.parentNode,
255
+ sorted = new Array(n) as SlotGroup[];
208
256
 
209
257
  for (let i = 0; i < n; i++) {
210
258
  sorted[i] = nodes[order[i]];
211
259
  }
212
260
 
213
261
  this.nodes = sorted;
214
- this.sync();
262
+
263
+ if (!parent || keep.size === n) {
264
+ return;
265
+ }
266
+
267
+ let ref: Node | null = end,
268
+ useMoveBefore = 'moveBefore' in parent;
269
+
270
+ for (let i = n - 1; i >= 0; i--) {
271
+ let group = sorted[i];
272
+
273
+ if (keep.has(i)) {
274
+ ref = group.head;
275
+ continue;
276
+ }
277
+
278
+ let node: Node | null = group.tail;
279
+
280
+ while (node) {
281
+ let prev: Node | null = node === group.head ? null : node.previousSibling;
282
+
283
+ if (useMoveBefore) {
284
+ (parent as any).moveBefore(node, ref);
285
+ }
286
+ else {
287
+ parent.insertBefore(node, ref);
288
+ }
289
+
290
+ ref = node;
291
+ node = prev;
292
+ }
293
+ }
215
294
  }
216
295
 
217
296
  private splice(start: number, stop: number = this.nodes.length, items: T[]) {
@@ -232,6 +311,27 @@ class ArraySlot<T> {
232
311
  return;
233
312
  }
234
313
 
314
+ let parent = this.marker.parentNode;
315
+
316
+ if (parent && 'moveBefore' in parent) {
317
+ let ref: Node | null = nodes[0].tail.nextSibling;
318
+
319
+ for (let i = n - 1; i >= 0; i--) {
320
+ let group = nodes[i],
321
+ node: Node | null = group.tail;
322
+
323
+ while (node) {
324
+ let prev: Node | null = node === group.head ? null : node.previousSibling;
325
+
326
+ (parent as any).moveBefore(node, ref);
327
+ ref = node;
328
+ node = prev;
329
+ }
330
+ }
331
+
332
+ return;
333
+ }
334
+
235
335
  for (let i = 0; i < n; i++) {
236
336
  let group = nodes[i],
237
337
  next: Node | null,
@@ -1,4 +1,5 @@
1
1
  import { effect } from '@esportsplus/reactivity';
2
+ import { isAsyncFunction } from '@esportsplus/utilities';
2
3
  import { Element, Renderable, SlotGroup } from '../types';
3
4
  import { raf, text } from '../utilities'
4
5
  import { remove } from './cleanup';
@@ -20,37 +21,50 @@ function read(value: unknown): unknown {
20
21
 
21
22
  class EffectSlot {
22
23
  anchor: Element;
23
- disposer: VoidFunction;
24
+ disposer: VoidFunction | null;
24
25
  group: SlotGroup | null = null;
25
26
  scheduled = false;
26
27
  textnode: Node | null = null;
27
28
 
28
29
 
29
- constructor(anchor: Element, fn: (dispose?: VoidFunction) => Renderable<any>) {
30
- let dispose = fn.length ? () => this.dispose() : undefined,
31
- value: unknown;
32
-
30
+ constructor(anchor: Element, fn: ((...args: any[]) => any)) {
33
31
  this.anchor = anchor;
34
- this.disposer = effect(() => {
35
- value = read( fn(dispose) );
32
+ this.disposer = null;
36
33
 
37
- if (!this.disposer) {
38
- this.update(value);
39
- }
40
- else if (!this.scheduled) {
41
- this.scheduled = true;
34
+ if (isAsyncFunction(fn)) {
35
+ (fn as (fallback: (content: Renderable<any>) => void) => Promise<Renderable<any>>)(
36
+ (content) => this.update(content)
37
+ ).then((value) => this.update(value), () => {});
38
+ }
39
+ else {
40
+ let dispose = fn.length ? () => this.dispose() : undefined,
41
+ value: unknown;
42
+
43
+ this.disposer = effect(() => {
44
+ value = read( fn(dispose) );
42
45
 
43
- raf(() => {
44
- this.scheduled = false;
46
+ if (!this.disposer) {
45
47
  this.update(value);
46
- });
47
- }
48
- });
48
+ }
49
+ else if (!this.scheduled) {
50
+ this.scheduled = true;
51
+
52
+ raf(() => {
53
+ this.scheduled = false;
54
+ this.update(value);
55
+ });
56
+ }
57
+ });
58
+ }
49
59
  }
50
60
 
51
61
 
52
62
  dispose() {
53
- let { anchor, group, textnode } = this;
63
+ let { anchor, disposer, group, textnode } = this;
64
+
65
+ if (!disposer) {
66
+ return;
67
+ }
54
68
 
55
69
  if (textnode) {
56
70
  group = { head: anchor, tail: textnode as Element };
@@ -59,7 +73,7 @@ class EffectSlot {
59
73
  group.head = anchor;
60
74
  }
61
75
 
62
- this.disposer();
76
+ disposer();
63
77
 
64
78
  if (group) {
65
79
  remove(group);
@@ -69,6 +83,8 @@ class EffectSlot {
69
83
  update(value: unknown): void {
70
84
  let { anchor, group, textnode } = this;
71
85
 
86
+ value = read(value);
87
+
72
88
  if (group) {
73
89
  remove(group);
74
90
  this.group = null;
@@ -92,7 +108,17 @@ class EffectSlot {
92
108
  }
93
109
  else {
94
110
  let fragment = render(anchor, value),
111
+ head: Node | null,
112
+ tail: Node | null;
113
+
114
+ if (fragment.nodeType === 11) {
95
115
  head = fragment.firstChild;
116
+ tail = fragment.lastChild;
117
+ }
118
+ else {
119
+ head = fragment;
120
+ tail = fragment;
121
+ }
96
122
 
97
123
  if (textnode?.isConnected) {
98
124
  remove({ head: textnode as Element, tail: textnode as Element });
@@ -101,7 +127,7 @@ class EffectSlot {
101
127
  if (head) {
102
128
  this.group = {
103
129
  head: head as Element,
104
- tail: fragment.lastChild as Element
130
+ tail: tail as Element
105
131
  };
106
132
 
107
133
  anchor.after(fragment);
@@ -1,13 +1,10 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
2
  import { ARRAY_SLOT } from '../constants';
3
3
  import { Element } from '../types';
4
- import { clone, fragment, text } from '../utilities';
4
+ import { clone, EMPTY_FRAGMENT, text } from '../utilities';
5
5
  import { ArraySlot } from './array';
6
6
 
7
7
 
8
- const EMPTY_FRAGMENT = fragment('');
9
-
10
-
11
8
  export default function render(anchor: Element, value: unknown): Node {
12
9
  if (value == null || value === false || value === '') {
13
10
  return EMPTY_FRAGMENT;
package/src/utilities.ts CHANGED
@@ -19,6 +19,8 @@ const fragment = (html: string): DocumentFragment => {
19
19
  return element.content;
20
20
  };
21
21
 
22
+ const EMPTY_FRAGMENT = fragment('');
23
+
22
24
  const marker = fragment(SLOT_HTML).firstChild!;
23
25
 
24
26
  const raf = globalThis?.requestAnimationFrame;
@@ -50,4 +52,4 @@ const text = (value: string) => {
50
52
  };
51
53
 
52
54
 
53
- export { clone, fragment, template, marker, raf, text };
55
+ export { clone, EMPTY_FRAGMENT, fragment, marker, raf, template, text };
@@ -61,7 +61,7 @@ describe('attributes', () => {
61
61
  it('sets style attribute', () => {
62
62
  setProperty(element as unknown as Element, 'style', 'color: red');
63
63
 
64
- expect(element.getAttribute('style')).toBe('color: red');
64
+ expect(element.style.cssText).toContain('color: red');
65
65
  });
66
66
 
67
67
  it('sets data-* attributes via setAttribute', () => {
@@ -107,7 +107,8 @@ describe('attributes', () => {
107
107
  it('merges static and dynamic styles', () => {
108
108
  setList(element as unknown as Element, 'style', 'font-size: 14px', { style: 'color: red' });
109
109
 
110
- expect(element.getAttribute('style')).toBe('color: red;font-size: 14px');
110
+ expect(element.style.cssText).toContain('color: red');
111
+ expect(element.style.cssText).toContain('font-size: 14px');
111
112
  });
112
113
 
113
114
  it('handles null value', () => {