@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.
- package/README.md +33 -1
- package/bench/runtime.bench.ts +207 -0
- package/build/attributes.js +4 -1
- package/build/compiler/plugins/vite.d.ts +8 -4
- package/build/compiler/plugins/vite.js +37 -2
- package/build/hmr.d.ts +10 -0
- package/build/hmr.js +42 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/slot/array.js +69 -4
- package/build/slot/effect.d.ts +3 -3
- package/build/slot/effect.js +36 -17
- package/build/slot/render.js +1 -2
- package/build/utilities.d.ts +2 -1
- package/build/utilities.js +2 -1
- package/llm.txt +63 -4
- package/package.json +2 -1
- package/src/attributes.ts +4 -1
- package/src/compiler/plugins/vite.ts +74 -5
- package/src/hmr.ts +70 -0
- package/src/index.ts +1 -0
- package/src/slot/array.ts +104 -4
- package/src/slot/effect.ts +46 -20
- package/src/slot/render.ts +1 -4
- package/src/utilities.ts +3 -1
- package/{test/attributes.test.ts → tests/attributes.ts} +3 -2
- package/tests/compiler/codegen.ts +292 -0
- package/tests/compiler/integration.ts +252 -0
- package/tests/compiler/ts-parser.ts +160 -0
- package/tests/compiler/vite-hmr.ts +126 -0
- package/{test/constants.test.ts → tests/constants.ts} +5 -1
- package/tests/event/onconnect.ts +147 -0
- package/tests/event/onresize.ts +187 -0
- package/tests/event/ontick.ts +273 -0
- package/tests/hmr.ts +146 -0
- package/{test/slot/array.test.ts → tests/slot/array.ts} +475 -0
- package/tests/slot/async.ts +389 -0
- package/vitest.bench.config.ts +18 -0
- package/vitest.config.ts +1 -1
- package/storage/compiler-architecture-2026-01-13.md +0 -420
- /package/{test → examples}/index.ts +0 -0
- /package/{test → examples}/vite.config.ts +0 -0
- /package/{test/compiler/parser.test.ts → tests/compiler/parser.ts} +0 -0
- /package/{test/compiler/ts-analyzer.test.ts → tests/compiler/ts-analyzer.ts} +0 -0
- /package/{test → tests}/dist/test.js +0 -0
- /package/{test → tests}/dist/test.js.map +0 -0
- /package/{test/event/index.test.ts → tests/event/index.ts} +0 -0
- /package/{test/html.test.ts → tests/html.ts} +0 -0
- /package/{test/render.test.ts → tests/render.ts} +0 -0
- /package/{test/slot/cleanup.test.ts → tests/slot/cleanup.ts} +0 -0
- /package/{test/slot/effect.test.ts → tests/slot/effect.ts} +0 -0
- /package/{test/slot/index.test.ts → tests/slot/index.ts} +0 -0
- /package/{test/slot/render.test.ts → tests/slot/render.ts} +0 -0
- /package/{test/svg.test.ts → tests/svg.ts} +0 -0
- /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.
|
|
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'
|
|
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
|
-
|
|
3
|
+
|
|
3
4
|
import reactivity from '@esportsplus/reactivity/compiler';
|
|
4
5
|
import template from '..';
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
package/src/slot/effect.ts
CHANGED
|
@@ -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: (
|
|
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 =
|
|
35
|
-
value = read( fn(dispose) );
|
|
32
|
+
this.disposer = null;
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
130
|
+
tail: tail as Element
|
|
105
131
|
};
|
|
106
132
|
|
|
107
133
|
anchor.after(fragment);
|
package/src/slot/render.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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.
|
|
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.
|
|
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', () => {
|