@brust/runtime-dom 2.0.0-alpha.14 → 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 +1 -1
- package/src/directives/bind.ts +6 -4
- package/src/directives/common.ts +1 -0
- package/src/instance.ts +3 -0
- package/src/mount.ts +9 -2
- package/src/props-bind.ts +10 -0
package/package.json
CHANGED
package/src/directives/bind.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
})
|
package/src/directives/common.ts
CHANGED
|
@@ -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
|
|
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)
|