Skip to content

fix(render): resolve repeated element readiness and props in the row's own scope - #1062

Merged
blove merged 1 commit into
mainfrom
blove/render-repeat-readiness
Sep 8, 2026
Merged

fix(render): resolve repeated element readiness and props in the row's own scope#1062
blove merged 1 commit into
mainfrom
blove/render-repeat-readiness

Conversation

@blove

@blove blove commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

What was broken

RenderElementComponent rendered each repeat item through a per-item prop context, but computed the mount-readiness gate (notReady) and the mount class (mountClass()) from the NON-repeat prop context. resolveElementProps resolves { $item: … } / { $index: … } / { $bindItem: … } to undefined when the context carries no repeat scope, and isElementReady treats any undefined-valued prop as "still streaming".

So a repeating element whose props are all $item-bound tripped the gate and mounted its fallback (a bare registration means DefaultFallbackComponent) — or nothing at all — for every row, permanently. The diagnosis was confirmed against the code and then reproduced: the new spec file failed 8/8 before the fix, with expected [] to deeply equal ['Alpha', 'Beta', 'Gamma'] and a $bindItem used outside repeat scope: "label" warning from @json-render/core proving readiness ran with no scope.

The streaming demo hid the defect: partial JSON yields props: {} before the $item keys arrive, which reads as ready and latches mountedReal for the whole element. The specs added in #1056 sidestepped it by reading the item from REPEAT_SCOPE instead of binding $item props.

Two adjacent instances of the same root cause were fixed with it:

  • ActionBinding.params resolved through this.propCtx(), so { $item: … } params on a repeated element reached the handler as undefined.
  • RENDER_HOST was provided once per element, so a component emitting through injectRenderHost() inside a repeat could not say which row fired.

What changed

libs/render/src/lib/render-element.component.ts

  • One repeatPropCtxs computed is now the single source for every per-row derivation: props, bindings, readiness, mount class, input filtering, visibility and action params.
  • repeatRawNotReady / repeatNotReady / repeatMountClasses are the per-row counterparts of notReady / mountClass. A row shows its fallback while its own props are undefined or fail the entry's Standard Schema, while its ready siblings mount the real component. Fallback behavior for genuinely-undefined props is unchanged.
  • The one-way fallback latch is kept per row index — the identity the @for block already tracks — in a second constructor effect, mirroring the existing single-mount latch.
  • invokeHandlers(event, payload, repeatIndex?) resolves params in the emitting row's context. repeatInputs hands each row an emit bound to its index, and repeatInjectors provides a row-scoped RENDER_HOST so injectRenderHost().emit(…) behaves identically.
  • The mount-evidence telemetry guard dropped its visible()/notReady() precondition and relies on the outlet identity check alone: an element-level readiness flag cannot describe a list whose rows differ. Unknown, hidden, unready and empty-repeat elements are still not reported (covered by the existing development-mount.spec.ts table).

libs/render/src/lib/repeat-scope.spec.ts (new, 8 specs, all failing before the fix)

cockpit/render/repeat-loops — the fix makes the more direct demo possible, so the store now holds [{ label: 'Alpha' }, …] and the repeated element binds { $item: 'label' } instead of { $item: '' } over plain strings. The e2e still asserts the rendered rows and passes unchanged.

Docs: render/guides/repeat-loops.mdx gains a "Per-item readiness" section (per-row gating, per-row schema, per-row action params) and its callout now describes the object array; render/guides/registry.mdx says the fallback switch is one-way per row for a repeating element and that the host is row-scoped inside a repeat; RenderHost's JSDoc says the same, with api-docs.json regenerated.

Verification

  • npx nx run-many -t lint,test,build --projects=render,chat --skip-nx-cache — green (0 lint errors)
  • npx vitest run --root apps/website --reporter=dot — 137 files, 1382 tests passed
  • npx nx run-many -t build for all six cockpit-render-*-angular projects — green
  • npx nx e2e cockpit-render-repeat-loops-angular — 3 passed; the other five render e2e suites — 2 passed each
  • GROWTH_FORM_POLICY=growth_v1 npx nx build website — green

Not run: nx run render:type-tests — the render project has no type-tests target (only langgraph, chat and ag-ui define one). chat:type-tests fails on this branch and on main alike, with a pre-existing TS5101: Option 'baseUrl' is deprecated error in libs/chat/tsconfig.type-tests.json, untouched here.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
threadplane Ready Ready Preview Sep 8, 2026 4:11pm UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

…s own scope

A repeating element resolved its mount-readiness gate and its mount class
from the NON-repeat prop context: `notReady` and `mountClass` ran
`resolveElementProps` against the parent scope, where every `$item`,
`$index` and `$bindItem` expression resolves to `undefined`. An element
whose props were all `$item`-bound therefore tripped the readiness gate
and rendered its fallback (or nothing) for every row, forever. The
streaming demo hid this because a partially parsed `props: {}` latches
`mountedReal` before the `$item` keys arrive.

Readiness, mount class, input filtering, visibility and action `params`
are now all derived from one per-row prop-resolution context, and the
fallback latch is kept per row index — the identity the `@for` block
already tracks. A row still shows its fallback while its own props are
genuinely undefined or fail the entry's Standard Schema, while its ready
siblings mount the real component.

Each row also gets its own `RENDER_HOST`, so a component that emits
through `injectRenderHost()` inside a repeat resolves `{ $item: … }`
action params against the row that fired the event, matching the `emit`
input path.

The mount-evidence telemetry guard now relies on the outlet identity
check alone: an element-level `visible`/`notReady` pair cannot describe a
list whose rows differ.

The repeat-loops example holds objects and binds `{ $item: 'label' }`,
which the fix makes renderable; docs updated for per-row readiness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@blove
blove force-pushed the blove/render-repeat-readiness branch from 34a1215 to 6034bb1 Compare September 8, 2026 16:08
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove merged commit 758f624 into main Sep 8, 2026
77 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