fix(render,chat): A2UI datamodel back-propagation (Finding K)#231
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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)
🤖 Generated with Claude Code