Skip to content

Commit 6639d39

Browse files
docs(claude): note 30 described a limitation that no longer exists
`:prop="expr"` in a @foreach does receive the loop variable. The note said it did not and told readers to inline the markup instead of using a component -- the worst possible advice to leave in a file agents read first, and one that would have them undo working code. Kept the number so every later cross-reference still points at what it meant, and said what the note used to claim so a reader who remembers it knows it is gone. The object-prop transport it also describes is the real subtlety, so that stays. Two tests cover the scalar path the note was about (a string prop is evaluated in the loop context, not serialised like an object prop, so the existing tests did not reach it), including that the loop variable itself stays out of the component's scope.
1 parent a863fcd commit 6639d39

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ stx doctor [--json]
468468

469469
29. **Document Shell Comment Stripping**: `hasDocumentShell` strips HTML comments (like `<!-- stx-layout: ... -->`) before checking if the output starts with `<!DOCTYPE>` or `<html>`. Without this, layout comments caused double `<body>` wrapping — the document shell wrapped the already-complete layout output because it didn't detect the existing document structure.
470470

471-
30. **Known Limitation: Server-Side Component Props in Loops**: Component props (`:prop="expr"`) inside `@foreach` loops do not receive loop variables in their evaluation context. The component renderer creates an isolated context where loop variables like `feature` are not available. Workaround: inline the HTML directly in the `@foreach` loop instead of using a component. This affects server-side rendering only — client-side `:for` with components works correctly.
471+
30. **Component Props in Loops (fixed)**: `:prop="expr"` inside `@foreach` DOES receive the loop variable — `<Feature :title="feature.title" />` renders per row. This note previously said otherwise and told you to inline the HTML instead; that workaround is obsolete, and inlining to avoid a component is never the right call. Object props ride through the loop as base64 in a `__stx_` attribute (`loops.ts` / `component-processing.ts`) rather than as entity-escaped JSON, because `\"` was stripped by the attribute parser and any value containing a double quote arrived empty. Pinned by `test/directives/foreach-component-props.test.ts`.
472472

473473
31. **`<template>` Tag Stripping**: `bun-plugin/src/serve.ts` strips `<template>` wrapper tags from output (browsers don't render template content). Tags with reactive directives (`x-for`, `x-if`, `@for`, `@if`, `:for`, `:if`) are preserved for the client-side runtime. Prefer putting `x-for`/`x-if` directly on elements (`<div x-for="item in items">`) instead of using `<template>` wrappers to avoid stripping issues.
474474

‎packages/stx/test/directives/foreach-component-props.test.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ const { item } = defineProps()
3232
<template><div class="row">[name={{ item && item.name }}|n={{ item && item.n }}|first={{ (item && item.lines && item.lines[0] && item.lines[0].text) || 'NONE' }}]</div></template>
3333
`,
3434
)
35+
await writeFile(
36+
join(componentsDir, 'feature.stx'),
37+
'<div class="feature">[{{ title }}|{{ icon }}|{{ typeof feature }}]</div>\n',
38+
)
3539
})
3640

3741
const render = async (template: string): Promise<string> => {
@@ -86,4 +90,44 @@ const items = [
8690
expect(result).toContain('name=b|n=|first=plain')
8791
expect(result).not.toContain('first=NONE')
8892
})
93+
94+
/**
95+
* CLAUDE.md note 30 used to call this a known limitation and told readers to
96+
* inline the markup instead of using a component. It works; the note was
97+
* stale. A scalar prop takes a different path from the object props above --
98+
* it is evaluated in the loop's own context rather than serialised -- so it
99+
* needs its own test to stay fixed.
100+
*/
101+
it('evaluates a scalar prop against the loop variable', async () => {
102+
const template = `<script server>
103+
const features = [
104+
{ title: 'Fast', icon: 'bolt' },
105+
{ title: 'Small', icon: 'leaf' },
106+
]
107+
</script>
108+
@foreach(features as feature)
109+
<Feature :title="feature.title" :icon="feature.icon" />
110+
@endforeach`
111+
112+
const result = await render(template)
113+
114+
expect(result).toContain('[Fast|bolt|')
115+
expect(result).toContain('[Small|leaf|')
116+
// The loop variable itself is not leaked into the component's scope.
117+
expect(result).toContain('|undefined]')
118+
})
119+
120+
it('reads the loop index and the loop meta in a prop', async () => {
121+
const template = `<script server>
122+
const features = [{ title: 'Fast' }, { title: 'Small' }]
123+
</script>
124+
@foreach(features as feature, i)
125+
<Feature :title="feature.title" :icon="i + ':' + loop.iteration" />
126+
@endforeach`
127+
128+
const result = await render(template)
129+
130+
expect(result).toContain('[Fast|0:1|')
131+
expect(result).toContain('[Small|1:2|')
132+
})
89133
})

0 commit comments

Comments
 (0)