Search code examples
sveltesveltekitsvelte-3

Pass in a slot to a recursive component


In my SvelteKit app I have a component that renders a nested list of items, like so:

enter image description here

Until now I made this component very much tied to the rows it is rendering, but now that I have a second content type on my website that I also want to render nested like this, I want to make it generic. I am basically 90% done with that refactor, except the part where the <slot /> works recursively as well.

To give you an idea, here is the new version of the generic code (I've removed a bunch of logic to keep the code simple):

// lib/components/NestedList.svelte
<script lang="ts">
  type T = $$Generic<{
    id: number;
    children?: T[];
  }>;

  export let items: T[];
  export let level = 1;
  export let activeID: number;
</script>

<ul class="level-{level}">
  {#each items as item (item.id)}
    <li>
      <div>
        <div class="toggler">
          {#if item.children?.length}
            <button class="as-link" class:expanded={expanded(item, collapsedItems, activeID)} type="button" on:click={() => toggle(item)} />
          {/if}
        </div>

        <slot {item} />
      </div>

      {#if item.children?.length && expanded(item, collapsedItems, activeID)}
        <svelte:self items={item.children} level={level + 1} {activeID} />
      {/if}
    </li>
  {/each}
</ul>

Basically everything works as expected, except that on deeper levels the rows are missing since the recursive <svelte:self> component is missing the slot with the item.

enter image description here

The parent page renders this component like so:

<div id="list">
  <NestedList items={filteredLocations} activeID={+$page.params.locationId} let:item>
    <LocationRow location={item} />
  </NestedList>
</div>

So, how can I pass in the slot into the recursive self component?

This doesn't work correctly by the way, I tried:

<svelte:self items={item.children} level={level + 1} {activeID}>
  <slot {item} />
</svelte:self>

It just end up re-rendering the parent item row for all children:

enter image description here


Solution

  • I just noticed that let works on svelte:self as well, so you should only have to use that to get the current child:

    <svelte:self items={item.children} level={level + 1} {activeID}
                 let:item={child}>
      <slot item={child} />
    </svelte:self>
    

    REPL example

    (You do not have to alias the item to child, but that would shadow the item of the #each.)