@brust/runtime-dom 2.0.0-alpha.12 → 2.0.0-alpha.14

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.12",
3
+ "version": "2.0.0-alpha.14",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -0,0 +1,6 @@
1
+ // GENERATED from crates/brust-jinja/src/lib.rs (BOOLEAN_ATTRS, OVERLOADED_ATTRS, STRINGIFY_ATTRS); do not edit.
2
+ // Regenerate: bun scripts/gen-attr-kinds.ts. test/attr-kinds.test.ts fails when this drifts.
3
+ const set = (s: string): Set<string> => new Set(s.split(' '))
4
+ export const BOOLEAN = set('allowfullscreen async autofocus autoplay checked controls default defer disabled disablepictureinpicture disableremoteplayback formnovalidate hidden inert itemscope loop multiple muted nomodule novalidate open playsinline readonly required reversed scoped seamless selected')
5
+ export const OVERLOADED = set('capture download')
6
+ export const STRINGIFY = set('autoreverse contenteditable draggable externalresourcesrequired focusable preservealpha spellcheck value')
@@ -1,10 +1,10 @@
1
1
  import { BINDERS, parsed, read, warnOnce } from '../core-api'
2
+ import { BOOLEAN, OVERLOADED, STRINGIFY } from '../attr-kinds'
2
3
 
3
4
  // Attributes that execute or load code from a string; a bound value must never reach them as-is.
4
5
  const URL_ATTRS = new Set(['href', 'src', 'action', 'formaction', 'poster', 'data', 'xlink:href', 'ping'])
5
6
  // Scheme allowlist on the parsed URL (relative URLs resolve to http): new URL() applies the browser's tab/newline/control-char stripping.
6
7
  const safeUrl = (v: string): boolean => { try { return ['http:', 'https:', 'mailto:', 'tel:'].includes(new URL(v, 'http://x').protocol) } catch { return false } }
7
- const BOOLEAN = new Set(['disabled', 'checked', 'selected', 'readonly', 'required', 'hidden', 'open', 'multiple'])
8
8
  BINDERS.push({
9
9
  match: (a) => a.startsWith('x-bind-'),
10
10
  bind: ({ inst, el, attr, raw, scope }) => {
@@ -23,7 +23,12 @@ BINDERS.push({
23
23
  if (on) el.setAttribute(name, '') ; else el.removeAttribute(name)
24
24
  return
25
25
  }
26
- if (v == null || v === false) { el.removeAttribute(name); return }
26
+ // 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 (v == null || typeof v === 'boolean') { el.removeAttribute(name); return }
30
+ // 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 }
27
32
  if (URL_ATTRS.has(name) && !safeUrl(String(v))) { warnOnce(`b:${inst.name}:${name}`, `${attr} in ${inst.name}: unsafe URL refused`); return }
28
33
  el.setAttribute(name, String(v))
29
34
  })
@@ -1,10 +1,22 @@
1
1
  import { BINDERS, parsed, member } from '../core-api'
2
+
3
+ // React's SyntheticEvent has five members a DOM event lacks; code written for React calls them on the event it is handed
4
+ // (Radix: `event.isPropagationStopped()`), and on a DOM event that is a TypeError (F195). Added to the event the handler
5
+ // receives, never to Event.prototype, so nothing else on the page changes.
6
+ const SYNTHETIC: PropertyDescriptorMap = {
7
+ isPropagationStopped: { value(this: Event) { return this.cancelBubble } },
8
+ isDefaultPrevented: { value(this: Event) { return this.defaultPrevented } },
9
+ persist: { value() {} },
10
+ isPersistent: { value: () => true },
11
+ nativeEvent: { get(this: Event) { return this } },
12
+ }
2
13
  BINDERS.push({
3
14
  match: (a) => a.startsWith('x-on-'),
4
15
  bind: ({ inst, el, attr, raw, scope }) => {
5
16
  const event = attr.slice('x-on-'.length)
6
17
  const p = parsed(inst, attr, raw); if (!p) return
7
18
  const handler = (e: Event) => {
19
+ if (!('isPropagationStopped' in e)) Object.defineProperties(e, SYNTHETIC)
8
20
  const fn = member(inst, p, attr)
9
21
  if (typeof fn !== 'function') return
10
22
  const args = p.bindings.map((b) => scope[b])
package/src/instance.ts CHANGED
@@ -58,13 +58,14 @@ export class Instance {
58
58
  return true
59
59
  }
60
60
  onCleanup(fn: () => void): void { this.cleanups.push(fn) }
61
- ref<E extends Element = HTMLElement>(name: string): { current: E | null } {
61
+ ref<E extends Element = HTMLElement>(name: string, init?: unknown): { current: E | null } {
62
62
  let r = this.refs.get(name)
63
- if (!r) { r = { current: null }; this.refs.set(name, r) }
63
+ // `useRef(init)`: the cell starts at `init`; an `x-ref` element replaces it when it binds (React attaches the same way).
64
+ if (!r) { r = { current: (init ?? null) as Element | null }; this.refs.set(name, r) }
64
65
  return r as { current: E | null }
65
66
  }
66
67
  ctx(): BehaviorCtx {
67
- return { el: this.host, props: this.props, effect: (fn) => this.componentEffect(fn), onCleanup: (fn) => this.onCleanup(fn), ref: (n) => this.ref(n) }
68
+ return { el: this.host, props: this.props, effect: (fn) => this.componentEffect(fn), onCleanup: (fn) => this.onCleanup(fn), ref: (n, init) => this.ref(n, init) }
68
69
  }
69
70
  init(factory: BehaviorFactory): void {
70
71
  const m = factory(this.ctx())
package/src/registry.ts CHANGED
@@ -5,7 +5,7 @@ export interface BehaviorCtx {
5
5
  props: Signal<Record<string, unknown>>
6
6
  effect: (fn: () => void | (() => void)) => Dispose
7
7
  onCleanup: (fn: () => void) => void
8
- ref: <E extends Element = HTMLElement>(name: string) => { current: E | null }
8
+ ref: <E extends Element = HTMLElement>(name: string, init?: unknown) => { current: E | null }
9
9
  }
10
10
  export type BehaviorInstance = Record<string, unknown>
11
11
  export type BehaviorFactory = (ctx: BehaviorCtx) => BehaviorInstance | void