@continuum-js/dom 0.3.0 → 0.4.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/dist/index.d.ts +10 -0
- package/dist/index.js +82 -10
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ export declare function scope<T>(fn: () => T): {
|
|
|
8
8
|
};
|
|
9
9
|
/** Register a cleanup in the current owner (no-op outside any owner). */
|
|
10
10
|
export declare function onCleanup(fn: () => void): void;
|
|
11
|
+
/**
|
|
12
|
+
* Register a callback to run once the current scope's nodes are inserted into
|
|
13
|
+
* the DOM — after `mount`, or right after a dynamic region (`dyn`/`each`)
|
|
14
|
+
* inserts a freshly built subtree. Use it for focus, measurement, and
|
|
15
|
+
* third-party libraries that need a live element. No-op outside any owner.
|
|
16
|
+
*/
|
|
17
|
+
export declare function onMount(fn: () => void): void;
|
|
18
|
+
/** Anything placeable in JSX: nodes, behaviors (live-bound), primitives, arrays. */
|
|
11
19
|
export type Child = Node | Behavior<unknown> | string | number | boolean | null | undefined | Child[];
|
|
12
20
|
type Props = Record<string, unknown> | null;
|
|
13
21
|
/** Fragment marker for `--jsxFragmentFactory Fragment`. */
|
|
@@ -21,10 +29,12 @@ export declare function h(tag: string | Component | typeof Fragment, props: Prop
|
|
|
21
29
|
export declare function dyn<T>(b: Behavior<T>, render: (v: T) => Child): Node;
|
|
22
30
|
/** Keyed list: reuses rows by key, reorders with minimal moves (LIS). */
|
|
23
31
|
export declare function each<T, K>(items: Behavior<T[]>, key: (item: T) => K, render: (item: T) => Child): Node;
|
|
32
|
+
/** A context handle: identity plus the value used when nothing was provided. */
|
|
24
33
|
export interface Context<T> {
|
|
25
34
|
readonly id: symbol;
|
|
26
35
|
readonly defaultValue: T;
|
|
27
36
|
}
|
|
37
|
+
/** Create a context. Provide with `provide`, read with `use`. */
|
|
28
38
|
export declare function createContext<T>(defaultValue: T): Context<T>;
|
|
29
39
|
/** Write a context value into the current owner. */
|
|
30
40
|
export declare function provide<T>(ctx: Context<T>, value: T): void;
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ let currentOwner = null;
|
|
|
6
6
|
function createOwner(parent) {
|
|
7
7
|
const owner = {
|
|
8
8
|
cleanups: [],
|
|
9
|
+
mounts: null,
|
|
10
|
+
mounted: false,
|
|
9
11
|
children: [],
|
|
10
12
|
parent,
|
|
11
13
|
contexts: null,
|
|
@@ -28,6 +30,7 @@ function disposeOwner(owner) {
|
|
|
28
30
|
owner.cleanups[i]();
|
|
29
31
|
}
|
|
30
32
|
owner.cleanups.length = 0;
|
|
33
|
+
owner.mounts = null; // never mounted → its onMount callbacks never run
|
|
31
34
|
// detach from parent
|
|
32
35
|
if (owner.parent) {
|
|
33
36
|
const siblings = owner.parent.children;
|
|
@@ -63,6 +66,32 @@ export function onCleanup(fn) {
|
|
|
63
66
|
if (currentOwner)
|
|
64
67
|
currentOwner.cleanups.push(fn);
|
|
65
68
|
}
|
|
69
|
+
// Run the subtree's pending onMount callbacks: child scopes first, then this
|
|
70
|
+
// owner's own, newest registration first. With the idiomatic `onMount` at the
|
|
71
|
+
// top of a component body, that yields children-before-parents.
|
|
72
|
+
function flushMounts(owner) {
|
|
73
|
+
if (owner.disposed)
|
|
74
|
+
return;
|
|
75
|
+
for (const child of owner.children)
|
|
76
|
+
flushMounts(child);
|
|
77
|
+
owner.mounted = true;
|
|
78
|
+
const mounts = owner.mounts;
|
|
79
|
+
if (mounts) {
|
|
80
|
+
owner.mounts = null;
|
|
81
|
+
for (let i = mounts.length - 1; i >= 0; i--)
|
|
82
|
+
mounts[i]();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Register a callback to run once the current scope's nodes are inserted into
|
|
87
|
+
* the DOM — after `mount`, or right after a dynamic region (`dyn`/`each`)
|
|
88
|
+
* inserts a freshly built subtree. Use it for focus, measurement, and
|
|
89
|
+
* third-party libraries that need a live element. No-op outside any owner.
|
|
90
|
+
*/
|
|
91
|
+
export function onMount(fn) {
|
|
92
|
+
if (currentOwner)
|
|
93
|
+
(currentOwner.mounts ?? (currentOwner.mounts = [])).push(fn);
|
|
94
|
+
}
|
|
66
95
|
/** Attach an frp subscription to the current owner's lifecycle. */
|
|
67
96
|
function bind(un) {
|
|
68
97
|
onCleanup(un);
|
|
@@ -251,21 +280,26 @@ export function h(tag, props, ...children) {
|
|
|
251
280
|
// Dynamic regions (§7)
|
|
252
281
|
// ---------------------------------------------------------------------------
|
|
253
282
|
// Build `child` into a fragment under a fresh scope of `owner`. Returns the
|
|
254
|
-
// fragment's top-level nodes
|
|
283
|
+
// fragment's top-level nodes, the scope's dispose handle, and a `flush` that
|
|
284
|
+
// runs the scope's pending onMount callbacks (call it after insertion).
|
|
255
285
|
function buildScoped(owner, build) {
|
|
256
|
-
const
|
|
286
|
+
const scopeOwner = createOwner(owner);
|
|
287
|
+
const nodes = runUnder(scopeOwner, () => {
|
|
257
288
|
const built = build();
|
|
258
289
|
// Fast path: a single element/text node (the common row/component case)
|
|
259
290
|
// needs no fragment or NodeList copy.
|
|
260
|
-
if (built instanceof Node &&
|
|
261
|
-
built.nodeType !== 11 /* DocumentFragment */) {
|
|
291
|
+
if (built instanceof Node && built.nodeType !== 11 /* DocumentFragment */) {
|
|
262
292
|
return [built];
|
|
263
293
|
}
|
|
264
294
|
const frag = document.createDocumentFragment();
|
|
265
295
|
appendChild(frag, built);
|
|
266
296
|
return Array.from(frag.childNodes);
|
|
267
|
-
})
|
|
268
|
-
return {
|
|
297
|
+
});
|
|
298
|
+
return {
|
|
299
|
+
nodes,
|
|
300
|
+
dispose: () => disposeOwner(scopeOwner),
|
|
301
|
+
flush: () => flushMounts(scopeOwner),
|
|
302
|
+
};
|
|
269
303
|
}
|
|
270
304
|
/** Conditional / switching subtree: rebuilds on each change of `b`. */
|
|
271
305
|
export function dyn(b, render) {
|
|
@@ -276,17 +310,39 @@ export function dyn(b, render) {
|
|
|
276
310
|
frag.appendChild(start);
|
|
277
311
|
frag.appendChild(end);
|
|
278
312
|
let current = null;
|
|
279
|
-
|
|
313
|
+
// Level-triggered: render the behavior's CURRENT value, not the delivered
|
|
314
|
+
// occurrence. A listener that runs earlier in the post phase may re-enter
|
|
315
|
+
// with a new moment (e.g. a router redirect); the stale queued delivery
|
|
316
|
+
// then must not clobber the newer render. Deduping by Object.is also makes
|
|
317
|
+
// duplicate deliveries free.
|
|
318
|
+
let hasRendered = false;
|
|
319
|
+
let renderedValue;
|
|
320
|
+
const update = () => {
|
|
321
|
+
const v = b.sampleNoTrans();
|
|
322
|
+
if (hasRendered && Object.is(renderedValue, v))
|
|
323
|
+
return;
|
|
324
|
+
hasRendered = true;
|
|
325
|
+
renderedValue = v;
|
|
280
326
|
if (current) {
|
|
281
327
|
current.dispose();
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
328
|
+
// Sweep the whole live range between the markers: a nested dynamic
|
|
329
|
+
// region at the root of this one may have swapped nodes since build,
|
|
330
|
+
// so the recorded node list can be stale.
|
|
331
|
+
let n = start.nextSibling;
|
|
332
|
+
while (n && n !== end) {
|
|
333
|
+
const next = n.nextSibling;
|
|
334
|
+
n.parentNode?.removeChild(n);
|
|
335
|
+
n = next;
|
|
336
|
+
}
|
|
285
337
|
}
|
|
286
338
|
current = buildScoped(owner, () => render(v));
|
|
287
339
|
const parent = end.parentNode;
|
|
288
340
|
for (const n of current.nodes)
|
|
289
341
|
parent.insertBefore(n, end);
|
|
342
|
+
// During the initial build the whole tree flushes at mount; afterwards
|
|
343
|
+
// each freshly inserted subtree flushes here.
|
|
344
|
+
if (!owner || owner.mounted)
|
|
345
|
+
current.flush();
|
|
290
346
|
};
|
|
291
347
|
bind(b.listen(update));
|
|
292
348
|
onCleanup(() => current?.dispose());
|
|
@@ -338,6 +394,7 @@ export function each(items, key, render) {
|
|
|
338
394
|
const seen = new Set();
|
|
339
395
|
const next = [];
|
|
340
396
|
const seq = [];
|
|
397
|
+
const freshFlushes = [];
|
|
341
398
|
for (const item of list) {
|
|
342
399
|
const k = key(item);
|
|
343
400
|
if (seen.has(k))
|
|
@@ -352,6 +409,7 @@ export function each(items, key, render) {
|
|
|
352
409
|
const built = buildScoped(owner, () => render(item));
|
|
353
410
|
next.push({ key: k, nodes: built.nodes, dispose: built.dispose });
|
|
354
411
|
seq.push(-1);
|
|
412
|
+
freshFlushes.push(built.flush);
|
|
355
413
|
}
|
|
356
414
|
}
|
|
357
415
|
// dispose rows whose key disappeared
|
|
@@ -378,6 +436,9 @@ export function each(items, key, render) {
|
|
|
378
436
|
if (active instanceof HTMLElement && active.isConnected)
|
|
379
437
|
active.focus();
|
|
380
438
|
rows = next;
|
|
439
|
+
if (!owner || owner.mounted)
|
|
440
|
+
for (const f of freshFlushes)
|
|
441
|
+
f();
|
|
381
442
|
};
|
|
382
443
|
bind(items.listen(update));
|
|
383
444
|
onCleanup(() => {
|
|
@@ -386,6 +447,7 @@ export function each(items, key, render) {
|
|
|
386
447
|
});
|
|
387
448
|
return frag;
|
|
388
449
|
}
|
|
450
|
+
/** Create a context. Provide with `provide`, read with `use`. */
|
|
389
451
|
export function createContext(defaultValue) {
|
|
390
452
|
return { id: Symbol("context"), defaultValue };
|
|
391
453
|
}
|
|
@@ -458,6 +520,8 @@ export function mount(container, view) {
|
|
|
458
520
|
if (n.parentNode)
|
|
459
521
|
n.parentNode.removeChild(n);
|
|
460
522
|
});
|
|
523
|
+
if (currentOwner)
|
|
524
|
+
flushMounts(currentOwner);
|
|
461
525
|
return () => dispose();
|
|
462
526
|
});
|
|
463
527
|
}
|
|
@@ -480,6 +544,14 @@ export function animationFrames() {
|
|
|
480
544
|
}
|
|
481
545
|
// ---------------------------------------------------------------------------
|
|
482
546
|
// Control-flow components — JSX wrappers over the rendering helpers.
|
|
547
|
+
//
|
|
548
|
+
// The function/component pairing is a deliberate symmetry, not duplication:
|
|
549
|
+
// when(b, then, else) ↔ <Show when={b}> — conditional region
|
|
550
|
+
// dyn(b, render) ↔ <Dynamic of={b}> — switching subtree
|
|
551
|
+
// each(b, by, render) ↔ <Each of={b} by={..}> — keyed list
|
|
552
|
+
// portal(target, ch) ↔ <Portal mount={..}> — render elsewhere
|
|
553
|
+
// Functions compose in plain code (no JSX required); components read better
|
|
554
|
+
// inside markup. Both call the same implementation.
|
|
483
555
|
// ---------------------------------------------------------------------------
|
|
484
556
|
// JSX always delivers children as the rest array; a single render function
|
|
485
557
|
// arrives as `[fn]`. Normalize to a render callback.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@continuum-js/dom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Continuum DOM renderer — fine-grained bindings, dynamic regions, ownership, context",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,6 +38,6 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@continuum-js/frp": "0.
|
|
41
|
+
"@continuum-js/frp": "0.4.0"
|
|
42
42
|
}
|
|
43
43
|
}
|