Pass in a slot to a recursive component

Viewed 42

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

2 Answers

One approach would be to make the hierarchical component handle only a single item, that way the slot can be used to template every item independently. There will be one more #each at the root level unless you add additional logic to work around that, though.

Example:

<!-- Tree.svelte -->
<script>
    export let item;
    export let level = 0;
</script>

<div style:margin-left="{level * 20}px">
    <slot {item} />

    {#each item?.children ?? [] as child}
        <svelte:self item={child} level={level + 1}>
            <slot item={child} /> <!-- Slot templates child -->
        </svelte:self>
    {/each}
</div>

Usage:

<script>
    import Tree from './Tree.svelte';

    const items = [
        {
            type: 'person', first: 'Maru', last: 'Doe',
            children: [
                { type: 'person', first: 'Pochi', last: 'Doe' },
                { type: 'person', first: 'Tama', last: 'Doe' },
                { type: 'thing', manufacturer: 'ACME', product: 'missiles' },
            ]
        }
    ]
</script>

{#each items as item}
    <Tree {item} let:item>
        <!-- Use a property to discriminate between items -->
        {#if item.type == 'person'}
            {item.last}, {item.first}
        {:else if item.type == 'thing'}
            {item.product} ({item.manufacturer})
        {/if}
    </Tree>
{/each}

REPL

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.)

Related