svelte
svelte copied to clipboard
Svelte 5: List of children
Describe the problem
In Svelte 5 we can pass a children snippet to a component, as the default "slot". The way this makes writing a component feels intuitive, as what you write inside the component is what gets rendered when calling render children()
in the template.
But what is a little less intuitive here, is that the children is actually one block of template, ie. one Snippet, which might be several elements, or one element, not really a list of children.
I want to create a List component with a list of children, where each child gets wrapped with content defined in the List component. This is equivalent to the FancyList examples, but I have always found those examples to be incredibly unintuitive, coming from Flutter which has really modular component capabilities.
Today we can do the following in Svelte 5:
List component:
<script lang="ts">
import type { Snippet } from 'svelte'
let {
children = [],
activeIndex = $bindable(0)
}: {
children: Snippet[]
activeIndex?: number
} = $props()
</script>
<!-- The list wrapping -->
<div>
{#each children as child, index}
<!-- Each child wrapping -->
<button onclick={() => (activeIndex = index)}>
{@render child()}
</button>
{/each}
</div>
Using it:
{#snippet optA()}
<span>A</span>
{/snippet}
{#snippet optB()}
<span>B</span>
{/snippet}
{#snippet optC()}
<span>C</span>
{/snippet}
<MyList children={[optA, optB, optC]} bind:activeIndex />
I love that this is possible with the new Snippet approach, but I'm not sure I would immediately remember how to do it the second time. It doesn't "roll off the tongue", like other Svelte syntax may do.
Describe the proposed solution
At the very least, I would like to be able to do this:
<MyList children={[optA, optB, optC]} bind:activeIndex>
{#snippet optA()}
<span>A</span>
{/snippet}
{#snippet optB()}
<span>B</span>
{/snippet}
{#snippet optC()}
<span>C</span>
{/snippet}
</MyList>
But then we get errors as the children are not assigned before they are used.
But in a perfect world I would like to just do this:
<MyList bind:activeIndex>
<span>A</span>
<span>B</span>
<span>C</span>
</MyList>
With some way of telling Svelte that the default children Snippet, is in fact a list of children.
Importance
would make my life easier
Hello,
For the first solution, you can use the restProps to detect snippets :
<MyList bind:activeIndex>
{#snippet optA()}
<span>A</span>
{/snippet}
{#snippet optB()}
<span>B</span>
{/snippet}
{#snippet optC()}
<span>C</span>
{/snippet}
</MyList>
Example here : REPL
For the second solution it may be difficult with Svelte. It's "doable" by manipulating the DOM, but it will break the reactivity of the component...
Something similar can be possible using sub-components :
<MyList bind:activeIndex>
<MyItem>A</MyItem>
<MyItem>B</MyItem>
<MyItem>C</MyItem>
</MyList>
Example : REPL
But there is no way to check if it's used correctly, and order may not be respected some {#if}
or {#each}
are used...
Thank you! I like the first approach, and will probably use it in the meantime.
But I still think it feels too much of a workaround, and would rather wish for lists of dynamic content to be first class citizens of svelte. They almost are, it's just that the children snippet isn't interpreted as a list of children, and doing so requires special handling.
Leaving aside the feature request itself (not really sure how such a thing could work, need to think on it), can you provide a repro for this bit?
we get errors as the children are not assigned before they are used
Leaving aside the feature request itself (not really sure how such a thing could work, need to think on it),
Just a few simple ideas to create a link between the components.
=> On the child component, we should be able to access the parent component, using a rune or a specific function.
<script>
// return the direct parent component (as if we had used bind:this),
// (maybe null if this component is inside a HTML node ???)
const parent = $parent();
</script>
=> On the parent component, we should be able to access all its child components (direct only?). Perhaps via an improvement of @render, which would allow us to bind the child components into an array.
<script>
let {
children
} = $props();
// a simple array, which will be populated by the child components
const childs = $state([]);
</script>
<div>
<!-- render children, binding component on childs -->
{@render children() bind childs}
</div>
We could perhaps use a rune or a function for more advanced things :
const childs = $childs(); // bind all childs
const childs = $childs(true); // bind all childs
const childs = $childs(false); // bind only direct childs (root of children, without any HTML node)
const childs = $childs(MyItem); // bind only all childs of type MyItem
const childs = $childs(MyItem, MyComp); // bind only all childs of type MyItem or MyComp
Leaving aside the feature request itself (not really sure how such a thing could work, need to think on it), can you provide a repro for this bit?
we get errors as the children are not assigned before they are used
Possibly just a language tools thing? It shows errors like:
Cannot find name 'optA'. ts(2304)
Sorry! @Rich-Harris That does seem to work without errors during runtime!
The errors occur in development, like @brunnerh mentioned, one where passing the snippets to the children argument:
And one where declaring the snippets:
I'd argue the error is correct. Looking at the compiled output, optA and optB are passed as properties to MyList
, because that's how the snippet behavior is specified. In this case this isn't what's the desired behavior though, and so I argue while it works at runtime in this case it shouldn't be relied upon and the language tools error is sound.