@brust/runtime-dom 2.0.0-alpha.15 → 2.0.0-alpha.16

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.15",
3
+ "version": "2.0.0-alpha.16",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -17,18 +17,20 @@ BINDERS.push({
17
17
  // setAttribute: `className` is a getter-only SVGAnimatedString on SVG elements (F96a).
18
18
  if (name === 'class') { el.setAttribute('class', v == null ? '' : String(v)); return }
19
19
  if (name === 'value' && 'value' in el) { (el as HTMLInputElement).value = v == null ? '' : String(v); return }
20
- if (BOOLEAN.has(name)) {
20
+ // React passes every attribute of a custom element (a tag with `-`) through as given: `true` is ``, `false` omits it.
21
+ const custom = el.localName.includes('-')
22
+ if (!custom && BOOLEAN.has(name)) {
21
23
  const on = Boolean(v)
22
24
  if (name in el) (el as unknown as Record<string, unknown>)[name] = on
23
25
  if (on) el.setAttribute(name, '') ; else el.removeAttribute(name)
24
26
  return
25
27
  }
26
28
  // aria-*, data-* and the booleanish strings print `true`/`false`; download/capture print `true` as ``; on any other attribute React omits a boolean.
27
- if (name.startsWith('aria-') || name.startsWith('data-') || STRINGIFY.has(name)) { if (v == null) el.removeAttribute(name); else el.setAttribute(name, String(v)); return }
28
- if (OVERLOADED.has(name) && typeof v === 'boolean') { if (v) el.setAttribute(name, ''); else el.removeAttribute(name); return }
29
+ if (!custom && (name.startsWith('aria-') || name.startsWith('data-') || STRINGIFY.has(name))) { if (v == null) el.removeAttribute(name); else el.setAttribute(name, String(v)); return }
30
+ if ((custom || OVERLOADED.has(name)) && typeof v === 'boolean') { if (v) el.setAttribute(name, ''); else el.removeAttribute(name); return }
29
31
  if (v == null || typeof v === 'boolean') { el.removeAttribute(name); return }
30
32
  // React writes no `src=""` / `href=""` (it would load the page again); an anchor keeps its empty href.
31
- if (v === '' && (name === 'src' || name === 'href') && el.localName !== 'a') { el.removeAttribute(name); return }
33
+ if (v === '' && (name === 'src' || name === 'href') && el.localName !== 'a' && !custom) { el.removeAttribute(name); return }
32
34
  if (URL_ATTRS.has(name) && !safeUrl(String(v))) { warnOnce(`b:${inst.name}:${name}`, `${attr} in ${inst.name}: unsafe URL refused`); return }
33
35
  el.setAttribute(name, String(v))
34
36
  })
@@ -23,5 +23,6 @@ export function member(inst: Instance, p: ParsedValue, attr: string): unknown {
23
23
  }
24
24
  export function mismatch(inst: Instance, attr: string, _el: Element, server: unknown, client: unknown): void {
25
25
  if (inst.booted) return // element created after mount (new x-for row / x-if clone): nothing was server-rendered
26
+ if (inst.reseeded) return // F277: a host created later, started from its parent's current props, not the printed ones
26
27
  warnOnce(`x:${inst.name}:${attr}`, `first-paint mismatch ${attr} in ${inst.name}: ${JSON.stringify(server)} vs ${JSON.stringify(client)}`)
27
28
  }
package/src/instance.ts CHANGED
@@ -21,6 +21,9 @@ export class Instance {
21
21
  disposed = false
22
22
  /** false until the initial bind pass finishes; the first-paint tripwire only applies before then. */
23
23
  booted = false
24
+ /** F277: started from props other than the ones its markup was printed with (a host an `x-if` or a row created after
25
+ * first paint, linked to its parent's current value): its first bind is not a first paint, so no tripwire. */
26
+ reseeded = false
24
27
  /** Effects the component declared while its factory ran: they start once the instance is bound (x-ref, x-on), like React's commit (F97). */
25
28
  private pending: Array<[() => void | (() => void), Dispose?]> | null = []
26
29
 
package/src/mount.ts CHANGED
@@ -4,7 +4,7 @@ import { whenBehavior } from './registry'
4
4
  import { bindHost } from './directives/index'
5
5
  import { insideRaw } from './directives/common'
6
6
  import { warnOnce } from './warn'
7
- import { bindPropsFromParent } from './props-bind'
7
+ import { bindPropsFromParent, initialProps } from './props-bind'
8
8
 
9
9
  let observer: MutationObserver | null = null
10
10
 
@@ -15,13 +15,20 @@ function readProps(host: HTMLElement): Record<string, unknown> {
15
15
  catch { warnOnce(`j:${host.outerHTML.slice(0, 80)}`, `bad x-props JSON in ${host.getAttribute('x-data')}`); return {} }
16
16
  }
17
17
 
18
+ function sameJson(a: unknown, b: unknown): boolean {
19
+ try { return JSON.stringify(a) === JSON.stringify(b) } catch { return false }
20
+ }
21
+
18
22
  function mountHost(host: HTMLElement): void {
19
23
  if (instances.has(host) || !host.isConnected) return
20
24
  const name = host.getAttribute('x-data')!
21
25
  whenBehavior(name, (factory) => {
22
26
  if (instances.has(host) || !host.isConnected) return
23
27
  const parent = nearestInstance(host)
24
- const inst = new Instance(host, name, parent, readProps(host))
28
+ const printed = readProps(host)
29
+ const props = initialProps(host, parent, printed)
30
+ const inst = new Instance(host, name, parent, props)
31
+ inst.reseeded = props !== printed && !sameJson(props, printed)
25
32
  instances.set(host, inst)
26
33
  try { inst.init(factory) } catch (e) { console.error(`[brust] init threw: ${name}`, e); inst.dispose(); instances.delete(host); return }
27
34
  bindHost(inst, host, {})
package/src/props-bind.ts CHANGED
@@ -9,6 +9,16 @@ function scopeOf(el: Element): Record<string, unknown> {
9
9
  return {}
10
10
  }
11
11
 
12
+ /** F277: the props a linked host starts with: its parent's current `x-props-bind` value when the parent is mounted (a
13
+ * host an `x-if` or a row creates after first paint gets what the parent passes NOW, as React mounts with the current
14
+ * props: a `useState(prop)` starts from it), else its printed `x-props`. */
15
+ export function initialProps(host: HTMLElement, parent: Instance | null, printed: Record<string, unknown>): Record<string, unknown> {
16
+ const raw = host.getAttribute('x-props-bind'); if (!raw || !parent) return printed
17
+ const p = parseValue(raw); if (!p) return printed
18
+ const r = resolve(parent.members, p, scopeOf(host))
19
+ return r.ok && r.value && typeof r.value === 'object' ? (r.value as Record<string, unknown>) : printed
20
+ }
21
+
12
22
  export function bindPropsFromParent(inst: Instance): void {
13
23
  const raw = inst.host.getAttribute('x-props-bind'); if (!raw) return
14
24
  const p = parseValue(raw)