Skip to content

fix(render,chat): A2UI datamodel back-propagation (Finding K)#231

Merged
blove merged 1 commit into
mainfrom
claude/fix-a2ui-datamodel-back-prop
May 10, 2026
Merged

fix(render,chat): A2UI datamodel back-propagation (Finding K)#231
blove merged 1 commit into
mainfrom
claude/fix-a2ui-datamodel-back-prop

Conversation

@blove
Copy link
Copy Markdown
Contributor

@blove blove commented May 10, 2026

Summary

A2UI input components (TextField, MultipleChoice, CheckBox, Slider, DateTimeInput) have always emitted `a2ui:datamodel::` strings via their `emit` callback when the user changes value. No consumer existed for that format — neither libs/render's `emitFn` nor libs/chat's `` handled the magic-string protocol — so user input went nowhere. Forms rendered but were inert: typing in a TextField didn't update `surface.dataModel`, validation never saw input, Submit stayed disabled, binding-dependent UI never updated.

This was tracked as Finding K through Phase 4 and Phase 5 of the canonical demo's evolution.

Two coordinated fixes

1. `libs/render/src/lib/render-element.component.ts` — `emitFn` interception

`emitFn` now checks for the `a2ui:datamodel:` prefix before delegating to the `el.on[event]` handler binding. When matched, it parses out the path (split on the LAST colon — paths may contain colons per RFC 6901, and values certainly can: URLs, time strings, JSON arrays), coerces the raw string value back to a typed form, and writes via `this.ctx.store.set(path, value)`. The store triggers re-render with the new resolved value.

Type coercion handles: numeric literals (`/^-?\d+(?:\.\d+)?$/`), boolean literals (`true`/`false`), JSON-stringified arrays (MultipleChoice emits these), else string passthrough.

2. `libs/chat/src/lib/a2ui/surface-to-spec.ts` — preserve path refs as `$bindState` markers

Path-bound props were eagerly resolved at conversion time (`resolveDynamic` returned the literal value), so catalog components received a static snapshot that never changed even after datamodel writes hit the store. Fix: leave path refs as `{$bindState: '/path'}` markers — json-render's prop resolution reads the live state store on every render, so user writes propagate immediately. Literal DynamicValues (`{literalString: ...}`, `{literalNumber: ...}`, etc.) still resolve eagerly.

The `_bindings` map (prop name → path) is preserved so catalog components emit the magic string with the correct path on user input.

Verification

Live smoke against the canonical demo's feedback form:

  • TextField receives focus; `(input)` event fires on user typing
  • Catalog `onInput` calls `emitBinding` with prop name + new value
  • `emitBinding` emits `a2ui:datamodel:/form/name:Brian`
  • render-element's `emitFn` intercepts → `store.set('/form/name', 'Brian')`
  • json-render's prop resolution sees the new state on next render
  • TextField's `text()` input resolves to "Brian" via `$bindState`
  • UI updates reactively with the typed value

Confirmed via direct JS introspection:
```js
ng.getComponent(document.querySelector('a2ui-text-field'))?.text?.() // "Brian"
```

Two surface-to-spec tests updated to assert the new `$bindState` marker behavior instead of the old eager-resolution snapshot.

Out of scope (deferred)

  • Syncing json-render store back into `A2uiSurfaceStore.surface.dataModel` — for cross-render persistence (thread reload, view in chat-debug). For Phase 5 the in-form interactivity is the user-visible win; the round-trip (Submit → action message → re-submit) already captures values from the live store via `buildA2uiActionMessage`.

🤖 Generated with Claude Code

A2UI input components (TextField, MultipleChoice, CheckBox, Slider,
DateTimeInput) emit `a2ui:datamodel:<path>:<value>` strings via their
`emit` callback when the user changes value. No consumer existed —
neither libs/render's `emitFn` nor libs/chat's <a2ui-surface> handled
the magic-string format — so user input went nowhere. Forms rendered
but were inert: validation never saw input, Submit stayed disabled,
binding-dependent UI never updated.

Two coordinated fixes:

1. **`libs/render/src/lib/render-element.component.ts`** —
   `emitFn` now intercepts `a2ui:datamodel:<path>:<value>` strings
   before delegating to `el.on[event]` handlers. Splits on the LAST
   colon (path may itself contain colons per RFC 6901; values
   certainly can — URLs, time strings, JSON arrays). Coerces the
   raw string back to typed values: numeric (`/^-?\d+(?:\.\d+)?$/`),
   boolean (`true`/`false`), JSON arrays (passthrough for
   MultipleChoice's stringified arrays), else string. Writes via the
   render context's state store, which triggers re-render with the
   new resolved value.

2. **`libs/chat/src/lib/a2ui/surface-to-spec.ts`** — Path-bound props
   were eagerly resolved at conversion time, so catalog components
   received a static literal snapshot that never changed even after
   datamodel writes hit the store. Now leaves path refs as
   `{$bindState: '/path'}` markers — json-render's prop resolution
   reads the live state store on every render, so user writes
   propagate immediately. Literal DynamicValues (`{literalString:...}`,
   etc.) still resolve eagerly. The `_bindings` prop continues to map
   prop name → path so catalog components emit the magic string with
   the correct path on user input.

Verified live: feedback form's Name TextField receives focus, accepts
input, the catalog component's `text()` input updates reactively from
$bindState resolution against the json-render store. End-to-end data
flow (user types → emit → store.set → $bindState re-resolves →
component re-renders with new value) works.

Two surface-to-spec tests updated to assert $bindState markers
instead of pre-resolved literals.

Out of scope (future): syncing the json-render store back into
`A2uiSurfaceStore.surface.dataModel` so cross-render persistence
(thread reload, view in chat-debug) reflects user input. For Phase 5
the in-form interactivity is the user-visible win; the round-trip
(Submit → action message → re-submit) already captures values from
the live store via `buildA2uiActionMessage`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cacheplane Ready Ready Preview, Comment May 10, 2026 4:30am

Request Review

@blove blove merged commit caf7a71 into main May 10, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant