@evgkch/reactive-dom 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +431 -0
- package/dist/create-element.d.ts +5 -0
- package/dist/create-element.d.ts.map +1 -0
- package/dist/create-element.js +32 -0
- package/dist/ctx.d.ts +22 -0
- package/dist/ctx.d.ts.map +1 -0
- package/dist/ctx.js +35 -0
- package/dist/element.d.ts +13 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +44 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/list.d.ts +24 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/list.js +79 -0
- package/dist/log.d.ts +18 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +84 -0
- package/dist/slot.d.ts +6 -0
- package/dist/slot.d.ts.map +1 -0
- package/dist/slot.js +38 -0
- package/dist/struct.d.ts +11 -0
- package/dist/struct.d.ts.map +1 -0
- package/dist/struct.js +10 -0
- package/dist/unmount.d.ts +6 -0
- package/dist/unmount.d.ts.map +1 -0
- package/dist/unmount.js +22 -0
- package/examples/base.css +7 -0
- package/examples/console/app.js +97 -0
- package/examples/console/index.html +23 -0
- package/examples/console/styles.css +162 -0
- package/examples/index.html +28 -0
- package/examples/monitor/app.js +260 -0
- package/examples/monitor/index.html +23 -0
- package/examples/monitor/styles.css +425 -0
- package/examples/stream/app.js +136 -0
- package/examples/stream/index.html +23 -0
- package/examples/stream/styles.css +192 -0
- package/examples/styles.css +81 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# @evgkch/reactive-dom
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@evgkch/reactive-dom)
|
|
4
|
+
|
|
5
|
+
DOM bindings for [@evgkch/reactive](https://www.npmjs.com/package/@evgkch/reactive). No VDOM, no compiler, no magic — two primitives, one context, close to the platform.
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
The library maps directly onto the browser. Templates are real HTML parsed once by the browser. Elements are real DOM nodes. Reactivity is explicit — you decide what updates and when.
|
|
10
|
+
|
|
11
|
+
Two primitives mirror the data layer:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
R.Struct → UI.Struct // one element
|
|
15
|
+
R.List → UI.List // collection of elements
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
One context handles subscriptions. One function unmounts everything.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install @evgkch/reactive @evgkch/reactive-dom
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Peer:** `@evgkch/reactive` ^1.0.3. In 1.0.3 the reactive package added **debug logging**: `configure({ log: true })` or `configure({ log: "verbose" })` to log Batch runs and Watch patches; `getObjectId(obj)` returns a stable id (e.g. `"t1"`) for correlating in DevTools. See [@evgkch/reactive](https://www.npmjs.com/package/@evgkch/reactive).
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import * as R from "@evgkch/reactive"; // data
|
|
32
|
+
import * as UI from "@evgkch/reactive-dom"; // DOM
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## UI.Struct
|
|
38
|
+
|
|
39
|
+
Creates a reusable factory from an HTML template. The template is parsed **once** via `<template>`; each factory call clones it and runs `setup(props, refs, ctx)`.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const UserCard = UI.Struct(
|
|
43
|
+
`
|
|
44
|
+
<div class="card">
|
|
45
|
+
<span data-ref="name"></span>
|
|
46
|
+
<span data-ref="age"></span>
|
|
47
|
+
</div>
|
|
48
|
+
`,
|
|
49
|
+
(props: { user: User; theme: string }, refs, ctx) => {
|
|
50
|
+
ctx.batch(() => {
|
|
51
|
+
refs.name.textContent = props.user.name;
|
|
52
|
+
refs.age.textContent = String(props.user.age);
|
|
53
|
+
refs.el.className = `card ${props.theme}`;
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const el = UserCard({ user, theme: "dark" });
|
|
59
|
+
document.body.appendChild(el);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- `refs.el` — root element
|
|
63
|
+
- `refs[name]` — element with `[data-ref="name"]`
|
|
64
|
+
- `ctx` — component context, collects all subscriptions
|
|
65
|
+
|
|
66
|
+
`ctx.batch` runs **synchronously on first call** — the element is fully rendered before it enters the DOM. On subsequent reactive changes it re-runs automatically.
|
|
67
|
+
|
|
68
|
+
For manual removal:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
UI.remove(el); // unmount all subscriptions + el.remove()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## UI.List
|
|
77
|
+
|
|
78
|
+
Same signature as `UI.Struct`, but the factory has two modes depending on how it is called.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const TaskItem = UI.List(
|
|
82
|
+
`
|
|
83
|
+
<li>
|
|
84
|
+
<input type="checkbox" data-ref="check" />
|
|
85
|
+
<span data-ref="text"></span>
|
|
86
|
+
<button data-ref="remove">✕</button>
|
|
87
|
+
</li>
|
|
88
|
+
`,
|
|
89
|
+
(props: { item: Task }, refs, ctx) => {
|
|
90
|
+
ctx.batch(() => {
|
|
91
|
+
refs.text.textContent = props.item.text;
|
|
92
|
+
(refs.check as HTMLInputElement).checked = props.item.done;
|
|
93
|
+
refs.el.className = props.item.done ? "done" : "";
|
|
94
|
+
});
|
|
95
|
+
refs.check.onchange = () => {
|
|
96
|
+
props.item.done = (refs.check as HTMLInputElement).checked;
|
|
97
|
+
};
|
|
98
|
+
refs.remove.onclick = () => tasks.splice(tasks.indexOf(props.item), 1);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Mode 1 — single element.** Pass props, get an Element back. Same as `UI.Struct`:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const el = TaskItem({ item: task });
|
|
107
|
+
document.body.appendChild(el);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Mode 2 — mount list.** Pass a container, a reactive source, and a props mapper. Returns a stop function:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const stop = TaskItem(ul, tasks, (item) => ({ item }), {
|
|
114
|
+
onAdd: (el) => el.animate([{ opacity: 0 }, { opacity: 1 }], 150),
|
|
115
|
+
onRemove: (el, done) => el.animate([{ opacity: 1 }, { opacity: 0 }], 150).finished.then(done),
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
In list mode the library uses `Watch` to track operations on the source:
|
|
120
|
+
|
|
121
|
+
- `push` → one element created and appended
|
|
122
|
+
- `splice` → affected elements unmounted and removed
|
|
123
|
+
- `sort` / `reverse` → nodes reordered in place
|
|
124
|
+
|
|
125
|
+
`onAdd` fires only for elements added **after** initial mount — not for the initial list. Use it for enter animations, not for rendering.
|
|
126
|
+
|
|
127
|
+
`onRemove` receives `done` — the element is removed from DOM only after `done()` is called, enabling exit animations.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Context
|
|
132
|
+
|
|
133
|
+
Third argument in every setup after `props` and `refs`. Every subscription registered through `ctx` is automatically stopped when the component is unmounted.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
ctx.batch(fn) // reactive effect
|
|
137
|
+
ctx.watch(source, fn) // operation handler (sync, with patch)
|
|
138
|
+
ctx.list(container, source, propsFactory, factory, hooks?) // nested list
|
|
139
|
+
ctx.slot(container, getter) // conditional render
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
There is no need to manage stop functions manually — `ctx` collects them all.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Composition
|
|
147
|
+
|
|
148
|
+
**Nested list:**
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const ColumnItem = UI.Struct(
|
|
152
|
+
`
|
|
153
|
+
<div class="column">
|
|
154
|
+
<h3 data-ref="title"></h3>
|
|
155
|
+
<ul data-ref="tasks"></ul>
|
|
156
|
+
</div>
|
|
157
|
+
`,
|
|
158
|
+
(props: { col: Column }, refs, ctx) => {
|
|
159
|
+
ctx.batch(() => {
|
|
160
|
+
refs.title.textContent = props.col.title;
|
|
161
|
+
});
|
|
162
|
+
ctx.list(refs.tasks, props.col.tasks, (item) => ({ item }), TaskItem);
|
|
163
|
+
// TaskItem's stop is registered in ctx — unmounted automatically with ColumnItem
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Conditional render:**
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const App = UI.Struct(`<div><div data-ref="panel"></div></div>`, (props, refs, ctx) => {
|
|
172
|
+
ctx.slot(refs.panel, () => (user.isAdmin ? AdminPanel({ user }) : UserPanel({ user })));
|
|
173
|
+
// when user.isAdmin changes — old element is unmounted, new one mounted
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Multiple data sources in one component:**
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const Item = UI.Struct(`<div data-ref="el"></div>`, (props: { item: Task }, refs, ctx) => {
|
|
181
|
+
// ctx.batch reads from multiple reactive sources — tracks all of them
|
|
182
|
+
ctx.batch(() => {
|
|
183
|
+
refs.el.className = [
|
|
184
|
+
props.item.done ? "done" : "",
|
|
185
|
+
filter.get() === "active" && props.item.done ? "hidden" : "",
|
|
186
|
+
]
|
|
187
|
+
.join(" ")
|
|
188
|
+
.trim();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## UI.Slot
|
|
196
|
+
|
|
197
|
+
Conditional render outside a component:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const stop = UI.Slot(container, () => (isOpen.get() ? Modal({ title }) : null));
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
An anchor `Comment` node fixes the position in DOM. When the getter result changes — previous element is unmounted, new one is inserted.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## UI.remove / UI.unmount
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
UI.remove(el); // unmount all subscriptions + el.remove()
|
|
211
|
+
UI.unmount(el); // unmount only — element stays in DOM
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`UI.List` and `UI.Slot` call `UI.remove` automatically on their elements. For manually mounted elements call it explicitly.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Debug logging (component logger)
|
|
219
|
+
|
|
220
|
+
Turn on component lifecycle logs to see mount, list add/remove, slot swap, and unmount in the console. Uses a `[reactive-dom]` prefix and stable ids (`c1`, `c2`, …) so you can correlate with DevTools.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import * as UI from "@evgkch/reactive-dom";
|
|
224
|
+
|
|
225
|
+
UI.configure({ log: true });
|
|
226
|
+
// mount c1 <div>
|
|
227
|
+
// list mount c2
|
|
228
|
+
// list add c3
|
|
229
|
+
// unmount c3
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- `configure({ log: true })` — mount, unmount, list mount/add/remove, slot mount/swap/stop.
|
|
233
|
+
- `configure({ log: false })` — off (default).
|
|
234
|
+
- `getComponentId(el)` — returns the stable id for a component root (e.g. `"c1"`), creates one if missing.
|
|
235
|
+
|
|
236
|
+
In the **examples** (monitor, console, stream), both the reactive logger (`R.configure({ log: true })`) and the component logger (`UI.configure({ log: true })`) are enabled when you open the app in the browser, so you see Batch/Watch from reactive and mount/list/slot/unmount from reactive-dom together.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## ReactiveElement
|
|
241
|
+
|
|
242
|
+
Base class for Custom Elements. Override `mount(ctx)` — it is called by the browser lifecycle at the right time.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
class TaskItemEl extends UI.ReactiveElement {
|
|
246
|
+
#props: { item: Task } | null = null;
|
|
247
|
+
|
|
248
|
+
set props(v: { item: Task }) {
|
|
249
|
+
this.#props = v;
|
|
250
|
+
this.invalidate(); // re-mounts if already in DOM
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
protected mount(ctx: Context): void {
|
|
254
|
+
if (!this.#props) return;
|
|
255
|
+
const { item } = this.#props;
|
|
256
|
+
ctx.batch(() => {
|
|
257
|
+
this.textContent = item.text;
|
|
258
|
+
this.className = item.done ? "done" : "";
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
customElements.define("task-item", TaskItemEl);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`disconnectedCallback` calls `unmount` automatically. This means `UI.List` needs no manual stop — `el.remove()` triggers `disconnectedCallback` which cleans up:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
TaskItem(ul, tasks, (item) => {
|
|
270
|
+
const el = document.createElement("task-item") as TaskItemEl;
|
|
271
|
+
el.props = { item };
|
|
272
|
+
return el; // no stop needed
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Full example — process monitor
|
|
279
|
+
|
|
280
|
+
A compact terminal-style app: live CPU/MEM metrics, reactive process list, hotkeys.
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import * as R from "@evgkch/reactive";
|
|
284
|
+
import * as UI from "@evgkch/reactive-dom";
|
|
285
|
+
|
|
286
|
+
const filter = R.Value("all");
|
|
287
|
+
const cpu = R.Value(42);
|
|
288
|
+
const tasks = R.List([
|
|
289
|
+
R.Struct({ text: "infiltrate server", done: false }),
|
|
290
|
+
R.Struct({ text: "extract data", done: true }),
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
// live metrics
|
|
294
|
+
setInterval(() => {
|
|
295
|
+
cpu.set(Math.max(10, Math.min(99, cpu.get() + Math.floor(Math.random() * 11) - 5)));
|
|
296
|
+
}, 1000);
|
|
297
|
+
|
|
298
|
+
const TaskItem = UI.List(
|
|
299
|
+
`
|
|
300
|
+
<li>
|
|
301
|
+
<input type="checkbox" data-ref="check" />
|
|
302
|
+
<span data-ref="text"></span>
|
|
303
|
+
<span data-ref="status"></span>
|
|
304
|
+
<button data-ref="remove">kill</button>
|
|
305
|
+
</li>
|
|
306
|
+
`,
|
|
307
|
+
(props: { item: Task }, refs, ctx) => {
|
|
308
|
+
ctx.batch(() => {
|
|
309
|
+
refs.text.textContent = props.item.text;
|
|
310
|
+
(refs.check as HTMLInputElement).checked = props.item.done;
|
|
311
|
+
refs.el.className = props.item.done ? "done" : "";
|
|
312
|
+
refs.status.textContent = props.item.done ? "DONE" : "RUNNING";
|
|
313
|
+
|
|
314
|
+
const f = filter.get();
|
|
315
|
+
refs.el.style.display =
|
|
316
|
+
f === "active" ? (props.item.done ? "none" : "") : f === "done" ? (!props.item.done ? "none" : "") : "";
|
|
317
|
+
});
|
|
318
|
+
refs.check.onchange = () => {
|
|
319
|
+
props.item.done = (refs.check as HTMLInputElement).checked;
|
|
320
|
+
};
|
|
321
|
+
refs.remove.onclick = () => tasks.splice(tasks.indexOf(props.item), 1);
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const App = UI.Struct(
|
|
326
|
+
`
|
|
327
|
+
<div class="monitor">
|
|
328
|
+
<div class="cpu-bar" data-ref="cpu-bar"></div>
|
|
329
|
+
<div class="prompt">
|
|
330
|
+
<span>$</span>
|
|
331
|
+
<input data-ref="input" placeholder="spawn process..." />
|
|
332
|
+
</div>
|
|
333
|
+
<ul data-ref="list"></ul>
|
|
334
|
+
<div class="statusbar">
|
|
335
|
+
<span data-ref="count"></span>
|
|
336
|
+
<div data-ref="filters">
|
|
337
|
+
<button data-filter="all">all</button>
|
|
338
|
+
<button data-filter="active">running</button>
|
|
339
|
+
<button data-filter="done">done</button>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
`,
|
|
344
|
+
(props, refs, ctx) => {
|
|
345
|
+
ctx.list(refs.list, tasks, (item) => ({ item }), TaskItem, {
|
|
346
|
+
onAdd: (el) => el.animate([{ opacity: 0 }, { opacity: 1 }], 150),
|
|
347
|
+
onRemove: (el, done) => el.animate([{ opacity: 1 }, { opacity: 0 }], 130).finished.then(done),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
ctx.batch(() => {
|
|
351
|
+
refs["cpu-bar"].style.width = cpu.get() + "%";
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
ctx.batch(() => {
|
|
355
|
+
let running = 0;
|
|
356
|
+
tasks.forEach((t) => {
|
|
357
|
+
if (!t.done) running++;
|
|
358
|
+
});
|
|
359
|
+
refs.count.textContent = `${running} of ${tasks.length} running`;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
ctx.batch(() => {
|
|
363
|
+
refs.filters.querySelectorAll("[data-filter]").forEach((btn) => {
|
|
364
|
+
btn.className = filter.get() === btn.getAttribute("data-filter") ? "active" : "";
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
refs.filters.addEventListener("click", (e) => {
|
|
369
|
+
const f = (e.target as HTMLElement).getAttribute("data-filter");
|
|
370
|
+
if (f) filter.set(f);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const input = refs.input as HTMLInputElement;
|
|
374
|
+
input.addEventListener("keydown", (e) => {
|
|
375
|
+
if (e.key !== "Enter" || !input.value.trim()) return;
|
|
376
|
+
tasks.push(R.Struct({ text: input.value.trim(), done: false }));
|
|
377
|
+
input.value = "";
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// hotkeys
|
|
381
|
+
document.addEventListener("keydown", (e) => {
|
|
382
|
+
if (document.activeElement === input) return;
|
|
383
|
+
if (e.key === "1") filter.set("all");
|
|
384
|
+
if (e.key === "2") filter.set("active");
|
|
385
|
+
if (e.key === "3") filter.set("done");
|
|
386
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) input.focus();
|
|
387
|
+
});
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
document.getElementById("app")!.appendChild(App({}));
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## API reference
|
|
397
|
+
|
|
398
|
+
| API | Description |
|
|
399
|
+
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
400
|
+
| `Struct(html, setup)` | Factory `(props) => Element`. Setup: `(props, refs, ctx)`. Template parsed once. |
|
|
401
|
+
| `List(html, setup)` | Dual factory. `(props) => Element` or `(container, source, propsFactory, hooks?) => stop`. Setup: `(props, refs, ctx)`. `onAdd` fires only for new items, not initial mount. |
|
|
402
|
+
| `Slot(container, getter)` | Conditional slot. Swaps element on change. Returns `stop`. |
|
|
403
|
+
| `remove(el)` | `unmount(el)` + `el.remove()`. |
|
|
404
|
+
| `unmount(el)` | Stop all subscriptions. Element stays in DOM. |
|
|
405
|
+
| `ReactiveElement` | Base class for Custom Elements. Override `mount(ctx)`, call `invalidate()` from setters. |
|
|
406
|
+
| `configure({ log? })` | Component logger: `log: true` logs mount/unmount, list add/remove, slot mount/swap/stop. Default `false`. |
|
|
407
|
+
| `getComponentId(el)` | Stable id for a component root (e.g. `"c1"`). Use to correlate with logs or DevTools. |
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## GitHub Pages
|
|
412
|
+
|
|
413
|
+
Examples are deployed automatically on push to `main`. The workflow builds the lib, fills `docs/` (gitignored) with the site, and deploys it — no duplicated code in the repo.
|
|
414
|
+
|
|
415
|
+
**One-time setup:** **Settings → Pages → Build and deployment → Source** must be **GitHub Actions** (not "Deploy from a branch"). If you see 404 for `dist/index.js` or `lib/index.js`, the live site is coming from the branch; switch Source to **GitHub Actions** and re-run the workflow.
|
|
416
|
+
|
|
417
|
+
Site: `https://<username>.github.io/<repo>/`. To test the build locally: `npm run build:gh-pages` then serve the `docs/` folder.
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## Testing
|
|
422
|
+
|
|
423
|
+
```sh
|
|
424
|
+
npm run build && npm test
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## License
|
|
430
|
+
|
|
431
|
+
ISC
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Refs } from "./ctx.js";
|
|
2
|
+
import { Context } from "./ctx.js";
|
|
3
|
+
export declare function createElement<P>(tmpl: HTMLTemplateElement, setup: (props: P, refs: Refs, ctx: Context) => void, props: P): Element;
|
|
4
|
+
export declare function parseTemplate(html: string): HTMLTemplateElement;
|
|
5
|
+
//# sourceMappingURL=create-element.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-element.d.ts","sourceRoot":"","sources":["../src/create-element.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAInC,wBAAgB,aAAa,CAAC,CAAC,EAC3B,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,EACnD,KAAK,EAAE,CAAC,GACT,OAAO,CAuBT;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAI/D"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Context } from "./ctx.js";
|
|
2
|
+
import { logMount } from "./log.js";
|
|
3
|
+
import { setComponentStop } from "./unmount.js";
|
|
4
|
+
export function createElement(tmpl, setup, props) {
|
|
5
|
+
const fragment = tmpl.content.cloneNode(true);
|
|
6
|
+
const el = fragment.firstElementChild;
|
|
7
|
+
if (!el)
|
|
8
|
+
throw new Error("Template must have a root element");
|
|
9
|
+
const refs = { el };
|
|
10
|
+
const refEls = el.querySelectorAll("[data-ref]");
|
|
11
|
+
for (let i = 0; i < refEls.length; i++) {
|
|
12
|
+
const node = refEls[i];
|
|
13
|
+
const name = node.getAttribute("data-ref");
|
|
14
|
+
if (name)
|
|
15
|
+
refs[name] = node; // duplicate names: last wins
|
|
16
|
+
}
|
|
17
|
+
const stops = [];
|
|
18
|
+
const ctx = new Context(stops);
|
|
19
|
+
setup(props, refs, ctx);
|
|
20
|
+
const stop = () => {
|
|
21
|
+
for (const s of stops)
|
|
22
|
+
s();
|
|
23
|
+
};
|
|
24
|
+
setComponentStop(el, stop);
|
|
25
|
+
logMount(el);
|
|
26
|
+
return el;
|
|
27
|
+
}
|
|
28
|
+
export function parseTemplate(html) {
|
|
29
|
+
const tmpl = document.createElement("template");
|
|
30
|
+
tmpl.innerHTML = html.trim();
|
|
31
|
+
return tmpl;
|
|
32
|
+
}
|
package/dist/ctx.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type Refs = Record<string, Element> & {
|
|
2
|
+
el: Element;
|
|
3
|
+
};
|
|
4
|
+
export type WatchSource<Patch> = {
|
|
5
|
+
watch(fn: (patch: Patch) => void): () => void;
|
|
6
|
+
};
|
|
7
|
+
export interface ContextListHooks {
|
|
8
|
+
onAdd?: (el: Element) => void;
|
|
9
|
+
onRemove?: (el: Element, done: () => void) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare class Context {
|
|
12
|
+
#private;
|
|
13
|
+
constructor(stops: (() => void)[]);
|
|
14
|
+
batch(fn: () => void): void;
|
|
15
|
+
watch<Patch>(source: WatchSource<Patch>, fn: (patch: Patch) => void): void;
|
|
16
|
+
list<T>(container: Element, source: {
|
|
17
|
+
watch(fn: (p: unknown) => void): () => void;
|
|
18
|
+
forEach(fn: (item: T) => void): void;
|
|
19
|
+
}, propsFactory: (item: T) => unknown, itemFactory: (props: unknown) => Element, hooks?: ContextListHooks): void;
|
|
20
|
+
slot(container: Element, getter: () => Element | null): void;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=ctx.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ctx.d.ts","sourceRoot":"","sources":["../src/ctx.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,EAAE,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7D,MAAM,MAAM,WAAW,CAAC,KAAK,IAAI;IAC7B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACjD,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC7B,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9B,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CACtD;AAED,qBAAa,OAAO;;gBAGJ,KAAK,EAAE,CAAC,MAAM,IAAI,CAAC,EAAE;IAIjC,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAI3B,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI;IAI1E,IAAI,CAAC,CAAC,EACF,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,GAAG,IAAI,CAAA;KAAE,EAC7F,YAAY,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,EAClC,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,EACxC,KAAK,CAAC,EAAE,gBAAgB,GACzB,IAAI;IAKP,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,GAAG,IAAI,GAAG,IAAI;CAG/D"}
|
package/dist/ctx.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _Context_stops;
|
|
13
|
+
import { Batch } from "@evgkch/reactive";
|
|
14
|
+
import { mountList } from "./list.js";
|
|
15
|
+
import { Slot } from "./slot.js";
|
|
16
|
+
export class Context {
|
|
17
|
+
constructor(stops) {
|
|
18
|
+
_Context_stops.set(this, void 0);
|
|
19
|
+
__classPrivateFieldSet(this, _Context_stops, stops, "f");
|
|
20
|
+
}
|
|
21
|
+
batch(fn) {
|
|
22
|
+
__classPrivateFieldGet(this, _Context_stops, "f").push(Batch(fn));
|
|
23
|
+
}
|
|
24
|
+
watch(source, fn) {
|
|
25
|
+
__classPrivateFieldGet(this, _Context_stops, "f").push(source.watch(fn));
|
|
26
|
+
}
|
|
27
|
+
list(container, source, propsFactory, itemFactory, hooks) {
|
|
28
|
+
const factory = (item) => itemFactory(propsFactory(item));
|
|
29
|
+
__classPrivateFieldGet(this, _Context_stops, "f").push(mountList(container, source, factory, hooks));
|
|
30
|
+
}
|
|
31
|
+
slot(container, getter) {
|
|
32
|
+
__classPrivateFieldGet(this, _Context_stops, "f").push(Slot(container, getter));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
_Context_stops = new WeakMap();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Context } from "./ctx.js";
|
|
2
|
+
/**
|
|
3
|
+
* Base class for Custom Elements. Override mount(ctx); call invalidate() from setters when data changes.
|
|
4
|
+
* disconnectedCallback runs when UI.List does el.remove() — no manual stop needed.
|
|
5
|
+
*/
|
|
6
|
+
export declare abstract class ReactiveElement extends HTMLElement {
|
|
7
|
+
#private;
|
|
8
|
+
protected invalidate(): void;
|
|
9
|
+
connectedCallback(): void;
|
|
10
|
+
disconnectedCallback(): void;
|
|
11
|
+
protected abstract mount(ctx: Context): void;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=element.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC;;;GAGG;AACH,8BAAsB,eAAgB,SAAQ,WAAW;;IAGrD,SAAS,CAAC,UAAU,IAAI,IAAI;IAiB5B,iBAAiB,IAAI,IAAI;IAIzB,oBAAoB,IAAI,IAAI;IAI5B,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;CAC/C"}
|
package/dist/element.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
+
};
|
|
6
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
7
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
10
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11
|
+
};
|
|
12
|
+
var _ReactiveElement_instances, _ReactiveElement_stops, _ReactiveElement_doMount, _ReactiveElement_unmount;
|
|
13
|
+
import { Context } from "./ctx.js";
|
|
14
|
+
/**
|
|
15
|
+
* Base class for Custom Elements. Override mount(ctx); call invalidate() from setters when data changes.
|
|
16
|
+
* disconnectedCallback runs when UI.List does el.remove() — no manual stop needed.
|
|
17
|
+
*/
|
|
18
|
+
export class ReactiveElement extends HTMLElement {
|
|
19
|
+
constructor() {
|
|
20
|
+
super(...arguments);
|
|
21
|
+
_ReactiveElement_instances.add(this);
|
|
22
|
+
_ReactiveElement_stops.set(this, []);
|
|
23
|
+
}
|
|
24
|
+
invalidate() {
|
|
25
|
+
if (!this.isConnected)
|
|
26
|
+
return;
|
|
27
|
+
__classPrivateFieldGet(this, _ReactiveElement_instances, "m", _ReactiveElement_unmount).call(this);
|
|
28
|
+
__classPrivateFieldGet(this, _ReactiveElement_instances, "m", _ReactiveElement_doMount).call(this);
|
|
29
|
+
}
|
|
30
|
+
connectedCallback() {
|
|
31
|
+
__classPrivateFieldGet(this, _ReactiveElement_instances, "m", _ReactiveElement_doMount).call(this);
|
|
32
|
+
}
|
|
33
|
+
disconnectedCallback() {
|
|
34
|
+
__classPrivateFieldGet(this, _ReactiveElement_instances, "m", _ReactiveElement_unmount).call(this);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
_ReactiveElement_stops = new WeakMap(), _ReactiveElement_instances = new WeakSet(), _ReactiveElement_doMount = function _ReactiveElement_doMount() {
|
|
38
|
+
const stops = [];
|
|
39
|
+
this.mount(new Context(stops));
|
|
40
|
+
__classPrivateFieldSet(this, _ReactiveElement_stops, stops, "f");
|
|
41
|
+
}, _ReactiveElement_unmount = function _ReactiveElement_unmount() {
|
|
42
|
+
__classPrivateFieldGet(this, _ReactiveElement_stops, "f").forEach((s) => s());
|
|
43
|
+
__classPrivateFieldSet(this, _ReactiveElement_stops, [], "f");
|
|
44
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { Struct, Context } from "./struct.js";
|
|
2
|
+
export type { Refs, StructSetup } from "./struct.js";
|
|
3
|
+
export type { ListHooks } from "./list.js";
|
|
4
|
+
export { List } from "./list.js";
|
|
5
|
+
export type { Watchable, ListSetup } from "./list.js";
|
|
6
|
+
export { Slot } from "./slot.js";
|
|
7
|
+
export { unmount, remove } from "./unmount.js";
|
|
8
|
+
export { ReactiveElement } from "./element.js";
|
|
9
|
+
export { configure, getComponentId, setLogLevel } from "./log.js";
|
|
10
|
+
export type { LogLevel, ConfigureOptions } from "./log.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACrD,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAClE,YAAY,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Struct, Context } from "./struct.js";
|
|
2
|
+
export { List } from "./list.js";
|
|
3
|
+
export { Slot } from "./slot.js";
|
|
4
|
+
export { unmount, remove } from "./unmount.js";
|
|
5
|
+
export { ReactiveElement } from "./element.js";
|
|
6
|
+
export { configure, getComponentId, setLogLevel } from "./log.js";
|
package/dist/list.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ListPatch } from "@evgkch/reactive";
|
|
2
|
+
import type { Refs } from "./ctx.js";
|
|
3
|
+
import type { Context } from "./ctx.js";
|
|
4
|
+
export interface Watchable<T> {
|
|
5
|
+
watch(fn: (patch: ListPatch<T>) => void): () => void;
|
|
6
|
+
forEach(fn: (item: T) => void): void;
|
|
7
|
+
}
|
|
8
|
+
export interface ListHooks {
|
|
9
|
+
onAdd?: (el: Element) => void;
|
|
10
|
+
onRemove?: (el: Element, done: () => void) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Mount reactive list into container. Patches via Watch.
|
|
14
|
+
* On remove: unmount(el), then onRemove(el, done) or el.remove().
|
|
15
|
+
*/
|
|
16
|
+
export declare function mountList<T>(container: Element, source: Watchable<T>, factory: (item: T) => Element, hooks?: ListHooks): () => void;
|
|
17
|
+
export type ListSetup<P> = (props: P, refs: Refs, ctx: Context) => void;
|
|
18
|
+
/**
|
|
19
|
+
* List(html, setup) returns a dual-purpose function:
|
|
20
|
+
* - (props) => Element — create one item (for use in ctx.list as itemFactory)
|
|
21
|
+
* - (container, source, propsFactory, hooks?) => () => void — mount list, returns stop
|
|
22
|
+
*/
|
|
23
|
+
export declare function List<P>(html: string, setup: ListSetup<P>): ((props: P) => Element) & ((container: Element, source: Watchable<unknown>, propsFactory: (item: unknown) => P, hooks?: ListHooks) => () => void);
|
|
24
|
+
//# sourceMappingURL=list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../src/list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAIxC,MAAM,WAAW,SAAS,CAAC,CAAC;IACxB,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACrD,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,GAAG,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,SAAS;IACtB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9B,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CACtD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,EACvB,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,EACpB,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,EAC7B,KAAK,CAAC,EAAE,SAAS,GAClB,MAAM,IAAI,CAgDZ;AAUD,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;AAExE;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,GAAG,CAAC,CACnF,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,EAC1B,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,CAAC,EAClC,KAAK,CAAC,EAAE,SAAS,KAChB,MAAM,IAAI,CAAC,CAkBf"}
|