A <template :for> placed directly inside a <template :if> keeps its rendered rows on screen after the :if becomes false. The branch is hidden, but its list is still visible.
Found while verifying the #1954 fix. It is not caused by that fix: unmodified main at db32fc6618 behaves the same way, and so does 092ed2d12d.
Reproduction
<div id="host">
<template :if="shown()">
<template :for="i in items" :key="i">
<b class="row">{{ i }}</b>
</template>
</template>
</div>With items = [1, 2, 3], set shown to false.
- Expected: no
.rowelements in#host. - Observed: all 3
.rowelements remain.
Measured under happy-dom with the full client runtime, the same harness as packages/stx/test/signals/dispose-keeps-live-bindings.test.ts. Not yet checked in a real browser.
What does work
The same list wrapped in an element hides correctly:
<template :if="shown()">
<ul><template :for="i in items" :key="i"><li class="row">{{ i }}</li></template></ul>
</template>dispose-keeps-live-bindings.test.ts pins that variant across five hide/show cycles.
Likely cause (from reading the code, not confirmed)
Hiding a <template :if> removes currentNodes, the clones of el.content, in bindIf's template branch in packages/stx/src/signals.ts. A :for that is a direct child of the template renders its rows as siblings of its own clone, not inside it. The rows are not among currentNodes, so hide never removes them. Wrapping the list works because the wrapper is a clone and the rows are its descendants.
A fix probably needs the template :if to own whatever its direct-child structural directives insert: for example, removing the nodes between its placeholder and the end of its inserted range, rather than only the clones it created.
Test to add
This case was written for #1954 and taken out because it fails on main. It slots into dispose-keeps-live-bindings.test.ts, which provides mount, hits and settle:
it('removes rows of a <template :for> placed DIRECTLY under a hidden <template :if>', async () => {
const shown = window.stx.state(true)
const items = window.stx.state([1, 2, 3])
const page = await mount(`
<div id="host"><template :if="shown()"><template :for="i in items" :key="i"><b class="row" x-class="probe(i)">{{ i }}</b></template></template></div>`, { shown, items })
expect(hits('#host .row')).toBe(3)
for (let cycle = 0; cycle < 5; cycle++) {
shown.set(false)
await settle()
expect(document.querySelectorAll('#host .row').length).toBe(0)
expect(await page.oneUpdate()).toBe(0)
shown.set(true)
await settle()
expect(hits('#host .row')).toBe(3)
expect(await page.oneUpdate()).toBe(3)
}
})On main it fails at the first hide: expected 0 rows, received 3.
🤖 Generated with Claude Code