@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 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 and the scope's dispose handle.
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 s = runUnder(owner, () => scope(() => {
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 { nodes: s.value, dispose: s.dispose };
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.1",
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.3.1"
41
+ "@continuum-js/frp": "0.4.0"
42
42
  }
43
43
  }