Skip to content

Feature: add two-way data binding via ::prop in comark core #437

Description

@miguelrk

Description

Comark data binding is read-only. :prop="dot.path" resolves against the render context; {{ }} and ::if read the same context. There is no write channel.

Add ::prop="dot.path" in comark core so any component or native form element can write back to a small ComarkModel protocol. Native HTML form elements become the input surface — no first-party Inputs library.

Volume: :input{type="range" min="0" max="100" ::value="data.volume"}

The volume is **{{ data.volume }}%**.

::if{:value="data.volume" :gte="90"}
That's loud.
::

::prop is attribute-position only. It does not collide with block ::name, inline :name, one-way :prop, or {{ }}. The props parser currently drops ::value, so the namespace is unclaimed.

This belongs in core, not @comark/binding. Two-way binding extends the attribute-resolution contract in resolveAttributes() / NodeRenderData, consumed by all renderers.

flowchart LR
  MD["Markdown<br/><b>::value=#quot;data.volume#quot;</b>"]
  Core["comark core<br/>parse · resolveAttributes"]
  Model["ComarkModel<br/>get / set / subscribe"]
  UI["Renderers<br/>input · Binding · If"]

  MD -->|parse| Core
  Core -->|"X + onUpdate:X"| UI
  UI -->|set| Model
  Model -->|subscribe| UI

  classDef src fill:#ecfdf5,stroke:#059669,color:#065f46
  classDef core fill:#eff6ff,stroke:#2563eb,color:#1e3a8a
  classDef model fill:#fff7ed,stroke:#ea580c,color:#9a3412
  classDef ui fill:#f0f9ff,stroke:#0284c7,color:#0c4a6e

  class MD src
  class Core core
  class Model model
  class UI ui

  linkStyle 0,1 stroke:#2563eb,stroke-width:2px
  linkStyle 2 stroke:#ea580c,stroke-width:2px
  linkStyle 3 stroke:#0284c7,stroke-width:2px
Loading

Motivation

Interactive Markdown today needs either host-owned wrappers per control or an Inputs-style catalog. Both bypass the render context and do not work for plain HTML. A write at the attribute layer updates every reader of that path ({{ }}, ::if, other :prop bindings) without a component catalog.

Proposed solution

Syntax — Widen the props-key regex to accept ::. Reject a :: key with no value.

:input{type="text" ::value="data.name"}
:input{type="checkbox" ::checked="data.active"}
::dialog{::open="data.showHelp"}

ComarkModel (comark/model) — Dependency-free protocol: get / set / subscribe (optional batch). Ship createModelStore(). Hosts can pass their own implementation (Vue reactive(), Zustand, …). Default store is per render root.

Attribute resolution — For ::X="path" with a model: emit X (current value) + an update callback (onUpdate:X / onUpdateX). Filtered expressions, non-writable paths, and missing model degrade to one-way (warn in prod / throw in dev where appropriate). Reuse existing URL/HTML-sink guards for ::href / ::src / innerHTML. set() rejects prototype-pollution segments and paths outside writable (default: ['data']).

Native form elements — Shared helper wires input / select / textarea (value / checked / files) with type coercion. Custom components receive X + update callback and write themselves.

Renderers — React / Vue / Svelte / Angular bridge via subscriptions. HTML SSR emits inert data-comark-model* attrs; optional @comark/html/runtime for progressive enhancement. ANSI resolves ::X as :X (one dev warning). Markdown gains model / onModelChange; default creates a store from data when no model is passed.

::form — Optional aggregate that collects child binding paths into one object (same layering as Binding / If).

Out of scope

  • First-party Inputs library (use native :input{type=… ::value=…})
  • Table-as-input / structural widgets (component-library concern; same split as #427)
  • Attribute modifiers (.number / .lazy / .trim) — leave . in keys unclaimed
  • Shipping a reactivity engine in core (alien-signals etc. stay optional / separate)

Constraints

  • #286::prop is inline-attribute-only until YAML block props resolve bindings
  • #296 — Angular adapter after signal-input modernisation
  • #427 / PR #429 — filtered expressions are not writable
  • PR #415 — inside ::for, settle index-aware paths (item.donedata.items[i].done) jointly

Additional context

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions