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

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.24",
3
+ "version": "2.0.0-alpha.25",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -1,3 +1,4 @@
1
+ import { reactEvent } from './on'
1
2
  import { BINDERS, parsed, read, warnOnce } from '../core-api'
2
3
  import { BOOLEAN, OVERLOADED, STRINGIFY } from '../attr-kinds'
3
4
  import type { Instance } from '../instance'
@@ -11,6 +12,8 @@ function writeAttr(inst: Instance, attr: string, el: Element, name: string, v: u
11
12
  // setAttribute: `className` is a getter-only SVGAnimatedString on SVG elements (F96a).
12
13
  // F311: undefined/null removes `class` (React does; `className=""` keeps an empty one), and an empty `style` is no attribute (the first paint omits it)
13
14
  if (name === 'class') { if (v == null) el.removeAttribute('class'); else el.setAttribute('class', `${v}`); return }
15
+ // F434: a multiple select is bound to an array, one pick per option (`null`/`undefined` leave the picks alone, as React does)
16
+ if (name === 'value' && el instanceof HTMLSelectElement && el.multiple) { if (v != null) { const vs = ([] as unknown[]).concat(v).map(String); for (const o of el.options) o.selected = vs.includes(o.value) } return }
14
17
  if (name === 'value' && 'value' in el) { (el as HTMLInputElement).value = v == null ? '' : String(v); return }
15
18
  // React passes every attribute of a custom element (a tag with `-`) through as given: `true` is ``, `false` omits it.
16
19
  const custom = el.localName.includes('-')
@@ -54,6 +57,20 @@ BINDERS.push({
54
57
  match: (a) => a.startsWith('x-prop-'),
55
58
  bind: ({ inst, el, attr, raw }) => {
56
59
  const name = attr.slice('x-prop-'.length)
60
+ // F369c: `x-prop-on-<event>="<prop key>"` calls the instance's prop (a function a caller handed through the child's rest)
61
+ // with the event, reading it at call time so a prop that changes is the one that runs. The event is `[a-z0-9]+`.
62
+ if (name.startsWith('on-')) {
63
+ const event = name.slice(3)
64
+ if (!raw || !/^[a-z0-9]+$/.test(event)) { warnOnce(`pb:${inst.name}:${name}`, `${attr} in ${inst.name} refused`); return }
65
+ const handler = (e: Event) => {
66
+ reactEvent(e)
67
+ const ps = inst.props(); const fn = Object.hasOwn(ps, raw) ? ps[raw] : undefined
68
+ if (typeof fn === 'function') (fn as (e: Event) => void)(e)
69
+ }
70
+ el.addEventListener(event, handler)
71
+ inst.onCleanup(() => el.removeEventListener(event, handler))
72
+ return
73
+ }
57
74
  if (!name || !raw || name.startsWith('on') || name.startsWith('x-') || PROP_REFUSED.has(name)) {
58
75
  warnOnce(`pb:${inst.name}:${name}`, `${attr} in ${inst.name} refused`); return
59
76
  }
@@ -17,9 +17,10 @@ BINDERS.push({
17
17
  const h = () => { if (input.checked) s.set(input.value) }; el.addEventListener('change', h); inst.onCleanup(() => el.removeEventListener('change', h))
18
18
  inst.effect(() => { input.checked = s() === input.value })
19
19
  } else if (el instanceof HTMLSelectElement) {
20
- if (el.multiple) { warnOnce(`mm:${inst.name}`, `select[multiple] unsupported`); return }
21
- const h = () => s.set(el.value); el.addEventListener('change', h); inst.onCleanup(() => el.removeEventListener('change', h))
22
- const apply = () => { const v = s(); if (el.value !== String(v)) el.value = String(v ?? '') }
20
+ // F434: a multiple select is an array of the picked values
21
+ const many = el.multiple
22
+ const h = () => s.set(many ? Array.from(el.options).filter((o) => o.selected).map((o) => o.value) : el.value); el.addEventListener('change', h); inst.onCleanup(() => el.removeEventListener('change', h))
23
+ const apply = () => { const v = s(); if (many) { if (v != null) { const vs = ([] as unknown[]).concat(v).map(String); for (const o of el.options) o.selected = vs.includes(o.value) } } else if (el.value !== String(v)) el.value = String(v ?? '') }
23
24
  inst.effect(apply)
24
25
  const mo = new MutationObserver(() => apply()); mo.observe(el, { childList: true }); inst.onCleanup(() => mo.disconnect())
25
26
  } else {
@@ -10,13 +10,17 @@ const SYNTHETIC: PropertyDescriptorMap = {
10
10
  isPersistent: { value: () => true },
11
11
  nativeEvent: { get(this: Event) { return this } },
12
12
  }
13
+ /** Gives a DOM event the React members (once). Shared with `x-prop-on-*`. */
14
+ export function reactEvent(e: Event): void {
15
+ if (!('isPropagationStopped' in e)) Object.defineProperties(e, SYNTHETIC)
16
+ }
13
17
  BINDERS.push({
14
18
  match: (a) => a.startsWith('x-on-'),
15
19
  bind: ({ inst, el, attr, raw, scope }) => {
16
20
  const event = attr.slice('x-on-'.length)
17
21
  const p = parsed(inst, attr, raw); if (!p) return
18
22
  const handler = (e: Event) => {
19
- if (!('isPropagationStopped' in e)) Object.defineProperties(e, SYNTHETIC)
23
+ reactEvent(e)
20
24
  const fn = member(inst, p, attr)
21
25
  if (typeof fn !== 'function') return
22
26
  const args = p.bindings.map((b) => scope[b])