@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 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 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) {
@@ -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
- const update = (v) => {
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
- for (const n of current.nodes)
283
- if (n.parentNode)
284
- n.parentNode.removeChild(n);
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.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.3.0"
41
+ "@continuum-js/frp": "0.4.0"
42
42
  }
43
43
  }