What happens
A scoped slot prop only reaches the slot content when three names match: the child's loop variable, the slot prop and the name the parent destructures. Otherwise the parent reads undefined.
- The child binds the prop to a loop variable with another name (
<li :for="item in items()"> <slot name="actions" :row="item" />). The parent's#actions="{ row }"getsrow === undefined. - The child binds
:row="row"and the parent renames it in the destructure (#actions="{ row: entry }").entryisundefined.
It only works when the child's loop variable is itself called row, the prop is :row="row" and the parent reads row. So the slot content seems to resolve row against the child's loop scope by name, and never through the slot props.
Found while moving dashboard tables onto a shared Table component in stacksjs/stacks#2925. Table happens to name its loop variable row, which is the only reason its #cell="{ row }" slot works.
Repro (stx 0.2.661, stx dev slot-scope.stx)
components/BoxItem.stx:
<script client>
const items = useReactiveProp('items', [])
</script>
<ul>
<li :for="item in items()">
<span>{{ item.name }}</span>
<slot name="actions" :row="item" />
</li>
</ul>components/BoxRow.stx is the same file with the loop variable renamed: :for="row in items()", and :row="row".
slot-scope.stx:
<script client>
const items = state([{ id: 1, name: 'first' }, { id: 2, name: 'second' }])
</script>
<h2>1: child passes :row="item", parent reads row</h2>
<BoxItem :items="items()">
<template #actions="{ row }">
<code>typeof row=[{{ typeof row }}]</code>
</template>
</BoxItem>
<h2>2: same child, parent reads the child's loop variable item, which it never declared</h2>
<BoxItem :items="items()">
<template #actions="{ row }">
<code>item.name=[{{ typeof item === 'undefined' ? 'undefined' : item.name }}]</code>
</template>
</BoxItem>
<h2>3 (control): child passes :row="row", parent reads row</h2>
<BoxRow :items="items()">
<template #actions="{ row }">
<code>row.name=[{{ row ? row.name : 'undefined' }}]</code>
</template>
</BoxRow>
<h2>4: child passes :row="row", parent destructures it under another name</h2>
<BoxRow :items="items()">
<template #actions="{ row: entry }">
<code>entry.name=[{{ typeof entry === 'undefined' ? 'undefined' : entry.name }}]</code>
</template>
</BoxRow>Rendered text:
1: child passes :row="item", parent reads row
first typeof row=[undefined]
second typeof row=[undefined]
2: same child, parent reads the child's loop variable item, which it never declared
first item.name=[undefined]
second item.name=[undefined]
3 (control): child passes :row="row", parent reads row
first row.name=[first]
second row.name=[second]
4: child passes :row="row", parent destructures it under another name
first entry.name=[undefined]
second entry.name=[undefined]Expected
Cases 1 and 4 should print each row's name, as case 3 does. A slot prop's value is the evaluated expression (item), exposed under the prop's name (row), and the parent's destructure decides what it is called locally.
Case 2 shows this is not a simple leak of the child's scope into the parent. The child's own variable item is not visible either. Only row resolves, and only when the child happens to have a row in scope.