@continuum-js/dom 0.3.1 → 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 +7 -0
- package/dist/index.js +51 -6
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ 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;
|
|
11
18
|
/** Anything placeable in JSX: nodes, behaviors (live-bound), primitives, arrays. */
|
|
12
19
|
export type Child = Node | Behavior<unknown> | string | number | boolean | null | undefined | Child[];
|
|
13
20
|
type Props = Record<string, unknown> | null;
|
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) {
|
|
@@ -305,6 +339,10 @@ export function dyn(b, render) {
|
|
|
305
339
|
const parent = end.parentNode;
|
|
306
340
|
for (const n of current.nodes)
|
|
307
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();
|
|
308
346
|
};
|
|
309
347
|
bind(b.listen(update));
|
|
310
348
|
onCleanup(() => current?.dispose());
|
|
@@ -356,6 +394,7 @@ export function each(items, key, render) {
|
|
|
356
394
|
const seen = new Set();
|
|
357
395
|
const next = [];
|
|
358
396
|
const seq = [];
|
|
397
|
+
const freshFlushes = [];
|
|
359
398
|
for (const item of list) {
|
|
360
399
|
const k = key(item);
|
|
361
400
|
if (seen.has(k))
|
|
@@ -370,6 +409,7 @@ export function each(items, key, render) {
|
|
|
370
409
|
const built = buildScoped(owner, () => render(item));
|
|
371
410
|
next.push({ key: k, nodes: built.nodes, dispose: built.dispose });
|
|
372
411
|
seq.push(-1);
|
|
412
|
+
freshFlushes.push(built.flush);
|
|
373
413
|
}
|
|
374
414
|
}
|
|
375
415
|
// dispose rows whose key disappeared
|
|
@@ -396,6 +436,9 @@ export function each(items, key, render) {
|
|
|
396
436
|
if (active instanceof HTMLElement && active.isConnected)
|
|
397
437
|
active.focus();
|
|
398
438
|
rows = next;
|
|
439
|
+
if (!owner || owner.mounted)
|
|
440
|
+
for (const f of freshFlushes)
|
|
441
|
+
f();
|
|
399
442
|
};
|
|
400
443
|
bind(items.listen(update));
|
|
401
444
|
onCleanup(() => {
|
|
@@ -477,6 +520,8 @@ export function mount(container, view) {
|
|
|
477
520
|
if (n.parentNode)
|
|
478
521
|
n.parentNode.removeChild(n);
|
|
479
522
|
});
|
|
523
|
+
if (currentOwner)
|
|
524
|
+
flushMounts(currentOwner);
|
|
480
525
|
return () => dispose();
|
|
481
526
|
});
|
|
482
527
|
}
|
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
|
}
|