@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/README.md CHANGED
@@ -9,6 +9,9 @@ High-performance, compiler-optimized HTML templating library for JavaScript/Type
9
9
  - **Reactive integration** - Works with `@esportsplus/reactivity` for dynamic updates
10
10
  - **Event delegation** - Efficient event handling with automatic delegation
11
11
  - **Lifecycle events** - `onconnect`, `ondisconnect`, `onrender`, `onresize`, `ontick`
12
+ - **Async slots** - Async function support with fallback content in `EffectSlot`
13
+ - **Non-destructive reordering** - Uses `moveBefore` DOM API for array sort/reverse when available
14
+ - **HMR support** - Fine-grained hot module replacement for templates in development
12
15
  - **Type-safe** - Full TypeScript support
13
16
 
14
17
  ## Installation
@@ -29,10 +32,12 @@ import { defineConfig } from 'vite';
29
32
  import template from '@esportsplus/template/compiler/vite';
30
33
 
31
34
  export default defineConfig({
32
- plugins: [template]
35
+ plugins: [template()]
33
36
  });
34
37
  ```
35
38
 
39
+ HMR is automatically enabled in development mode (`vite dev`). When a file containing `html` templates is saved, only the affected template factories are invalidated and re-created — no full page reload needed.
40
+
36
41
  ### TypeScript Compiler (tsc)
37
42
 
38
43
  For direct `tsc` compilation, use the transformer:
@@ -179,6 +184,31 @@ const todoList = (todos: string[]) =>
179
184
  html`<ul>${html.reactive(todos, todo => html`<li>${todo}</li>`)}</ul>`;
180
185
  ```
181
186
 
187
+ Array sort and reverse operations use the `moveBefore` DOM API when available, preserving element state (focus, animations, iframe content) during reordering. Falls back to `insertBefore` in older browsers.
188
+
189
+ ### Async Slots
190
+
191
+ Async functions are supported in effect slots, with an optional fallback callback for loading states:
192
+
193
+ ```typescript
194
+ import { html } from '@esportsplus/template';
195
+
196
+ // Async with fallback content while loading
197
+ const asyncContent = () =>
198
+ html`<div>${async (fallback: (content: any) => void) => {
199
+ fallback(html`<span>Loading...</span>`);
200
+ const data = await fetchData();
201
+ return html`<span>${data}</span>`;
202
+ }}</div>`;
203
+
204
+ // Simple async (no fallback)
205
+ const simpleAsync = () =>
206
+ html`<div>${async () => {
207
+ const data = await fetchData();
208
+ return html`<span>${data}</span>`;
209
+ }}</div>`;
210
+ ```
211
+
182
212
  ## Events
183
213
 
184
214
  ### Standard DOM Events
@@ -270,6 +300,8 @@ const circle = (fill: string) =>
270
300
  | `slot` | Slot rendering |
271
301
  | `ArraySlot` | Reactive array rendering |
272
302
  | `EffectSlot` | Reactive effect rendering |
303
+ | `accept` | HMR accept handler (dev only) |
304
+ | `createHotTemplate` | HMR template factory (dev only) |
273
305
 
274
306
  ### Types
275
307
 
