@brust/runtime-dom 2.0.0-alpha.22 → 2.0.0-alpha.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brust/runtime-dom",
3
- "version": "2.0.0-alpha.22",
3
+ "version": "2.0.0-alpha.24",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -33,6 +33,9 @@ BINDERS.push({
33
33
  if (!adopt.includes(el)) el.remove()
34
34
  let firstRun = true
35
35
 
36
+ // F415: a removed row's effects and cleanups go with it (they were bound under `inst.capture`).
37
+ const drops = new Map<Element, () => void>()
38
+ const drop = (n: Element) => { drops.get(n)?.(); drops.delete(n); n.remove() }
36
39
  let rows = new Map<unknown, Element[]>() // key -> elements (array to tolerate duplicates)
37
40
  let order: Element[] = []
38
41
 
@@ -71,12 +74,12 @@ BINDERS.push({
71
74
  const rowScope: RowScope = { ...scope, [itemName]: item, ...(indexName ? { [indexName]: i } : {}), __own: own, __tick: computed(() => (outer ? outer() : 0) + own()) }
72
75
  ;(node as unknown as { __scope: RowScope }).__scope = rowScope
73
76
  // a row that is itself a nested host is bound by its own instance
74
- if (!node.hasAttribute('x-data')) { bindElement(inst, node, rowScope); walkChildren(inst, node, rowScope) }
77
+ if (!node.hasAttribute('x-data')) { const n = node; drops.set(n, inst.capture(() => { bindElement(inst, n, rowScope); walkChildren(inst, n, rowScope) })) }
75
78
  }
76
79
  ;(next.get(key) ?? next.set(key, []).get(key)!).push(node)
77
80
  nextOrder.push(node)
78
81
  })
79
- for (const [, pool] of rows) for (const stale of pool) stale.remove()
82
+ for (const [, pool] of rows) for (const stale of pool) drop(stale)
80
83
  for (const left of adopt.splice(0)) left.remove()
81
84
  firstRun = false
82
85
  // place in order after the anchor
@@ -85,6 +88,6 @@ BINDERS.push({
85
88
  rows = next; order = nextOrder
86
89
  for (const n of created) hooks.mountTree(n) // nested hosts in new rows mount now, not on the observer's next tick
87
90
  })
88
- inst.onCleanup(() => { for (const n of order) n.remove() })
91
+ inst.onCleanup(() => { for (const n of order) drop(n) })
89
92
  },
90
93
  })
@@ -1,4 +1,11 @@
1
1
  import { BINDERS, bindElement, walkChildren, parsed, read, hooks } from '../core-api'
