Search code examples
javascriptsveltesveltekitsvelte-componentsvelte-5

Other than children(), can I import and render multiple snippets in +layout.svelte that are defined in +page.svelte?


New to SvelteKit and trying to figure this part out...

Here's a couple of ways I was trying to achieve this, but the sidebar either doesn't load, or the navigation appears twice for some reason; with the snippet text only appearing in the second instance. I started searching Github to review examples via path:**/+layout.svelte /@render children/ and path:**/+layout.svelte /children: Snippet; *: Snippet, but there are no results that actually import and use duplicate renders; from what I've seen. I planned to use +layout.svelte to breakdown my site into Header, Sidebar, Main and Footer and dynamically change the content based on +page.svelte. The docs touch on this and I've seen it done with cards and +page.svelte, but not in +layout.svelte itself. Am I doing this wrong?

+layout.svelte

<script lang="ts">
    import "../app.css";
    import type { Snippet } from "svelte";
    import { sineIn } from "svelte/easing";
    import { Drawer, Sidebar } from "flowbite-svelte";

    // Destructure specific props from $props()
    interface Props {
        children?: Snippet;
        filterSidebar?: Snippet<[() => void]>;
    }
    let { 
        children, 
        filterSidebar
    }: Props = $props();

    const transitionParams = {
        x: -320,
        duration: 200,
        easing: sineIn
    };

    let hidden = $state(true);

    function toggleSidebar(): void {
        hidden = !hidden;
    }
</script>
<div class="header flex items-center justify-between p-5 border-b">
    <h1 class="text-xl">SvelteKit Render Test</h1>
    <button
        aria-label="Toggle Filters"
        onclick={toggleSidebar}
    >
        <svg
            class="w-6 h-6"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
        >
            <path
                stroke-linecap="round"
                stroke-linejoin="round"
                stroke-width="2"
                d="M4 6h16M4 12h16m-7 6h7"
            />
        </svg>
    </button>
</div>
<Drawer transitionType="fly" {transitionParams} bind:hidden={hidden} id="sidebar">
    <Sidebar asideClass="w-full">
        {@render filterSidebar?.(toggleSidebar)}
    </Sidebar>
</Drawer>
<main>
    {@render children?.()}
</main>


+page.svelte v1

<script lang="ts">
    import Layout from "./+layout.svelte";
    import { CloseButton, SidebarGroup } from "flowbite-svelte";=
</script>
{#snippet filterSidebar(toggleSidebar: () => void)}
    <SidebarGroup ulClass="flex items-center">
        <h1>Filters</h1>
        <CloseButton onclick={toggleSidebar} />
    </SidebarGroup>
{/snippet}
<h1>Test Element</h1>

Results +page.svelte v1

+page.svelte v1 - blank sidebar


+page.svelte v2

<script lang="ts">
    import Layout from "./+layout.svelte";
    import { CloseButton, SidebarGroup } from "flowbite-svelte";
    console.log("Page rendered");
</script>
<Layout>
    {#snippet filterSidebar(toggleSidebar: () => void)}
        <SidebarGroup ulClass="flex items-center">
            <h1>Filters</h1>
            <CloseButton onclick={toggleSidebar} />
        </SidebarGroup>
    {/snippet}
    <h1>Test Element</h1>
</Layout>

Results +page.svelte v2

+page.svelte v2 - duplicated content


Solution

  • I will reiterate my answer over here, though it is more a workaround than a proper solution: You can use a context to communicate between page and layout and use {@render children()} only to retrieve snippets from the page for rendering in the layout.

    // $lib/layout-slots.svelte.js
    import { getContext, setContext } from 'svelte';
    
    const key = Symbol('layout-slots');
    
    export function initSlots() {
        const slots = $state({});
        return setContext(key, slots);
    }
    
    export function setSlots(slots) {
        const context = getContext(key);
        Object.assign(context, slots);
    }
    
    <!-- +layout.svelte -->
    <script>
        import { initSlots } from '$lib/layout-slots.svelte.js';
    
        let { children } = $props();
    
        const slots = initSlots();
    </script>
    
    <!--
        run page logic first to get slot snippets,
        the page should have no output.
    -->
    {@render children()}
    
    <h1>{@render slots.heading()}</h1>
    
    {@render slots.children()}
    
    <!-- +page.svelte -->
    <script>
        import { setSlots } from '$lib/layout-slots.svelte.js';
    
        setSlots({ heading, children });
    </script>
    
    {#snippet heading()}
        Hello there
    {/snippet}
    
    {#snippet children()}
        Main slot content
    {/snippet}
    

    SvelteLab

    (And no, this was not possible before with slots, it is just how SvelteKit in particular works. If you nest specific, regular components, you can use slots or snippets to compose them however you want, but this is not the case for layouts and pages which have a very generic relationship that only provides the single default slot/snippet. See this issue.)