@esportsplus/template 0.28.3 → 0.29.2
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 +431 -0
- package/build/attributes.d.ts +7 -1
- package/build/attributes.js +86 -33
- package/build/constants.d.ts +3 -11
- package/build/constants.js +4 -32
- package/build/event/constants.d.ts +3 -0
- package/build/event/constants.js +13 -0
- package/build/event/index.d.ts +9 -1
- package/build/event/index.js +29 -35
- package/build/event/ontick.js +6 -9
- package/build/html.d.ts +9 -0
- package/build/html.js +7 -0
- package/build/index.d.ts +8 -2
- package/build/index.js +8 -1
- package/build/render.d.ts +2 -2
- package/build/render.js +2 -3
- package/build/runtime.d.ts +1 -0
- package/build/runtime.js +5 -0
- package/build/slot/array.d.ts +3 -3
- package/build/slot/array.js +11 -14
- package/build/slot/cleanup.d.ts +1 -1
- package/build/slot/cleanup.js +1 -2
- package/build/slot/effect.js +5 -7
- package/build/slot/index.js +1 -7
- package/build/slot/render.js +6 -8
- package/build/svg.d.ts +1 -1
- package/build/svg.js +1 -1
- package/build/transformer/codegen.d.ts +18 -0
- package/build/transformer/codegen.js +316 -0
- package/build/transformer/index.d.ts +12 -0
- package/build/transformer/index.js +62 -0
- package/build/transformer/parser.d.ts +18 -0
- package/build/transformer/parser.js +166 -0
- package/build/transformer/plugins/esbuild.d.ts +5 -0
- package/build/transformer/plugins/esbuild.js +35 -0
- package/build/transformer/plugins/tsc.d.ts +3 -0
- package/build/transformer/plugins/tsc.js +4 -0
- package/build/transformer/plugins/vite.d.ts +5 -0
- package/build/transformer/plugins/vite.js +37 -0
- package/build/transformer/ts-parser.d.ts +21 -0
- package/build/transformer/ts-parser.js +72 -0
- package/build/transformer/type-analyzer.d.ts +7 -0
- package/build/transformer/type-analyzer.js +230 -0
- package/build/types.d.ts +2 -3
- package/build/utilities.d.ts +7 -0
- package/build/utilities.js +31 -0
- package/package.json +33 -4
- package/src/attributes.ts +115 -51
- package/src/constants.ts +6 -53
- package/src/event/constants.ts +16 -0
- package/src/event/index.ts +36 -42
- package/src/event/onconnect.ts +1 -1
- package/src/event/onresize.ts +1 -1
- package/src/event/ontick.ts +7 -11
- package/src/html.ts +18 -0
- package/src/index.ts +8 -2
- package/src/render.ts +6 -7
- package/src/runtime.ts +8 -0
- package/src/slot/array.ts +18 -24
- package/src/slot/cleanup.ts +3 -4
- package/src/slot/effect.ts +6 -8
- package/src/slot/index.ts +2 -8
- package/src/slot/render.ts +7 -9
- package/src/svg.ts +1 -1
- package/src/transformer/codegen.ts +518 -0
- package/src/transformer/index.ts +98 -0
- package/src/transformer/parser.ts +239 -0
- package/src/transformer/plugins/esbuild.ts +46 -0
- package/src/transformer/plugins/tsc.ts +7 -0
- package/src/transformer/plugins/vite.ts +49 -0
- package/src/transformer/ts-parser.ts +123 -0
- package/src/transformer/type-analyzer.ts +334 -0
- package/src/types.ts +3 -4
- package/src/utilities.ts +52 -0
- package/storage/rewrite-analysis-2026-01-04.md +439 -0
- package/test/constants.ts +69 -0
- package/test/effects.ts +237 -0
- package/test/events.ts +318 -0
- package/test/imported-values.ts +253 -0
- package/test/nested.ts +298 -0
- package/test/slots.ts +259 -0
- package/test/spread.ts +290 -0
- package/test/static.ts +118 -0
- package/test/templates.ts +473 -0
- package/test/tsconfig.json +17 -0
- package/test/vite.config.ts +50 -0
- package/build/html/index.d.ts +0 -9
- package/build/html/index.js +0 -29
- package/build/html/parser.d.ts +0 -5
- package/build/html/parser.js +0 -165
- package/build/utilities/element.d.ts +0 -11
- package/build/utilities/element.js +0 -9
- package/build/utilities/fragment.d.ts +0 -3
- package/build/utilities/fragment.js +0 -10
- package/build/utilities/marker.d.ts +0 -2
- package/build/utilities/marker.js +0 -4
- package/build/utilities/node.d.ts +0 -9
- package/build/utilities/node.js +0 -10
- package/build/utilities/raf.d.ts +0 -2
- package/build/utilities/raf.js +0 -1
- package/build/utilities/text.d.ts +0 -2
- package/build/utilities/text.js +0 -9
- package/src/html/index.ts +0 -48
- package/src/html/parser.ts +0 -235
- package/src/utilities/element.ts +0 -28
- package/src/utilities/fragment.ts +0 -19
- package/src/utilities/marker.ts +0 -6
- package/src/utilities/node.ts +0 -29
- package/src/utilities/raf.ts +0 -1
- package/src/utilities/text.ts +0 -15
package/README.md
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# @esportsplus/template
|
|
2
|
+
|
|
3
|
+
High-performance, compiler-optimized HTML templating library for JavaScript/TypeScript. Templates are transformed at build time into optimized DOM manipulation code with zero runtime parsing overhead.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Compile-time transformation** - Templates converted to optimized code during build
|
|
8
|
+
- **Zero runtime parsing** - No template parsing at runtime
|
|
9
|
+
- **Reactive integration** - Works with `@esportsplus/reactivity` for dynamic updates
|
|
10
|
+
- **Event delegation** - Efficient event handling with automatic delegation
|
|
11
|
+
- **Lifecycle events** - `onconnect`, `ondisconnect`, `onrender`, `onresize`, `ontick`
|
|
12
|
+
- **Type-safe** - Full TypeScript support
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @esportsplus/template
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Transformer Plugins
|
|
21
|
+
|
|
22
|
+
The library requires a build-time transformer to convert template literals into optimized code. Choose the plugin for your build tool:
|
|
23
|
+
|
|
24
|
+
### Vite
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// vite.config.ts
|
|
28
|
+
import { defineConfig } from 'vite';
|
|
29
|
+
import templatePlugin from '@esportsplus/template/plugins/vite';
|
|
30
|
+
|
|
31
|
+
export default defineConfig({
|
|
32
|
+
plugins: [
|
|
33
|
+
templatePlugin()
|
|
34
|
+
]
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Options:**
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
templatePlugin({
|
|
42
|
+
root: string // Optional: project root (defaults to config.root)
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### ESBuild
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// esbuild.config.ts
|
|
50
|
+
import * as esbuild from 'esbuild';
|
|
51
|
+
import templatePlugin from '@esportsplus/template/plugins/esbuild';
|
|
52
|
+
|
|
53
|
+
await esbuild.build({
|
|
54
|
+
entryPoints: ['src/index.ts'],
|
|
55
|
+
bundle: true,
|
|
56
|
+
outfile: 'dist/bundle.js',
|
|
57
|
+
plugins: [
|
|
58
|
+
templatePlugin()
|
|
59
|
+
]
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Options:**
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
templatePlugin({
|
|
67
|
+
root: string // Optional: project root (defaults to process.cwd())
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### TypeScript Compiler (tsc)
|
|
72
|
+
|
|
73
|
+
For direct `tsc` compilation, use the transformer factory:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// tsconfig.json (with ts-patch or ttypescript)
|
|
77
|
+
{
|
|
78
|
+
"compilerOptions": {
|
|
79
|
+
"plugins": [
|
|
80
|
+
{ "transform": "@esportsplus/template/plugins/tsc" }
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or programmatically:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import ts from 'typescript';
|
|
90
|
+
import templateTransformer from '@esportsplus/template/plugins/tsc';
|
|
91
|
+
|
|
92
|
+
const program = ts.createProgram(['src/index.ts'], {});
|
|
93
|
+
const result = ts.emit(program, undefined, undefined, false, {
|
|
94
|
+
before: [templateTransformer(program)]
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Basic Usage
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { html, render } from '@esportsplus/template';
|
|
102
|
+
|
|
103
|
+
// Static template
|
|
104
|
+
const greeting = html`<div class="greeting">Hello World</div>`;
|
|
105
|
+
|
|
106
|
+
// Dynamic text
|
|
107
|
+
const message = (text: string) => html`<div>${text}</div>`;
|
|
108
|
+
|
|
109
|
+
// Dynamic attributes
|
|
110
|
+
const button = (cls: string) => html`<button class="${cls}">Click</button>`;
|
|
111
|
+
|
|
112
|
+
// Render to DOM
|
|
113
|
+
render(document.body, greeting);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Template Syntax
|
|
117
|
+
|
|
118
|
+
### Text Slots
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Basic text interpolation
|
|
122
|
+
const text = (value: string) => html`<span>${value}</span>`;
|
|
123
|
+
|
|
124
|
+
// Multiple slots
|
|
125
|
+
const multi = (a: string, b: string) => html`<p>${a} and ${b}</p>`;
|
|
126
|
+
|
|
127
|
+
// Adjacent slots
|
|
128
|
+
const adjacent = (a: string, b: string, c: string) => html`<div>${a}${b}${c}</div>`;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Attribute Slots
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// Class
|
|
135
|
+
const dynamic = (cls: string) => html`<div class="${cls}"></div>`;
|
|
136
|
+
|
|
137
|
+
// Mixed static and dynamic
|
|
138
|
+
const mixed = (dynamic: string) => html`<div class="base ${dynamic}"></div>`;
|
|
139
|
+
|
|
140
|
+
// Multiple attributes
|
|
141
|
+
const attrs = (id: string, cls: string, style: string) =>
|
|
142
|
+
html`<div id="${id}" class="${cls}" style="${style}"></div>`;
|
|
143
|
+
|
|
144
|
+
// Data attributes
|
|
145
|
+
const data = (value: string) => html`<div data-value="${value}"></div>`;
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Spread Attributes
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// Spread object as attributes
|
|
152
|
+
const spread = (attrs: Record<string, unknown>) => html`<div ${attrs}></div>`;
|
|
153
|
+
|
|
154
|
+
// With static attributes
|
|
155
|
+
const spreadMixed = (attrs: Record<string, unknown>) =>
|
|
156
|
+
html`<div class="base" ${attrs}></div>`;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Nested Templates
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Map to nested templates
|
|
163
|
+
const list = (items: string[]) =>
|
|
164
|
+
html`<ul>${items.map(item => html`<li>${item}</li>`)}</ul>`;
|
|
165
|
+
|
|
166
|
+
// Deeply nested
|
|
167
|
+
const nested = (sections: { title: string; items: string[] }[]) =>
|
|
168
|
+
html`<div>
|
|
169
|
+
${sections.map(section => html`
|
|
170
|
+
<section>
|
|
171
|
+
<h2>${section.title}</h2>
|
|
172
|
+
<ul>${section.items.map(item => html`<li>${item}</li>`)}</ul>
|
|
173
|
+
</section>
|
|
174
|
+
`)}
|
|
175
|
+
</div>`;
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Conditional Rendering
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// Conditional class
|
|
182
|
+
const toggle = (active: boolean) =>
|
|
183
|
+
html`<div class="${active ? 'active' : 'inactive'}"></div>`;
|
|
184
|
+
|
|
185
|
+
// Conditional content
|
|
186
|
+
const conditional = (show: boolean, content: string) =>
|
|
187
|
+
html`<div>${show ? content : ''}</div>`;
|
|
188
|
+
|
|
189
|
+
// Conditional template
|
|
190
|
+
const conditionalTemplate = (items: string[] | null) =>
|
|
191
|
+
html`<div>
|
|
192
|
+
${items ? html`<ul>${items.map(i => html`<li>${i}</li>`)}</ul>` : html`<p>No items</p>`}
|
|
193
|
+
</div>`;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Reactive Bindings
|
|
197
|
+
|
|
198
|
+
### Effect Slots
|
|
199
|
+
|
|
200
|
+
Function expressions automatically become reactive effect slots:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { html } from '@esportsplus/template';
|
|
204
|
+
|
|
205
|
+
// Arrow function = reactive
|
|
206
|
+
const counter = (state: { count: number }) =>
|
|
207
|
+
html`<div>${() => state.count}</div>`;
|
|
208
|
+
|
|
209
|
+
// Reactive class
|
|
210
|
+
const toggle = (state: { active: boolean }) =>
|
|
211
|
+
html`<div class="${() => state.active ? 'on' : 'off'}"></div>`;
|
|
212
|
+
|
|
213
|
+
// Reactive style
|
|
214
|
+
const progress = (state: { width: number }) =>
|
|
215
|
+
html`<div style="${() => `width: ${state.width}px`}"></div>`;
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Array Slots
|
|
219
|
+
|
|
220
|
+
For reactive arrays, use `html.reactive`:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { html } from '@esportsplus/template';
|
|
224
|
+
|
|
225
|
+
const todoList = (todos: string[]) =>
|
|
226
|
+
html`<ul>${html.reactive(todos, todo => html`<li>${todo}</li>`)}</ul>`;
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Events
|
|
230
|
+
|
|
231
|
+
### Standard DOM Events
|
|
232
|
+
|
|
233
|
+
Events use delegation by default for efficiency:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// Click
|
|
237
|
+
const button = (handler: () => void) =>
|
|
238
|
+
html`<button onclick="${handler}">Click</button>`;
|
|
239
|
+
|
|
240
|
+
// Keyboard
|
|
241
|
+
const input = (handler: (e: KeyboardEvent) => void) =>
|
|
242
|
+
html`<input onkeydown="${handler}">`;
|
|
243
|
+
|
|
244
|
+
// Input
|
|
245
|
+
const textbox = (handler: (e: Event) => void) =>
|
|
246
|
+
html`<input oninput="${handler}">`;
|
|
247
|
+
|
|
248
|
+
// Multiple events
|
|
249
|
+
const interactive = (click: () => void, hover: () => void) =>
|
|
250
|
+
html`<div onclick="${click}" onmouseenter="${hover}">Interact</div>`;
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Lifecycle Events
|
|
254
|
+
|
|
255
|
+
Custom lifecycle events for DOM attachment:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// Called when element is added to DOM
|
|
259
|
+
const connect = (handler: (el: HTMLElement) => void) =>
|
|
260
|
+
html`<div onconnect="${handler}">Connected</div>`;
|
|
261
|
+
|
|
262
|
+
// Called when element is removed from DOM
|
|
263
|
+
const disconnect = (handler: (el: HTMLElement) => void) =>
|
|
264
|
+
html`<div ondisconnect="${handler}">Will disconnect</div>`;
|
|
265
|
+
|
|
266
|
+
// Called after template renders
|
|
267
|
+
const render = (handler: (el: HTMLElement) => void) =>
|
|
268
|
+
html`<div onrender="${handler}">Rendered</div>`;
|
|
269
|
+
|
|
270
|
+
// Called on element resize
|
|
271
|
+
const resize = (handler: (el: HTMLElement) => void) =>
|
|
272
|
+
html`<div onresize="${handler}">Resizable</div>`;
|
|
273
|
+
|
|
274
|
+
// Called on animation frame (with dispose function)
|
|
275
|
+
const tick = (handler: (dispose: () => void, el: HTMLElement) => void) =>
|
|
276
|
+
html`<div ontick="${handler}">Animating</div>`;
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Direct Attach Events
|
|
280
|
+
|
|
281
|
+
Some events don't bubble and are attached directly:
|
|
282
|
+
|
|
283
|
+
- `onfocus`, `onblur`, `onfocusin`, `onfocusout`
|
|
284
|
+
- `onload`, `onerror`
|
|
285
|
+
- `onplay`, `onpause`, `onended`, `ontimeupdate`
|
|
286
|
+
- `onscroll`
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
const input = (focus: () => void, blur: () => void) =>
|
|
290
|
+
html`<input onfocus="${focus}" onblur="${blur}">`;
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## SVG Support
|
|
294
|
+
|
|
295
|
+
Use `svg` for SVG templates:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { svg } from '@esportsplus/template';
|
|
299
|
+
|
|
300
|
+
const circle = (fill: string) =>
|
|
301
|
+
svg`<svg width="100" height="100">
|
|
302
|
+
<circle cx="50" cy="50" r="40" fill="${fill}"/>
|
|
303
|
+
</svg>`;
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## API Reference
|
|
307
|
+
|
|
308
|
+
### Exports
|
|
309
|
+
|
|
310
|
+
| Export | Description |
|
|
311
|
+
|--------|-------------|
|
|
312
|
+
| `html` | Template literal tag for HTML |
|
|
313
|
+
| `svg` | Template literal tag for SVG |
|
|
314
|
+
| `render` | Mount renderable to DOM element |
|
|
315
|
+
| `attributes` | Attribute manipulation utilities |
|
|
316
|
+
| `event` | Event system |
|
|
317
|
+
| `slot` | Slot rendering |
|
|
318
|
+
| `ArraySlot` | Reactive array rendering |
|
|
319
|
+
| `EffectSlot` | Reactive effect rendering |
|
|
320
|
+
|
|
321
|
+
### Types
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
type Renderable = DocumentFragment | Node | string | number | null | undefined;
|
|
325
|
+
type Element = HTMLElement & { [STORE]?: Record<string, unknown> };
|
|
326
|
+
type Attributes = Record<string, unknown>;
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### render(parent, renderable)
|
|
330
|
+
|
|
331
|
+
Mounts a renderable to a parent element.
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { html, render } from '@esportsplus/template';
|
|
335
|
+
|
|
336
|
+
const app = html`<div>App</div>`;
|
|
337
|
+
render(document.getElementById('root'), app);
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Complete Example
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
import { html, render } from '@esportsplus/template';
|
|
344
|
+
|
|
345
|
+
type Todo = { id: number; text: string; done: boolean };
|
|
346
|
+
|
|
347
|
+
const TodoItem = (todo: Todo, onToggle: () => void, onDelete: () => void) => html`
|
|
348
|
+
<li class="${() => todo.done ? 'completed' : ''}">
|
|
349
|
+
<input type="checkbox"
|
|
350
|
+
checked="${() => todo.done}"
|
|
351
|
+
onchange="${onToggle}">
|
|
352
|
+
<span>${todo.text}</span>
|
|
353
|
+
<button onclick="${onDelete}">Delete</button>
|
|
354
|
+
</li>
|
|
355
|
+
`;
|
|
356
|
+
|
|
357
|
+
const TodoApp = (state: {
|
|
358
|
+
todos: Todo[];
|
|
359
|
+
input: string;
|
|
360
|
+
addTodo: () => void;
|
|
361
|
+
toggleTodo: (id: number) => void;
|
|
362
|
+
deleteTodo: (id: number) => void;
|
|
363
|
+
setInput: (value: string) => void;
|
|
364
|
+
}) => html`
|
|
365
|
+
<div class="todo-app">
|
|
366
|
+
<h1>Todos</h1>
|
|
367
|
+
<form onsubmit="${(e: Event) => { e.preventDefault(); state.addTodo(); }}">
|
|
368
|
+
<input type="text"
|
|
369
|
+
value="${() => state.input}"
|
|
370
|
+
oninput="${(e: Event) => state.setInput((e.target as HTMLInputElement).value)}"
|
|
371
|
+
placeholder="Add todo...">
|
|
372
|
+
<button type="submit">Add</button>
|
|
373
|
+
</form>
|
|
374
|
+
<ul>
|
|
375
|
+
${html.reactive(state.todos, todo =>
|
|
376
|
+
TodoItem(
|
|
377
|
+
todo,
|
|
378
|
+
() => state.toggleTodo(todo.id),
|
|
379
|
+
() => state.deleteTodo(todo.id)
|
|
380
|
+
)
|
|
381
|
+
)}
|
|
382
|
+
</ul>
|
|
383
|
+
</div>
|
|
384
|
+
`;
|
|
385
|
+
|
|
386
|
+
// Mount
|
|
387
|
+
render(document.body, TodoApp(/* state */));
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## How Transformation Works
|
|
391
|
+
|
|
392
|
+
The transformer converts template literals into optimized code at build time:
|
|
393
|
+
|
|
394
|
+
**Input:**
|
|
395
|
+
```typescript
|
|
396
|
+
const greeting = (name: string) => html`<div class="hello">${name}</div>`;
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Output (simplified):**
|
|
400
|
+
```typescript
|
|
401
|
+
const $template = (() => {
|
|
402
|
+
let cached;
|
|
403
|
+
return () => {
|
|
404
|
+
if (!cached) cached = template('<div class="hello"><!--$--></div>');
|
|
405
|
+
return clone(cached);
|
|
406
|
+
};
|
|
407
|
+
})();
|
|
408
|
+
|
|
409
|
+
const greeting = (name: string) => {
|
|
410
|
+
const fragment = $template();
|
|
411
|
+
const $0 = firstChild(fragment);
|
|
412
|
+
slot($0.firstChild, name);
|
|
413
|
+
return fragment;
|
|
414
|
+
};
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Key optimizations:
|
|
418
|
+
- Template HTML is parsed once and cached
|
|
419
|
+
- Slot positions are computed at compile time
|
|
420
|
+
- Type analysis determines optimal slot binding
|
|
421
|
+
- No runtime template parsing
|
|
422
|
+
|
|
423
|
+
## Dependencies
|
|
424
|
+
|
|
425
|
+
- `@esportsplus/reactivity` - Reactive primitives
|
|
426
|
+
- `@esportsplus/queue` - Object pooling
|
|
427
|
+
- `@esportsplus/utilities` - Utility functions
|
|
428
|
+
|
|
429
|
+
## License
|
|
430
|
+
|
|
431
|
+
MIT
|
package/build/attributes.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { Attributes, Element } from './types.js';
|
|
2
2
|
declare const set: (element: Element, name: string, value: unknown) => void;
|
|
3
|
+
declare const setClass: (element: Element, classlist: false | string | undefined, value: unknown) => void;
|
|
4
|
+
declare const setProperty: (element: Element, name: string, value: unknown) => void;
|
|
5
|
+
declare const setStyle: (element: Element, styles: false | string | undefined, value: unknown) => void;
|
|
3
6
|
declare const spread: (element: Element, value: Attributes | Attributes[]) => void;
|
|
4
7
|
declare const _default: {
|
|
5
8
|
set: (element: Element, name: string, value: unknown) => void;
|
|
9
|
+
setClass: (element: Element, classlist: false | string | undefined, value: unknown) => void;
|
|
10
|
+
setProperty: (element: Element, name: string, value: unknown) => void;
|
|
11
|
+
setStyle: (element: Element, styles: false | string | undefined, value: unknown) => void;
|
|
6
12
|
spread: (element: Element, value: Attributes | Attributes[]) => void;
|
|
7
13
|
};
|
|
8
14
|
export default _default;
|
|
9
|
-
export { set, spread };
|
|
15
|
+
export { set, setClass, setProperty, setStyle, spread };
|
package/build/attributes.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { effect } from '@esportsplus/reactivity';
|
|
2
2
|
import { isArray, isObject } from '@esportsplus/utilities';
|
|
3
|
-
import { STATE_HYDRATING, STATE_NONE, STATE_WAITING } from './constants.js';
|
|
4
|
-
import {
|
|
3
|
+
import { DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, STATE_HYDRATING, STATE_NONE, STATE_WAITING } from './constants.js';
|
|
4
|
+
import { raf } from './utilities.js';
|
|
5
5
|
import q from '@esportsplus/queue';
|
|
6
|
-
import raf from './utilities/raf.js';
|
|
7
6
|
import event from './event/index.js';
|
|
8
7
|
const STORE = Symbol();
|
|
9
8
|
let delimiters = {
|
|
@@ -12,13 +11,13 @@ let delimiters = {
|
|
|
12
11
|
}, queue = q(64), scheduled = false;
|
|
13
12
|
function apply(element, name, value) {
|
|
14
13
|
if (value == null || value === false || value === '') {
|
|
15
|
-
removeAttribute
|
|
14
|
+
element.removeAttribute(name);
|
|
16
15
|
}
|
|
17
16
|
else if (name === 'class') {
|
|
18
|
-
className
|
|
17
|
+
element.className = value;
|
|
19
18
|
}
|
|
20
19
|
else if (name === 'style' || (name[0] === 'd' && name.startsWith('data-')) || element['ownerSVGElement']) {
|
|
21
|
-
setAttribute
|
|
20
|
+
element.setAttribute(name, value);
|
|
22
21
|
}
|
|
23
22
|
else {
|
|
24
23
|
element[name] = value;
|
|
@@ -31,7 +30,7 @@ function list(ctx, element, id, name, state, value) {
|
|
|
31
30
|
if (value == null || value === false || value === '') {
|
|
32
31
|
value = '';
|
|
33
32
|
}
|
|
34
|
-
let base = name + '.static', delimiter = delimiters[name], store = (ctx ??= context(element)).store ??= {}, dynamic = store[name];
|
|
33
|
+
let base = name + '.static', changed = false, delimiter = delimiters[name], store = (ctx ??= context(element)).store ??= {}, dynamic = store[name];
|
|
35
34
|
if (dynamic === undefined) {
|
|
36
35
|
let value = (element.getAttribute(name) || '').trim();
|
|
37
36
|
store[base] = value;
|
|
@@ -39,10 +38,15 @@ function list(ctx, element, id, name, state, value) {
|
|
|
39
38
|
}
|
|
40
39
|
if (id === null) {
|
|
41
40
|
if (value && typeof value === 'string') {
|
|
41
|
+
changed = true;
|
|
42
42
|
store[base] += (store[base] ? delimiter : '') + value;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
else {
|
|
46
|
+
if (store[id + '.raw'] === value) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
store[id + '.raw'] = value;
|
|
46
50
|
let hot = {};
|
|
47
51
|
if (value && typeof value === 'string') {
|
|
48
52
|
let part, parts = value.split(delimiter);
|
|
@@ -51,7 +55,10 @@ function list(ctx, element, id, name, state, value) {
|
|
|
51
55
|
if (part === '') {
|
|
52
56
|
continue;
|
|
53
57
|
}
|
|
54
|
-
dynamic.
|
|
58
|
+
if (!dynamic.has(part)) {
|
|
59
|
+
changed = true;
|
|
60
|
+
dynamic.add(part);
|
|
61
|
+
}
|
|
55
62
|
hot[part] = true;
|
|
56
63
|
}
|
|
57
64
|
}
|
|
@@ -61,11 +68,15 @@ function list(ctx, element, id, name, state, value) {
|
|
|
61
68
|
if (hot[part] === true) {
|
|
62
69
|
continue;
|
|
63
70
|
}
|
|
71
|
+
changed = true;
|
|
64
72
|
dynamic.delete(part);
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
store[id] = hot;
|
|
68
76
|
}
|
|
77
|
+
if (!changed) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
69
80
|
value = store[base];
|
|
70
81
|
for (let key of dynamic) {
|
|
71
82
|
value += (value ? delimiter : '') + key;
|
|
@@ -95,6 +106,26 @@ function property(ctx, element, id, name, state, value) {
|
|
|
95
106
|
schedule(ctx, element, name, state, value);
|
|
96
107
|
}
|
|
97
108
|
}
|
|
109
|
+
function reactive(element, name, state, value) {
|
|
110
|
+
let ctx = context(element), fn = name === 'class' || name === 'style' ? list : property;
|
|
111
|
+
ctx.effect ??= 0;
|
|
112
|
+
let id = ctx.effect++;
|
|
113
|
+
effect(() => {
|
|
114
|
+
let v = value(element);
|
|
115
|
+
if (v == null || typeof v !== 'object') {
|
|
116
|
+
fn(ctx, element, id, name, state, v);
|
|
117
|
+
}
|
|
118
|
+
else if (isArray(v)) {
|
|
119
|
+
let last = v.length - 1;
|
|
120
|
+
for (let i = 0, n = v.length; i < n; i++) {
|
|
121
|
+
fn(ctx, element, id, name, state === STATE_HYDRATING
|
|
122
|
+
? state
|
|
123
|
+
: i !== last ? STATE_WAITING : state, v[i]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
state = STATE_NONE;
|
|
128
|
+
}
|
|
98
129
|
function schedule(ctx, element, name, state, value) {
|
|
99
130
|
ctx ??= context(element);
|
|
100
131
|
(ctx.updates ??= {})[name] = value;
|
|
@@ -126,32 +157,25 @@ function task() {
|
|
|
126
157
|
}
|
|
127
158
|
}
|
|
128
159
|
const set = (element, name, value) => {
|
|
129
|
-
let fn = name === 'class' || name === 'style' ? list : property, state = STATE_HYDRATING;
|
|
130
160
|
if (typeof value === 'function') {
|
|
131
161
|
if (name[0] === 'o' && name[1] === 'n') {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
ctx.effect ??= 0;
|
|
136
|
-
let id = ctx.effect++;
|
|
137
|
-
effect(() => {
|
|
138
|
-
let v = value(element);
|
|
139
|
-
if (v == null || typeof v !== 'object') {
|
|
140
|
-
fn(ctx, element, id, name, state, v);
|
|
162
|
+
let e = name.slice(2).toLowerCase(), key = name.toLowerCase();
|
|
163
|
+
if (LIFECYCLE_EVENTS.has(key)) {
|
|
164
|
+
event[key](element, value);
|
|
141
165
|
}
|
|
142
|
-
else if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
: i !== last ? STATE_WAITING : state, v[i]);
|
|
148
|
-
}
|
|
166
|
+
else if (DIRECT_ATTACH_EVENTS.has(key)) {
|
|
167
|
+
event.direct(element, e, value);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
event.delegate(element, e, value);
|
|
149
171
|
}
|
|
150
|
-
}
|
|
151
|
-
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
reactive(element, name, STATE_HYDRATING, value);
|
|
175
|
+
}
|
|
152
176
|
return;
|
|
153
177
|
}
|
|
154
|
-
if (typeof value !== 'object') {
|
|
178
|
+
else if (typeof value !== 'object') {
|
|
155
179
|
}
|
|
156
180
|
else if (isArray(value)) {
|
|
157
181
|
for (let i = 0, n = value.length; i < n; i++) {
|
|
@@ -163,12 +187,41 @@ const set = (element, name, value) => {
|
|
|
163
187
|
}
|
|
164
188
|
return;
|
|
165
189
|
}
|
|
166
|
-
|
|
190
|
+
(name === 'class' || name === 'style' ? list : property)(null, element, null, name, STATE_HYDRATING, value);
|
|
191
|
+
};
|
|
192
|
+
const setClass = (element, classlist, value) => {
|
|
193
|
+
let ctx = context(element), store = ctx.store ??= {};
|
|
194
|
+
store['class.static'] = classlist || '';
|
|
195
|
+
store['class'] ??= new Set();
|
|
196
|
+
if (typeof value === 'function') {
|
|
197
|
+
reactive(element, 'class', STATE_HYDRATING, value);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
list(ctx, element, null, 'class', STATE_HYDRATING, value);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const setProperty = (element, name, value) => {
|
|
204
|
+
if (typeof value === 'function') {
|
|
205
|
+
reactive(element, name, STATE_HYDRATING, value);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
property(null, element, null, name, STATE_HYDRATING, value);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const setStyle = (element, styles, value) => {
|
|
212
|
+
let ctx = context(element), store = ctx.store ??= {};
|
|
213
|
+
store['style.static'] = styles || '';
|
|
214
|
+
store['style'] ??= new Set();
|
|
215
|
+
if (typeof value === 'function') {
|
|
216
|
+
reactive(element, 'style', STATE_HYDRATING, value);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
list(ctx, element, null, 'style', STATE_HYDRATING, value);
|
|
220
|
+
}
|
|
167
221
|
};
|
|
168
222
|
const spread = function (element, value) {
|
|
169
223
|
if (isObject(value)) {
|
|
170
|
-
let
|
|
171
|
-
while (name = names.pop()) {
|
|
224
|
+
for (let name in value) {
|
|
172
225
|
let v = value[name];
|
|
173
226
|
if (v == null || v === false || v === '') {
|
|
174
227
|
continue;
|
|
@@ -182,5 +235,5 @@ const spread = function (element, value) {
|
|
|
182
235
|
}
|
|
183
236
|
}
|
|
184
237
|
};
|
|
185
|
-
export default { set, spread };
|
|
186
|
-
export { set, spread };
|
|
238
|
+
export default { set, setClass, setProperty, setStyle, spread };
|
|
239
|
+
export { set, setClass, setProperty, setStyle, spread };
|
package/build/constants.d.ts
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
declare const ARRAY_SLOT: unique symbol;
|
|
2
2
|
declare const CLEANUP: unique symbol;
|
|
3
3
|
declare const EMPTY_FRAGMENT: DocumentFragment;
|
|
4
|
-
declare const NODE_CLOSING = 1;
|
|
5
|
-
declare const NODE_ELEMENT = 3;
|
|
6
|
-
declare const NODE_SLOT = 4;
|
|
7
|
-
declare const NODE_VOID = 5;
|
|
8
|
-
declare const NODE_WHITELIST: Record<string, number>;
|
|
9
|
-
declare const REGEX_EMPTY_TEXT_NODES: RegExp;
|
|
10
|
-
declare const REGEX_EVENTS: RegExp;
|
|
11
|
-
declare const REGEX_SLOT_ATTRIBUTES: RegExp;
|
|
12
|
-
declare const REGEX_SLOT_NODES: RegExp;
|
|
13
4
|
declare const SLOT_HTML = "<!--$-->";
|
|
14
|
-
declare const SLOT_MARKER = "{{$}}";
|
|
15
5
|
declare const STATE_HYDRATING = 0;
|
|
16
6
|
declare const STATE_NONE = 1;
|
|
17
7
|
declare const STATE_WAITING = 2;
|
|
18
|
-
|
|
8
|
+
declare const STORE: unique symbol;
|
|
9
|
+
export { ARRAY_SLOT, CLEANUP, EMPTY_FRAGMENT, SLOT_HTML, STATE_HYDRATING, STATE_NONE, STATE_WAITING, STORE };
|
|
10
|
+
export * from './event/constants.js';
|