2
+ /** The callback refs of the branch being inserted: attached once its whole subtree is bound and mounted (React attaches
3
+ * refs in the commit, after the DOM is placed and before layout effects). An inner branch inserted with it hands its
4
+ * refs over, so the outermost attaches them all in DOM post-order (children first, as React's commit does). */
5
+ type Attach = [el: Element, on: () => void]
6
+ let attach: Attach[] | null = null
7
+ /** `a` first when `b` contains it (8) or follows it (4) without being inside it (16). */
8
+ const postOrder = ([a]: Attach, [b]: Attach) => { const p = a.compareDocumentPosition(b); return p & 8 || (p & 20) === 4 ? -1 : 1 }
2
9
  BINDERS.push({
3
10
  match: (a) => a === 'x-if',
4
11
  bind: ({ inst, el, attr, raw, scope }) => {
@@ -8,18 +15,46 @@ BINDERS.push({
8
15
  template.removeAttribute('x-if'); template.removeAttribute('hidden')
9
16
  el.replaceWith(anchor) // the server-rendered element becomes the first clone below
10
17
  let current: Element | null = null
18
+ // F415/F416: what the branch bound (its effects, its x-ref) is let go before the element leaves, as React detaches
19
+ // a removed host's refs in the same commit.
20
+ let drop = () => {}
21
+ const remove = () => { drop(); drop = () => {}; current?.remove(); current = null } // the observer disposes nested hosts
11
22
  inst.effect(() => {
12
23
  const r = read(inst, p, scope, attr); if (!r.ok) return
13
24
  const want = Boolean(r.value)
14
25
  if (want && !current) {
15
- current = template.cloneNode(true) as Element
16
- anchor.after(current)
17
- bindElement(inst, current, scope); walkChildren(inst, current, scope)
18
- hooks.mountTree(current)
19
- } else if (!want && current) {
20
- current.remove(); current = null // the observer disposes nested hosts
21
- }
26
+ const node = (current = template.cloneNode(true) as Element)
27
+ anchor.after(node)
28
+ const outer = attach; const mine: Attach[] = (attach = [])
29
+ try {
30
+ // nested hosts mount inside the capture too: an `x-cref` on one registers its detach on this branch
31
+ drop = inst.capture(() => { bindElement(inst, node, scope); walkChildren(inst, node, scope); hooks.mountTree(node) })
32
+ } finally { attach = outer }
33
+ if (outer) outer.push(...mine)
34
+ else for (const [, on] of mine.sort(postOrder)) on()
35
+ } else if (!want && current) remove()
22
36
  })
23
- inst.onCleanup(() => { current?.remove() })
37
+ inst.onCleanup(remove)
38
+ },
39
+ })
40
+ // F288 phase B (G5): `x-cref="cell"`, a callback ref inside a branch. The cell holds the callback (or a ref object); it
41
+ // gets the node when the branch is inserted and `null` (or the callback's returned cleanup) when it is removed. On a
42
+ // nested host the host's own instance meets it while it mounts, inside the branch's capture. (A host whose behavior
43
+ // chunk is not loaded yet binds later through whenBehavior, outside the capture: it attaches at once and detaches only
44
+ // with the owner, not the branch: reachable only if an inlined child's root can have its behavior in another chunk.)
45
+ BINDERS.push({
46
+ match: (a) => a === 'x-cref',
47
+ bind: ({ inst, el, raw }) => {
48
+ // on a nested host (an inlined native child's root) the cell is the caller's, as `x-pref`'s is
49
+ const owner = el === inst.host ? inst.parent : inst
50
+ if (!owner) return
51
+ const r = owner.ref(raw.trim()).current as unknown
52
+ let off: (() => void) | void
53
+ owner.onCleanup(() => { off?.(); off = undefined })
54
+ const on = () => {
55
+ if (typeof r === 'function') { const c = r(el); off = typeof c === 'function' ? c : () => r(null) }
56
+ else if (r) { (r as { current: unknown }).current = el; off = () => { (r as { current: unknown }).current = null } }
57
+ }
58
+ attach ? attach.push([el, on]) : on()
24
59
  },
25
60
  })
package/src/instance.ts CHANGED
@@ -18,6 +18,8 @@ export class Instance {
18
18
  children = new Set<Instance>()
19
19
  private disposers: Dispose[] = []
20
20
  private cleanups: Array<() => void> = []
21
+ /** F415: while an `x-if` branch or an `x-for` row binds, what it registers is owned by that part (`capture`). */
22
+ private sink: Array<() => void> | null = null
21
23
  disposed = false
22
24
  /** false until the initial bind pass finishes; the first-paint tripwire only applies before then. */
23
25
  booted = false
@@ -36,7 +38,7 @@ export class Instance {
36
38
  }
37
39
 
38
40
  effect(fn: () => void | (() => void), tier = 0): Dispose {
39
- const d = coreEffect(fn, tier); this.disposers.push(d); return d
41
+ const d = coreEffect(fn, tier); (this.sink ?? this.disposers).push(d); return d
40
42
  }
41
43
  /** `ctx.effect`: deferred until `start()` while the factory runs, immediate afterwards. A compiled effect re-runs after
42
44
  * the bindings: layout (`layout` true) before passive (F288, ruling 94404307 amended). */
@@ -64,7 +66,15 @@ export class Instance {
64
66
  }
65
67
  return true
66
68
  }
67
- onCleanup(fn: () => void): void { this.cleanups.push(fn) }
69
+ onCleanup(fn: () => void): void { (this.sink ?? this.cleanups).push(fn) }
70
+ /** F415/F416: runs `fn` with the effects and cleanups it registers owned by the returned disposer, which an `x-if`
71
+ * branch or an `x-for` row runs when it is removed: its effects stop and its cleanups run (an `x-ref` lets go of the
72
+ * node), in the order they were registered. */
73
+ capture(fn: () => void): () => void {
74
+ const prev = this.sink; const own: Array<() => void> = (this.sink = [])
75
+ try { fn() } finally { this.sink = prev }
76
+ return () => runCleanups(own)
77
+ }
68
78
  ref<E extends Element = HTMLElement>(name: string, init?: unknown): { current: E | null } {
69
79
  let r = this.refs.get(name)
70
80
  // `useRef(init)`: the cell starts at `init`; an `x-ref` element replaces it when it binds (React attaches the same way).
@@ -87,10 +97,14 @@ export class Instance {
87
97
  for (const c of Array.from(this.children)) c.dispose()
88
98
  this.parent?.children.delete(this)
89
99
  for (const d of this.disposers.splice(0)) d()
90
- for (const c of this.cleanups.splice(0)) { try { c() } catch (e) { console.error('[brust] cleanup threw', e) } }
100
+ runCleanups(this.cleanups)
91
101
  }
92
102
  }
93
103
 
104
+ function runCleanups(list: Array<() => void>): void {
105
+ for (const c of list.splice(0)) { try { c() } catch (e) { console.error('[brust] cleanup threw', e) } }
106
+ }
107
+
94
108
  export const instances = new WeakMap<Element, Instance>()
95
109
  export function instanceOf(el: Element): Instance | undefined { return instances.get(el) }
96
110
  /** Instance of the nearest ancestor HOST. A host that has not mounted yet (chunk loading) yields null: wait, never skip to a farther ancestor. */