Skip to content

fix(iur_hydration): expand_list_repeat traverse wrapper :child relationship for two-level row templates (#114) - #118

Merged
pcharbon70 merged 2 commits into
mainfrom
claude/iur-hydration-two-level-traversal-issue-114
May 18, 2026
Merged

pcharbon70 merged 2 commits into
mainfrom
claude/iur-hydration-two-level-traversal-issue-114

Conversation

@ty13r

@ty13r ty13r commented May 18, 2026 •

Copy link
Copy Markdown
Member

Summary

Addresses #114. References ariston-ui PR #388 as the concrete consumer.

Pascal-review REQUIRED — DRAFT — no auto-merge regardless of Codex/CI verdict. Happy to revise to Option B or Option C if preferred.


Problem

AshUI.LiveView.IURHydration.expand_list_repeat/1 only pattern-matches on %{"type" => "list_repeat"} nodes. For screens whose composition roots have types like "custom:doc_block_numbered", "artifact_row", or other widget kinds — where the repeat directive lives on the screen's ui_relationships entry and is compiled into metadata.composition.repeat by the LiveView's compile_node helper — the hydration pass never fires and 0 rows are rendered instead of N.

This is the substrate gap discovered during Wave AshUI-3.9 Item 2. ariston-ui PR #388 restructured Screens to use the canonical :list_repeat wrapper pattern (clearing 12 :missing_list_binding validator failures), but the 5 DSL Screen LiveViews in ariston-ui that use screen-scoped list bindings (manuscript, swimlane, research page, conversation, document chat workspace) still fail because their composition-root template nodes are not typed list_repeat.

A second factor: screen-scoped bindings (those with metadata.owner_scope: "screen") are filtered out of hydrate/2's element-scoped pass, so the list items never reach the template node even if we widened the type check.


Implementation chosen: Option A (smallest delta)

Modified files (only two, per brief constraint):

  • lib/ash_ui/liveview/iur_hydration.ex
  • test/ash_ui/liveview/iur_hydration_test.exs

Changes

  1. hydrate/2 now separates binding states into element_states (element-scoped, existing behaviour) and all_states (full non-action set including screen-scoped). Both are threaded into hydrate_node/3.

  2. hydrate_node/3 adds all_states as a third parameter (defaulting to [] for backward compat). After expand_list_repeat/1 runs, it calls the new expand_repeat_template/2.

  3. expand_repeat_template/2 — fires when a node has metadata.composition.repeat.binding_id AND props["hydrated?"] is NOT already true. Looks up the binding in all_states by id OR target. On match, clones the node once per row using the existing materialize_repeat_template/6. Returns a synthetic "list_repeat" wrapper with hydrated?: true so the LiveUIAdapter renders the children as a contiguous group without double-expansion.

  4. strip_repeat_metadata/1 — removes the "repeat" key from metadata.composition in each cloned row to prevent re-triggering expand_repeat_template/2 on that row.

  5. New unit test in iur_hydration_test.exs covering the exact two-level IUR shape: screen-scoped list binding (owner_scope: "screen") + repeat-marked custom:* template → synthetic wrapper + 3 cloned rows with row-scoped field resolution + repeat metadata stripped from rows.

What is NOT changed

  • packages/live_ui/lib/live_ui/renderer.ex — not touched
  • packages/live_ui/lib/live_ui/widgets/* — not touched
  • Any ariston-ui files — separate repo

Alternative options (for Pascal's consideration)

Option B — find_repeat_row_templates helper

Extract a find_repeat_row_templates/1 helper that walks the IUR tree until it finds the actual renderable templates, then call it in place of Map.get(node, "children", []) inside expand_list_repeat. Cleaner separation: the expansion logic stays in one place; only the template-finding step is generalised. No change to hydrate/2 or the binding-scope threading. Downside: the screen-scoped binding lookup problem still needs solving separately.

Option C — IUR-generation refactor (most invasive)

Refactor the IUR-generation step upstream in the LiveView compile_node helpers and/or the Authority payload compiler so the wrapper's ui_relationships kind: :child directly inlines the row template as the list_repeat's immediate child at IUR time, eliminating the relationship-only wrapper entirely. This makes hydration a pure implementation of the list_repeat contract with no special-casing, but requires coordinating changes across the ariston-ui LiveViews and potentially the Authority payload format. Most future-proof but the highest Pascal-coordination cost.


Test plan

  • mix format --check-formatted on both changed files — clean
  • mix compile — clean
  • Existing iur_hydration_test.exs tests still pass
  • list_repeat_hydration_test.exs — 7 passing (canonical list_repeat + Phase31 fixtures)
  • phase_31_integration_test.exs — 3 passing (resource-authority + compilation + hydration)
  • New positive test: expands repeat-marked non-list_repeat nodes using screen-scoped list binding — exercises the two-level IUR shape directly

Cross-references

ty13r and others added 2 commits May 17, 2026 23:14
…onship for two-level row templates (#114)

Extends AshUI.LiveView.IURHydration to fan-out repeat-marked elements
whose widget type is NOT "list_repeat" (e.g. "custom:doc_block_numbered",
"artifact_row") but which carry repeat metadata in
metadata.composition.repeat.binding_id via the LiveView compile_node helper.

Previously only list_repeat-typed nodes were expanded by expand_list_repeat/1.
For screens like ProcessManuscriptScreen whose composition roots are typed as
custom:* or other widget kinds (with the repeat directive on the
ui_relationships entry, compiled as metadata.composition.repeat), the hydration
pass never cloned the template per row.

The fix adds two private helpers:

- expand_repeat_template/2: called after expand_list_repeat/1 on every hydrated
  node; detects metadata.composition.repeat.binding_id, looks up the matching
  binding from the full (unfiltered) binding state list including screen-scoped
  bindings excluded from element-level hydration, and materialises one cloned
  row per item. Returns a synthetic list_repeat wrapper so the LiveUIAdapter
  renders the rows as a contiguous group. The hydrated? flag prevents
  double-expansion.

- strip_repeat_metadata/1: removes repeat from the composition metadata of each
  cloned template row so they do not re-trigger expand_repeat_template/2 on
  subsequent hydrate passes.

hydrate/2 now preserves all non-action binding states and threads them through
hydrate_node/3 as all_states for use by expand_repeat_template/2.

Adds a positive unit test in iur_hydration_test.exs exercising the two-level
IUR shape: screen-scoped list binding + repeat-marked custom:* template ->
synthetic wrapper + 3 cloned rows with row-scoped field resolution + repeat
metadata stripped.

Addresses #114. References ariston-ui PR #388 as the concrete consumer.

DRAFT: Pascal-review REQUIRED. Happy to revise to Option B or Option C if preferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pcharbon70
pcharbon70 marked this pull request as ready for review May 18, 2026 07:58
@pcharbon70
pcharbon70 merged commit 2b97722 into main May 18, 2026
8 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.

2 participants