@@ -0,0 +1,207 @@
1
+ import { bench, describe } from 'vitest';
2
+
3
+
4
+ describe('attributes - apply', () => {
5
+ let element: HTMLDivElement;
6
+
7
+ bench('setAttribute style', () => {
8
+ element = document.createElement('div');
9
+ element.setAttribute('style', 'color: red; font-size: 14px; display: flex;');
10
+ });
11
+
12
+ bench('style.cssText', () => {
13
+ element = document.createElement('div');
14
+ element.style.cssText = 'color: red; font-size: 14px; display: flex;';
15
+ });
16
+
17
+ bench('className assignment', () => {
18
+ element = document.createElement('div');
19
+ element.className = 'foo bar baz qux';
20
+ });
21
+
22
+ bench('setAttribute class', () => {
23
+ element = document.createElement('div');
24
+ element.setAttribute('class', 'foo bar baz qux');
25
+ });
26
+ });
27
+
28
+
29
+ describe('attributes - class list rebuild', () => {
30
+ bench('Set for..of + string concat', () => {
31
+ let set = new Set(['alpha', 'beta', 'gamma', 'delta', 'epsilon']),
32
+ result = '';
33
+
34
+ for (let key of set) {
35
+ result += (result ? ' ' : '') + key;
36
+ }
37
+ });
38
+
39
+ bench('Array.from(set).join', () => {
40
+ let set = new Set(['alpha', 'beta', 'gamma', 'delta', 'epsilon']);
41
+
42
+ Array.from(set).join(' ');
43
+ });
44
+
45
+ bench('set forEach + string concat', () => {
46
+ let set = new Set(['alpha', 'beta', 'gamma', 'delta', 'epsilon']),
47
+ result = '';
48
+
49
+ set.forEach(key => {
50
+ result += (result ? ' ' : '') + key;
51
+ });
52
+ });
53
+ });
54
+
55
+
56
+ describe('event - defineProperty overhead', () => {
57
+ let event: Event;
58
+
59
+ bench('defineProperty per dispatch', () => {
60
+ event = new Event('click');
61
+ let node: HTMLElement | null = document.createElement('div');
62
+
63
+ Object.defineProperty(event, 'currentTarget', {
64
+ configurable: true,
65
+ get() {
66
+ return node || document;
67
+ }
68
+ });
69
+ });
70
+
71
+ bench('defineProperty once + closure update', () => {
72
+ event = new Event('click');
73
+ let currentNode: HTMLElement | null = null;
74
+
75
+ Object.defineProperty(event, 'currentTarget', {
76
+ configurable: true,
77
+ get() {
78
+ return currentNode || document;
79
+ }
80
+ });
81
+
82
+ currentNode = document.createElement('div');
83
+ });
84
+ });
85
+
86
+
87
+ describe('marker - comment vs text node', () => {
88
+ let comment = document.createComment('$'),
89
+ textNode = document.createTextNode('');
90
+
91
+ bench('clone comment node', () => {
92
+ comment.cloneNode();
93
+ });
94
+
95
+ bench('clone text node', () => {
96
+ textNode.cloneNode();
97
+ });
98
+ });
99
+
100
+
101
+ describe('ontick - Set iteration', () => {
102
+ let tasks = new Set<VoidFunction>();
103
+
104
+ for (let i = 0; i < 10; i++) {
105
+ tasks.add(() => {});
106
+ }
107
+
108
+ bench('for..of Set', () => {
109
+ for (let task of tasks) {
110
+ task();
111
+ }
112
+ });
113
+
114
+ bench('Set.forEach', () => {
115
+ tasks.forEach(task => task());
116
+ });
117
+ });
118
+
119
+
120
+ describe('array sync - fragment append', () => {
121
+ let fragment: DocumentFragment,
122
+ nodes: Node[];
123
+
124
+ bench('individual append', () => {
125
+ fragment = document.createDocumentFragment();
126
+ nodes = [];
127
+
128
+ for (let i = 0; i < 50; i++) {
129
+ nodes.push(document.createElement('div'));
130
+ }
131
+
132
+ for (let i = 0, n = nodes.length; i < n; i++) {
133
+ fragment.append(nodes[i]);
134
+ }
135
+ });
136
+
137
+ bench('batch append spread', () => {
138
+ fragment = document.createDocumentFragment();
139
+ nodes = [];
140
+
141
+ for (let i = 0; i < 50; i++) {
142
+ nodes.push(document.createElement('div'));
143
+ }
144
+
145
+ fragment.append(...nodes);
146
+ });
147
+ });
148
+
149
+
150
+ describe('array sort - full resync vs minimal moves', () => {
151
+ let parent: HTMLDivElement,
152
+ fragment: DocumentFragment;
153
+
154
+ bench('full detach + reattach (current)', () => {
155
+ parent = document.createElement('div');
156
+ fragment = document.createDocumentFragment();
157
+
158
+ for (let i = 0; i < 50; i++) {
159
+ parent.appendChild(document.createElement('span'));
160
+ }
161
+
162
+ let children = Array.from(parent.children);
163
+
164
+ // Simulate: detach all, reattach in new order
165
+ for (let i = 0, n = children.length; i < n; i++) {
166
+ fragment.append(children[i]);
167
+ }
168
+
169
+ parent.appendChild(fragment);
170
+ });
171
+
172
+ bench('targeted insertBefore (LIS approach)', () => {
173
+ parent = document.createElement('div');
174
+
175
+ for (let i = 0; i < 50; i++) {
176
+ parent.appendChild(document.createElement('span'));
177
+ }
178
+
179
+ let children = Array.from(parent.children);
180
+
181
+ // Simulate: only move 5 out of 50 elements (90% stay)
182
+ for (let i = 0; i < 5; i++) {
183
+ let idx = Math.floor(Math.random() * children.length);
184
+
185
+ parent.insertBefore(children[idx], children[(idx + 10) % children.length]);
186
+ }
187
+ });
188
+ });
189
+
190
+
191
+ describe('fragment - dedup empty', () => {
192
+ let tmpl = document.createElement('template');
193
+
194
+ bench('fragment() call', () => {
195
+ let element = tmpl.cloneNode() as HTMLTemplateElement;
196
+
197
+ element.innerHTML = '';
198
+
199
+ element.content;
200
+ });
201
+
202
+ bench('cached fragment clone', () => {
203
+ let cached = document.createDocumentFragment();
204
+
205
+ cached.cloneNode(true);
206
+ });
207
+ });
@@ -12,7 +12,10 @@ function apply(element, name, value) {
12
12
  else if (name === 'class') {
13
13
  element.className = value;
14
14
  }
15
- else if (name === 'style' || (name[0] === 'd' && name.startsWith('data-')) || element['ownerSVGElement']) {
15
+ else if (name === 'style') {
16
+ element.style.cssText = value;
17
+ }
18
+ else if ((name[0] === 'd' && name.startsWith('data-')) || element['ownerSVGElement']) {
16
19
  element.setAttribute(name, value);
17
20
  }
18
21
  else {
@@ -1,13 +1,17 @@
1
1
  declare const _default: ({ root }?: {
2
2
  root?: string;
3
3
  }) => {
4
- configResolved: (config: unknown) => void;
5
- enforce: "pre";
6
- name: string;
7
- transform: (code: string, id: string) => {
4
+ configResolved(config: any): void;
5
+ handleHotUpdate(_ctx: {
6
+ file: string;
7
+ modules: any[];
8
+ }): void;
9
+ transform(code: string, id: string): {
8
10
  code: string;
9
11
  map: null;
10
12
  } | null;
13
+ enforce: "pre";
14
+ name: string;
11
15
  watchChange: (id: string) => void;
12
16
  };
13
17
  export default _default;
@@ -1,8 +1,43 @@
1
+ import { NAMESPACE, PACKAGE_NAME } from '../constants.js';
1
2
  import { plugin } from '@esportsplus/typescript/compiler';
2
- import { PACKAGE_NAME } from '../constants.js';
3
3
  import reactivity from '@esportsplus/reactivity/compiler';
4
4
  import template from '../index.js';
5
- export default plugin.vite({
5
+ const TEMPLATE_SEARCH = NAMESPACE + '.template(';
6
+ const TEMPLATE_CALL_REGEX = new RegExp('(const\\s+(\\w+)\\s*=\\s*' + NAMESPACE + '\\.template\\()(`)', 'g');
7
+ let base = plugin.vite({
6
8
  name: PACKAGE_NAME,
7
9
  plugins: [reactivity, template]
8
10
  });
11
+ function injectHMR(code, id) {
12
+ let hmrId = id.replace(/\\/g, '/'), hotReplace = NAMESPACE + '.createHotTemplate("' + hmrId + '", "', injected = code.replace(TEMPLATE_CALL_REGEX, function (_match, prefix, varName, backtick) {
13
+ return prefix.replace(TEMPLATE_SEARCH, hotReplace + varName + '", ') + backtick;
14
+ });
15
+ if (injected === code) {
16
+ return code;
17
+ }
18
+ injected += '\nif (import.meta.hot) { import.meta.hot.accept(() => { ' + NAMESPACE + '.accept("' + hmrId + '"); }); }';
19
+ return injected;
20
+ }
21
+ export default ({ root } = {}) => {
22
+ let isDev = false, vitePlugin = base({ root });
23
+ return {
24
+ ...vitePlugin,
25
+ configResolved(config) {
26
+ vitePlugin.configResolved(config);
27
+ isDev = config?.command === 'serve' || config?.mode === 'development';
28
+ },
29
+ handleHotUpdate(_ctx) {
30
+ },
31
+ transform(code, id) {
32
+ let result = vitePlugin.transform(code, id);
33
+ if (!result || !isDev) {
34
+ return result;
35
+ }
36
+ let injected = injectHMR(result.code, id);
37
+ if (injected === result.code) {
38
+ return result;
39
+ }
40
+ return { code: injected, map: null };
41
+ }
42
+ };
43
+ };
package/build/hmr.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ declare let modules: Map<string, Map<string, HotTemplate>>;
2
+ type HotTemplate = {
3
+ cached: DocumentFragment | undefined;
4
+ factory: () => DocumentFragment;
5
+ html: string;
6
+ };
7
+ declare const accept: (moduleId: string) => void;
8
+ declare const createHotTemplate: (moduleId: string, templateId: string, html: string) => () => DocumentFragment;
9
+ declare const hmrReset: () => void;
10
+ export { accept, createHotTemplate, hmrReset, modules };
package/build/hmr.js ADDED
@@ -0,0 +1,42 @@
1
+ let clone = (node, deep = true) => node.cloneNode(deep), modules = new Map(), tmpl = typeof document !== 'undefined' ? document.createElement('template') : null;
2
+ function invalidate(moduleId) {
3
+ let templates = modules.get(moduleId);
4
+ if (!templates) {
5
+ return;
6
+ }
7
+ for (let [, entry] of templates) {
8
+ entry.cached = undefined;
9
+ }
10
+ }
11
+ function register(moduleId, templateId, html) {
12
+ let entry = {
13
+ cached: undefined,
14
+ factory: () => {
15
+ if (!entry.cached) {
16
+ let element = tmpl.cloneNode();
17
+ element.innerHTML = entry.html;
18
+ entry.cached = element.content;
19
+ }
20
+ return clone(entry.cached, true);
21
+ },
22
+ html
23
+ };
24
+ (modules.get(moduleId) ?? (modules.set(moduleId, new Map()), modules.get(moduleId))).set(templateId, entry);
25
+ return entry.factory;
26
+ }
27
+ const accept = (moduleId) => {
28
+ invalidate(moduleId);
29
+ };
30
+ const createHotTemplate = (moduleId, templateId, html) => {
31
+ let existing = modules.get(moduleId)?.get(templateId);
32
+ if (existing) {
33
+ existing.cached = undefined;
34
+ existing.html = html;
35
+ return existing.factory;
36
+ }
37
+ return register(moduleId, templateId, html);
38
+ };
39
+ const hmrReset = () => {
40
+ modules.clear();
41
+ };
42
+ export { accept, createHotTemplate, hmrReset, modules };
package/build/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './attributes.js';
2
2
  export * from './event/index.js';
3
+ export * from './hmr.js';
3
4
  export * from './utilities.js';
4
5
  export { default as html } from './html.js';
5
6
  export { default as render } from './render.js';
package/build/index.js CHANGED
@@ -5,6 +5,7 @@ if (typeof Node !== 'undefined') {
5
5
  }
6
6
  export * from './attributes.js';
7
7
  export * from './event/index.js';
8
+ export * from './hmr.js';
8
9
  export * from './utilities.js';
9
10
  export { default as html } from './html.js';
10
11
  export { default as render } from './render.js';
@@ -1,8 +1,37 @@
1
1
  import { read, root, signal, write } from '@esportsplus/reactivity';
2
2
  import { ARRAY_SLOT } from '../constants.js';
3
- import { clone, fragment, marker, raf } from '../utilities.js';
3
+ import { clone, EMPTY_FRAGMENT, marker, raf } from '../utilities.js';
4
4
  import { ondisconnect, remove } from './cleanup.js';
5
- const EMPTY_FRAGMENT = fragment('');
5
+ function lis(arr) {
6
+ let n = arr.length;
7
+ if (n === 0) {
8
+ return new Set();
9
+ }
10
+ let ends = new Int32Array(n), predecessors = new Int32Array(n), len = 0;
11
+ for (let i = 0; i < n; i++) {
12
+ let lo = 0, hi = len, val = arr[i];
13
+ while (lo < hi) {
14
+ let mid = (lo + hi) >> 1;
15
+ if (arr[ends[mid]] < val) {
16
+ lo = mid + 1;
17
+ }
18
+ else {
19
+ hi = mid;
20
+ }
21
+ }
22
+ ends[lo] = i;
23
+ predecessors[i] = lo > 0 ? ends[lo - 1] : -1;
24
+ if (lo >= len) {
25
+ len = lo + 1;
26
+ }
27
+ }
28
+ let idx = ends[len - 1], result = new Set();
29
+ for (let i = len - 1; i >= 0; i--) {
30
+ result.add(idx);
31
+ idx = predecessors[idx];
32
+ }
33
+ return result;
34
+ }
6
35
  class ArraySlot {
7
36
  array;
8
37
  marker;
@@ -153,12 +182,34 @@ class ArraySlot {
153
182
  this.marker.after(this.fragment);
154
183
  return;
155
184
  }
156
- let sorted = new Array(n);
185
+ let end = n > 0 ? nodes[n - 1].tail.nextSibling : null, keep = lis(order), parent = this.marker.parentNode, sorted = new Array(n);
157
186
  for (let i = 0; i < n; i++) {
158
187
  sorted[i] = nodes[order[i]];
159
188
  }
160
189
  this.nodes = sorted;
161
- this.sync();
190
+ if (!parent || keep.size === n) {
191
+ return;
192
+ }
193
+ let ref = end, useMoveBefore = 'moveBefore' in parent;
194
+ for (let i = n - 1; i >= 0; i--) {
195
+ let group = sorted[i];
196
+ if (keep.has(i)) {
197
+ ref = group.head;
198
+ continue;
199
+ }
200
+ let node = group.tail;
201
+ while (node) {
202
+ let prev = node === group.head ? null : node.previousSibling;
203
+ if (useMoveBefore) {
204
+ parent.moveBefore(node, ref);
205
+ }
206
+ else {
207
+ parent.insertBefore(node, ref);
208
+ }
209
+ ref = node;
210
+ node = prev;
211
+ }
212
+ }
162
213
  }
163
214
  splice(start, stop = this.nodes.length, items) {
164
215
  if (!items.length) {
@@ -173,6 +224,20 @@ class ArraySlot {
173
224
  if (!n) {
174
225
  return;
175
226
  }
227
+ let parent = this.marker.parentNode;
228
+ if (parent && 'moveBefore' in parent) {
229
+ let ref = nodes[0].tail.nextSibling;
230
+ for (let i = n - 1; i >= 0; i--) {
231
+ let group = nodes[i], node = group.tail;
232
+ while (node) {
233
+ let prev = node === group.head ? null : node.previousSibling;
234
+ parent.moveBefore(node, ref);
235
+ ref = node;
236
+ node = prev;
237
+ }
238
+ }
239
+ return;
240
+ }
176
241
  for (let i = 0; i < n; i++) {
177
242
  let group = nodes[i], next, node = group.head;
178
243
  while (node) {
@@ -1,11 +1,11 @@
1
- import { Element, Renderable, SlotGroup } from '../types.js';
1
+ import { Element, SlotGroup } from '../types.js';
2
2
  declare class EffectSlot {
3
3
  anchor: Element;
4
- disposer: VoidFunction;
4
+ disposer: VoidFunction | null;
5
5
  group: SlotGroup | null;
6
6
  scheduled: boolean;
7
7
  textnode: Node | null;
8
- constructor(anchor: Element, fn: (dispose?: VoidFunction) => Renderable<any>);
8
+ constructor(anchor: Element, fn: ((...args: any[]) => any));
9
9
  dispose(): void;
10
10
  update(value: unknown): void;
11
11
  }
@@ -1,4 +1,5 @@
1
1
  import { effect } from '@esportsplus/reactivity';
2
+ import { isAsyncFunction } from '@esportsplus/utilities';
2
3
  import { raf, text } from '../utilities.js';
3
4
  import { remove } from './cleanup.js';
4
5
  import render from './render.js';
@@ -18,37 +19,47 @@ class EffectSlot {
18
19
  scheduled = false;
19
20
  textnode = null;
20
21
  constructor(anchor, fn) {
21
- let dispose = fn.length ? () => this.dispose() : undefined, value;
22
22
  this.anchor = anchor;
23
- this.disposer = effect(() => {
24
- value = read(fn(dispose));
25
- if (!this.disposer) {
26
- this.update(value);
27
- }
28
- else if (!this.scheduled) {
29
- this.scheduled = true;
30
- raf(() => {
31
- this.scheduled = false;
23
+ this.disposer = null;
24
+ if (isAsyncFunction(fn)) {
25
+ fn((content) => this.update(content)).then((value) => this.update(value), () => { });
26
+ }
27
+ else {
28
+ let dispose = fn.length ? () => this.dispose() : undefined, value;
29
+ this.disposer = effect(() => {
30
+ value = read(fn(dispose));
31
+ if (!this.disposer) {
32
32
  this.update(value);
33
- });
34
- }
35
- });
33
+ }
34
+ else if (!this.scheduled) {
35
+ this.scheduled = true;
36
+ raf(() => {
37
+ this.scheduled = false;
38
+ this.update(value);
39
+ });
40
+ }
41
+ });
42
+ }
36
43
  }
37
44
  dispose() {
38
- let { anchor, group, textnode } = this;
45
+ let { anchor, disposer, group, textnode } = this;
46
+ if (!disposer) {
47
+ return;
48
+ }
39
49
  if (textnode) {
40
50
  group = { head: anchor, tail: textnode };
41
51
  }
42
52
  else if (group) {
43
53
  group.head = anchor;
44
54
  }
45
- this.disposer();
55
+ disposer();
46
56
  if (group) {
47
57
  remove(group);
48
58
  }
49
59
  }
50
60
  update(value) {
51
61
  let { anchor, group, textnode } = this;
62
+ value = read(value);
52
63
  if (group) {
53
64
  remove(group);
54
65
  this.group = null;
@@ -68,14 +79,22 @@ class EffectSlot {
68
79
  }
69
80
  }
70
81
  else {
71
- let fragment = render(anchor, value), head = fragment.firstChild;
82
+ let fragment = render(anchor, value), head, tail;
83
+ if (fragment.nodeType === 11) {
84
+ head = fragment.firstChild;
85
+ tail = fragment.lastChild;
86
+ }
87
+ else {
88
+ head = fragment;
89
+ tail = fragment;
90
+ }
72
91
  if (textnode?.isConnected) {
73
92
  remove({ head: textnode, tail: textnode });
74
93
  }
75
94
  if (head) {
76
95
  this.group = {
77
96
  head: head,
78
- tail: fragment.lastChild
97
+ tail: tail
79
98
  };
80
99
  anchor.after(fragment);
81
100
  }
@@ -1,7 +1,6 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
2
  import { ARRAY_SLOT } from '../constants.js';
3
- import { clone, fragment, text } from '../utilities.js';
4
- const EMPTY_FRAGMENT = fragment('');
3
+ import { clone, EMPTY_FRAGMENT, text } from '../utilities.js';
5
4
  export default function render(anchor, value) {
6
5
  if (value == null || value === false || value === '') {
7
6
  return EMPTY_FRAGMENT;
@@ -1,7 +1,8 @@
1
1
  declare const clone: <T extends Node>(node: T, options?: boolean | ImportNodeOptions) => T;
2
2
  declare const fragment: (html: string) => DocumentFragment;
3
+ declare const EMPTY_FRAGMENT: DocumentFragment;
3
4
  declare const marker: ChildNode;
4
5
  declare const raf: typeof requestAnimationFrame;
5
6
  declare const template: (html: string) => () => DocumentFragment;
6
7
  declare const text: (value: string) => Node;
7
- export { clone, fragment, template, marker, raf, text };
8
+ export { clone, EMPTY_FRAGMENT, fragment, marker, raf, template, text };
@@ -8,6 +8,7 @@ const fragment = (html) => {
8
8
  element.innerHTML = html;
9
9
  return element.content;
10
10
  };
11
+ const EMPTY_FRAGMENT = fragment('');
11
12
  const marker = fragment(SLOT_HTML).firstChild;
12
13
  const raf = globalThis?.requestAnimationFrame;
13
14
  const template = (html) => {
@@ -28,4 +29,4 @@ const text = (value) => {
28
29
  }
29
30
  return element;
30
31
  };
31
- export { clone, fragment, template, marker, raf, text };
32
+ export { clone, EMPTY_FRAGMENT, fragment, marker, raf, template, text };