{"version":3,"file":"bindings.cjs","names":[],"sources":["../../src/template/bindings.ts"],"sourcesContent":["/**\n * template/bindings.ts — Runtime binding appliers.\n *\n * Responsibilities:\n * - Apply each Binding variant to the live DOM (attr, event, html, ref,\n *   directive).\n * - Manage reactive effects and cleanup registration.\n * - Expose `applyBinding()` as the single dispatch entry point.\n * - Own the signal-to-form-control write path (`syncFormControl`) for regular\n *   attribute bindings.\n */\n\nimport { computed, isReactive, type Readable, effect as rawEffect, untrack } from '@vielzeug/ripple';\n\nimport { isLiveBinding } from '../directives/live';\nimport { invariant } from '../errors';\nimport { getPropMeta, type PropMeta } from '../props';\nimport { createReplaceableSlot, isStructuredValue, listen, setAttr } from '../utils/dom';\nimport type {\n  AttrBinding,\n  Binding,\n  DirectiveBinding,\n  EventBinding,\n  HtmlBinding,\n  HtmlBindingValue,\n  RefBinding,\n} from './binding-types';\nimport { isHtmlResult } from './result';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type RegisterCleanup = (fn: () => void) => void;\n\n// ─── Signal helpers ───────────────────────────────────────────────────────────\n\nconst signalEffect = (\n  signal: Readable<unknown>,\n  update: (v: unknown) => void,\n  registerCleanup: RegisterCleanup,\n): void => {\n  const sub = rawEffect(() => {\n    update(signal.value);\n    return undefined;\n  });\n\n  registerCleanup(() => sub.dispose());\n};\n\n// ─── Form control value sync ──────────────────────────────────────────────────\n\nconst isNativeFormInput = (el: HTMLElement): el is HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement =>\n  el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement;\n\nconst isCheckableInput = (el: HTMLElement): el is HTMLInputElement =>\n  el instanceof HTMLInputElement && (el.type === 'checkbox' || el.type === 'radio');\n\ntype LiveWriteState = { last: unknown };\n\n/**\n * The single write path from a reactive value to a form control's `value`/`checked`\n * property for regular attribute bindings.\n *\n * Live-write: when the binding was created from `live(source)`, a write is skipped\n * if the DOM value has diverged from this binding's last write (in-progress user\n * input) — unless the incoming value already matches the DOM (write would be a no-op).\n */\nexport const syncFormControl = (\n  el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,\n  value: unknown,\n  isLive?: boolean,\n  state: LiveWriteState = { last: undefined },\n): void => {\n  const checkable = isCheckableInput(el);\n  const next: boolean | string = checkable ? Boolean(value) : value == null ? '' : String(value);\n  const current: boolean | string = checkable ? (el as HTMLInputElement).checked : el.value;\n\n  if (isLive && state.last !== undefined && !Object.is(current, state.last) && !Object.is(current, next)) return;\n\n  if (checkable) (el as HTMLInputElement).checked = next as boolean;\n  else el.value = next as string;\n\n  if (isLive) state.last = next;\n};\n\n// ─── Attributes ───────────────────────────────────────────────────────────────\n\nconst syncRegisteredProp = (el: HTMLElement, meta: PropMeta, binding: AttrBinding, value: unknown): void => {\n  const parsed = isStructuredValue(value)\n    ? value\n    : meta.parse(\n        binding.mode === 'bool' ? (value ? '' : null) : value == null || value === false ? null : String(value),\n      );\n\n  if (\n    !Object.is(\n      untrack(() => meta.signal.value),\n      parsed,\n    )\n  ) {\n    meta.signal.value = parsed;\n  }\n\n  if (!meta.reflect) {\n    if (isStructuredValue(value)) return;\n\n    if (binding.mode === 'bool') el.toggleAttribute(binding.name, Boolean(value));\n    else setAttr(el, binding.name, value);\n  }\n};\n\nexport const applyAttrBinding = (binding: AttrBinding, registerCleanup: RegisterCleanup): void => {\n  const { el, mode, name, propMeta } = binding;\n  const liveState: LiveWriteState = { last: undefined };\n\n  const update = (value: unknown): void => {\n    if (propMeta) {\n      syncRegisteredProp(el, propMeta, binding, value);\n\n      return;\n    }\n\n    if (!isReactive(value) && isStructuredValue(value)) {\n      if (name !== '__proto__' && name !== 'constructor' && name !== 'prototype') {\n        (el as unknown as Record<string, unknown>)[name] = value;\n      }\n\n      return;\n    }\n\n    if ((name === 'value' && isNativeFormInput(el)) || (name === 'checked' && el instanceof HTMLInputElement)) {\n      syncFormControl(el as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, value, binding.live, liveState);\n\n      return;\n    }\n\n    if (mode === 'bool') el.toggleAttribute(name, Boolean(value));\n    else setAttr(el, name, value);\n  };\n\n  if ('signal' in binding) {\n    signalEffect(binding.signal, update, registerCleanup);\n  } else {\n    update(binding.value);\n  }\n};\n\n// ─── Events ───────────────────────────────────────────────────────────────────\n\nconst applyEventBinding = (binding: EventBinding, registerCleanup: RegisterCleanup): void => {\n  registerCleanup(listen(binding.el, binding.name, binding.handler));\n};\n\n// ─── Refs ─────────────────────────────────────────────────────────────────────\n\nconst applyRefBinding = (binding: RefBinding, registerCleanup: RegisterCleanup): void => {\n  const { el, ref } = binding;\n\n  if (typeof ref === 'function') {\n    ref(el as never);\n    registerCleanup(() => ref(null as never));\n\n    return;\n  }\n\n  ref.value = el as never;\n  registerCleanup(() => {\n    ref.value = null as never;\n  });\n};\n\n// ─── HTML (reactive) ─────────────────────────────────────────────────────────\n\nconst insertHtmlValues = (\n  values: HtmlBindingValue[],\n  insertBefore: ChildNode,\n  registerCleanup: RegisterCleanup,\n): Node[] => {\n  const nodes: Node[] = [];\n  const parent = insertBefore.parentNode;\n\n  invariant(parent, 'html binding anchor has no parent node');\n\n  for (const v of values) {\n    if (isHtmlResult(v)) {\n      const captured = Array.from(v.fragment.childNodes);\n\n      parent.insertBefore(v.fragment, insertBefore);\n      v.apply(registerCleanup);\n      nodes.push(...captured);\n    } else if (v != null && v !== false) {\n      const text = document.createTextNode(String(v));\n\n      parent.insertBefore(text, insertBefore);\n      nodes.push(text);\n    }\n  }\n\n  return nodes;\n};\n\nexport const applyHtmlBinding = (binding: HtmlBinding, registerCleanup: RegisterCleanup): void => {\n  const { anchor, signal } = binding;\n\n  const slot = createReplaceableSlot();\n\n  const stop = rawEffect(() => {\n    const raw = signal.value;\n\n    slot.clear();\n\n    if (raw == null || raw.length === 0) return;\n\n    untrack(() => {\n      slot.setNodes(insertHtmlValues(raw, anchor, slot.registerCleanup));\n    });\n  });\n\n  registerCleanup(() => {\n    stop.dispose();\n    slot.clear();\n  });\n};\n\n// ─── Directives ───────────────────────────────────────────────────────────────\n\nconst applyDirectiveBinding = (binding: DirectiveBinding, registerCleanup: RegisterCleanup): void => {\n  binding.directive.mount(binding.anchor, registerCleanup);\n};\n\n// ─── Binding dispatch ─────────────────────────────────────────────────────────\n\nexport const applyBinding = (binding: Binding, registerCleanup: RegisterCleanup): void => {\n  switch (binding.type) {\n    case 'attr':\n      applyAttrBinding(binding, registerCleanup);\n      break;\n    case 'directive':\n      applyDirectiveBinding(binding, registerCleanup);\n      break;\n    case 'event':\n      applyEventBinding(binding, registerCleanup);\n      break;\n    case 'html':\n      applyHtmlBinding(binding, registerCleanup);\n      break;\n    case 'ref':\n      applyRefBinding(binding, registerCleanup);\n      break;\n  }\n};\n\n// ─── Attr binding factory (used by instantiator) ──────────────────────────────\n\nexport const createAttrBindingFromValue = (\n  el: HTMLElement,\n  mode: 'attr' | 'bool',\n  name: string,\n  value: unknown,\n): AttrBinding => {\n  const propMeta = getPropMeta(el, name);\n\n  if (isLiveBinding(value)) {\n    return { el, live: true, mode, name, propMeta, signal: value.source, type: 'attr' };\n  }\n\n  if (typeof value === 'function') {\n    return { el, mode, name, propMeta, signal: computed(value as () => unknown), type: 'attr' };\n  }\n\n  if (isReactive(value)) {\n    return { el, mode, name, propMeta, signal: value as Readable<unknown>, type: 'attr' };\n  }\n\n  return { el, mode, name, propMeta, type: 'attr', value };\n};\n\nexport const resolveStaticText = (value: unknown): string => {\n  if (value == null) return '';\n\n  return String(value);\n};\n"],"mappings":"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"}