render: expand ChildList templates from the data model - #49
Merged
Conversation
A ChildList's dynamic template form now renders instead of drawing the "[a2tea: dynamic children not yet supported]" placeholder. The template's Path resolves in the surface data model to a list, and the template component renders once per element with bindings scoped to that element: lookupBinding consults the template scope stack innermost-first (an empty path or "/" is the element itself; nested segments traverse maps and numeric list indices) before falling back to the surface data model, so instances can mix per-element and shared values. updateDataModel on the bound list grows or shrinks the children on the next render. An unresolved path or empty list expands to no children; a non-list value renders a caption diagnostic. Cycle safety rides the existing seen-set guard in renderComponent, so a template component referencing its own container reports a cycle per instance instead of recursing. childIDs now counts the template component as referenced, keeping root derivation on the container and letting the focus walk reach the template subtree. Covers static, template, grow/shrink, empty/absent-list, scalar-element, nested-path, scope-fallback, non-list, root-derivation, and cycle cases in render/template_test.go. Closes #44 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
joestump
commented
Jul 18, 2026
joestump
left a comment
Collaborator
Author
There was a problem hiding this comment.
Adversarial review of the ChildList template implementation against #44. Verified locally on the branch: go build ./... && go vet ./... && go test ./... all green, plus independent probe tests for cases the PR's suite doesn't cover (nested templates, a self-nested template component, buttons/textfields inside a template, JSON null at the template path).
Blockers
None found.
What I specifically hunted and confirmed correct:
- Scope stack correctness (
render/template.go:27-38): push/pop around each instance is balanced, innermost-first lookup inlookupBindinghandles nested templates correctly (verified: an outer/groupstemplate containing an inner/memberstemplate renders all leaves with correct element scoping), and outside templates the stack is empty sodynStringbehavior is byte-identical to the olds.datalookup — including the""key for/paths, matchingapplyDataModel's storage. - Cycle safety: rides
renderComponent's unwound seen set, so N sequential instances of the same template component are not mistaken for a cycle (the 3-element render test would fail otherwise), while a template component wrapping its own container — and even a component that is its own nested-template target (root -> grp -> grp) — reports[a2tea: cycle at ...]instead of recursing. Verified both. - Root/focus plumbing (
childListIDs): the template component counts as referenced in bothrootID(NewSurface) andderiveRootID(applyComponents), andcollectFocusableswalks into the template subtree.TestTemplateRootDerivationgenuinely exercises the declaration-order trap. []stringhandling:asList's reflection path converts typed slices element-by-element; the scalar-element test asserts real content and the List bullet chrome, not mere execution.- No bypasses left: every
ChildListconsumer routes throughrenderChildren/childListIDs; the only remaining directs.dataread iseditSeed, which runs at edit time when the scope stack is provably empty — equivalent. - Test quality: assertions are behavioral (content presence, ordering via
strings.Index, absence after shrink, placeholder leakage), not smoke tests. Grow and shrink are both asserted.
All four Done-when items are satisfied (docs update done in docs/wire-format.md; website/ correctly deferred to the docs pass per the workflow rule).
Nits (non-blocking)
render/template.go:31— JSON null renders a producer-error diagnostic.updateDataModel {path: "/items", value: null}is a natural JSON idiom for clearing a list, butasList(nil)reports false (reflect.Invalid), so the surface shows[a2tea: template path "/items" is not a list]instead of expanding to no children. Consider treating a nil value like an unresolved path (return nil before theasListcheck) so agents can clear a list with null.- Shared focus across instances. An interactive component inside a template is one focusable ID: with 2+ elements, focusing it renders every instance in focused (reverse) style simultaneously, and activation/
FieldValuescarry no element identity. This is consistent with the surface's documented adjacency-list-reuse semantics ("one interactive element however many times it is drawn") and out of #44's scope, but it's worth a sentence indocs/wire-format.md's template paragraph so producers don't put buttons in templates expecting per-row actions. resolveElementPathdoesn't unescape JSON Pointer~0/~1tokens. Matches the pre-existing flat data model (applyDataModeldoesn't either), so this is a consistency nit, not a regression — but if element keys can contain/or~it will misresolve. Same bucket: sub-path updates (/items/0/name) don't merge into the stored/itemslist; that's the pre-existing flat-model limitation, and template lists correctly respond to whole-list updates as the ticket requires.
Verdict: approve-quality. Nothing here ships a bug or misses the ticket contract.
🤖 Posted on behalf of @joestump by Claude.
Resolve docs/wire-format.md conflict by keeping both newly implemented bullets (ChildList templates and the createSurface no-op from #47) and dropping both from the 'Not yet' list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
joestump
added a commit
that referenced
this pull request
Jul 18, 2026
Union merge of interactive tab switching (#45) with the five feature PRs that landed on main since the branch forked (#48 createSurface no-op, #49 ChildList templates, #51 Modal open/close, #52 editable inputs, #53 InputSubmitted/ChoiceSelected dispatch). Nothing from either side was dropped: - render/render.go: Surface keeps ALL state — activeTabs (branch) plus fieldValues/checkValues/choiceValues/sliderValues/choiceCursor/ openModals/scope (main). isInteractive now also covers non-empty Tabs bars. collectFocusables walks only the ACTIVE tab's subtree, so inputs and modals inside inactive tabs are excluded from the focus ring, and only an open modal's content joins it. Key routing: left/right step a focused slider and switch tabs on a focused tab bar; h/l switch tabs only on the tab bar and stay literal runes on a focused TextField/DateTimeInput; enter/space/esc keep main's button/checkbox/choice/modal/InputSubmitted behavior. - render/composite.go: deleteSurface clears every state map including activeTabs; applyComponents preserves active-tab, focus, modal, and edited-value state across merges, with out-of-range tabs clamping to the first at read time. - render/tabs_test.go: two new tests pin the union — slider-focused vs tab-bar-focused left/right disambiguation, and inputs/modals inside an inactive tab excluded from the focus ring. - README.md / docs/wire-format.md: merged feature descriptions; the 'not yet' lists are now empty (issues #42–#47 all landed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
What
Implements the dynamic-template form of
ChildList. A container whoseChildren.Templateis set now renders one instance of the template component per element of the bound data-model list, instead of the[a2tea: dynamic children not yet supported]placeholder.updateDataModelon the list grows or shrinks the children on the next render.How
render/template.go(new):renderTemplateChildrenresolvesTemplate.Pathin the data model, converts the value to a list ([]anyfast path, reflection for typed Go slices like[]string), and rendersTemplate.ComponentIDonce per element with that element pushed onto a scope stack.lookupBindingconsults the scope stack innermost-first — an empty path or/is the element itself, and multi-segment paths traverse nested maps and numeric list indices — before falling back to the surface data model, so instances can mix per-element and shared values.dynStringnow routes through it; behavior outside templates is unchanged.childIDscounts the template component as referenced via the newchildListIDshelper, so root derivation stays on the container and the focus walk reaches interactive components inside the template subtree.[a2tea: ...]style. Cycle safety ridesrenderComponent's existing ancestor-path seen set (the Surface renderer falsely reports a cycle when a component is referenced by two parents #10 behavior): a template component referencing its own container reports a cycle per instance instead of recursing.docs/wire-format.md: movedChildListtemplates from the "Not yet" list to implemented, with a description of the scoping rules.Tests
render/template_test.gocovers: template render with per-element bindings and ordering, grow/shrink viaupdateDataModel, empty and absent lists, cycle through the container, scalar[]stringelements with/self-binding (plus List bullet chrome), scope fallback to the surface data model, nested element paths, non-list diagnostic, root derivation with the template component declared first, and the static-ID form staying unaffected.go build ./... && go vet ./... && go test ./...all green.Note: the
website/docs "Not yet" list (which links these gaps to backlog tickets) is deliberately untouched — it's deferred to the post-merge docs pass.Fixes #44
🤖 Posted on behalf of
@joestumpby Claude.