-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Treat layout pages like components when using named slots #627
Comments
This comment was marked as duplicate.
This comment was marked as duplicate.
I guess it doesn't work because the Svelte compiler needs to know at compile-time which parent-component the slots will end up in. |
This comment was marked as off-topic.
This comment was marked as off-topic.
Well, there's nothing keeping us from writing our pages like <Layout>
<h1 slot="above">...</h1>
<main>derp derp lalalala</main>
</Layout> |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
You have to do it in a regular way, by creating a custom layout: (_myLayout.svelte) |
Can someone show how to include a _customLayout.svelte as zakaria-chahboun suggests? |
Example:We have a route called
The _customLayout.svelte is like this: <slot name="sidebar"></slot>
<slot name="profile"></slot> And the [id].svelte is like this: <script context="module">
export async function preload(page) {
const { id } = page.params;
return {id};
}
</script>
<script>
import customLayout from "./_customLayout.svelte";
</script>
<customLayout>
<div slot="sidebar">test</div>
</customLayout> |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
In the pseudocode snippet below I demonstrate how I get around this problem in my project. <!-- ⚠ untested pseudo-ish code ahead ⚠ -->
<!-- __layout.svelte -->
<script context="module">
export async function load({ page })
{
const slots = {}
if( page.path === '/foo' )
{
slots.navigator = ( await import( `$lib/page-slot-components/foo/navigator.svelte`) ).default
slots.sidemenu = ( await import( `$lib/page-slot-components/foo/sidemenu.svelte`) ).default
}
else
{
slots.navigator = ( await import( `$lib/page-slot-components/bar/navigator.svelte`) ).default
slots.sidemenu = ( await import( `$lib/page-slot-components/bar/sidemenu.svelte`) ).default
}
return {
status: 200,
props: {
slots
}
}
}
</script>
<script>
export let slots
</script>
{ #if slots.navigator }
<svelte:component this={slots.navigator}></svelte:component>
{/if}
{ #if slots.sidemenu }
<svelte:component this={slots.navigator}></svelte:component>
{/if} This approach is fully SSR compatible and does not produce flashes of content. There are limitations though of course. Note: svelte-kit currently isn't able to properly parse template literals that also use import aliases like |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
Marked the majority of comments as 'off-topic' because most of them boil down to '+1'. Please resist the urge to add comments along the lines of 'I also want this' or 'why isn't this done yet' or 'here's my use case, which is basically identical to all the other use cases that have already been mentioned'! You're not just making the thread harder to follow without adding useful information, you're actually harming your cause — a simple 👍 reaction on the OP of a GitHub issue thread carries more weight than a redundant comment, because those reactions can be used as a sorting mechanism. (Granted, this is already the top-reacted thread.) With that out of the way: Why this isn't supported yetAllowing named slots in layouts would require that we had a mechanism for 'passing' slots to components. Effectively we need to be able to represent this concept... <aside><slot name="sidebar" /></aside>
<main><slot /></main> <Layout data={...}>
<div slot="sidebar">
<nav>...</nav>
</div>
<p>the content</p>
</Layout> ...without knowing ahead of time what goes inside <Layout data={...}>
<Page $$slots={???} />
</Layout> Right now, there's just no mechanism to do that in Svelte itself. It's something we'd like to add, but the bulk of our energy over the last year or so has been directed towards SvelteKit, and we're just now working through the backlog of Svelte PRs so that we're in a position to start thinking about major new features like this. It'll happen, but it won't happen overnight. Why you probably don't need it anyway
// src/routes/some/deeply/nested/+page.js
export function load() {
return {
message: 'hello from all the way down here'
};
} <!-- src/routes/+layout.svelte -->
<script>
import { page } from '$app/stores';
</script>
<p>{$page.data.message ?? '...'}</p> You can define If you're using universal import links from './Links.svelte';
export function load() {
return { links }
} <nav>
<!-- standard links -->
<a href="...">...</a>
<!-- page-specific links -->
<svelte:component this={$page.data.links} />
</nav> It's not quite as idiomatic as |
our solution to this problem is relatively simple, <script>
export let data;
</script>
<Layout {data}>
<div slot="x">
<!-- Content for slot x -->
</div>
<div>
<!-- main slot goes here -->
</div>
</Layout> and in the <slot name="x" />
<div>
<slot/>
</div> hope this helps someone ! |
@nazihahmed could you explain how it worked in your project? because unfortunately it doesn't work for me |
@nazihahmed Unfortunately, this will still inherit the root layout, so by default you'd get the layout inside itself.
<slot/>
<nav>
Always-on nav stuff here
<slot name="nav" />
</nav>
<div>
<slot/>
</div>
<script>
import Layout from "$routes/(haslayout)/+layout.svelte";
export let data;
</script>
<Layout {data}>
<span slot="nav">
Hey! More stuff in the nav!
</span>
<div>
Rest of the page goes here.
</div>
</Layout> I find it somewhat annoying having to use a group like that, but it seems to be the easiest workaround for now. |
Just bike shedding here, but |
How do you guys deal with the data or code that would normally get loaded in the I guess it would still be nice if there were a proper feature for this, for now I've gone with appending elements to the desired location via |
FWIW, I had the solution in #627 (comment) for some time in my code until I introduced groups, e.g. The link component works when your access group1 in your browser but once you navigate to group2, the link from both groups can be seen at the same time/kind of overlap. Tried few work-arounds before I wrote this but couldn't find a good solution yet. |
Edit: |
@Rich-Harris Thanks for your sample and detailed explanation. I'm using a CSR app only and so I'm not so sure this approach works (page data is always null, but still debugging). Regardless coming from multiple experiences with different frameworks this doesn't feel idiomatic at all. I have a super simple app with a sidebar (drawer) and having a named sidebar slot in this case seems very intuitive. I am finding myself really digging through samples finding solutions. It would be great if features like this one could be showcased in a common but simple starter app. |
I do it the same way as before, still in the top |
This will be solved through snippets in Svelte 5, if SSR is not a concern for you: https://www.sveltelab.dev/hd0z3oisqttk8tx In SSR this likely won't work because the child sets the context after the parent has rendered. |
The "snippet" and the counterpart "render" seem extremely promising as highlighted in the Svelte Summit talk. As we all know naming things is the hardest part in software engineering, so my initial reaction to the name "snippet" is that the connotation of a small code part (a snippet) may not (or may, depending on the intentions) imply the use of this framework feature for certain scenarios. If it would be intended for small code parts the name would be perfectly fine. On the other hand if this is the "new and better slot" feature, it would be used (definitely has the potential to) for larger pieces of code that allows us to structure code e.g. within +layouts, which this issue was originally all about. In this context the name snippet might be a bit misleading especially for new users. My first thoughts in this context would be names like "part", "piece" or "extract" which would nicely coexist with the render counterpart and make less assumptions about the size of code the feature is intended to handle. But that's just naming — the idea seems really great and certainly tops my wish-list! |
You know... I think we could solve this in SSR. Thinking aloud (apologies if this idea has already been presented, but this thread is too long to re-read in its entirety right now!) — perhaps if we had something like this...
...and the layout looked like this... <script>
let { data, children, header, footer } = $props();
</script>
<header>
{#if header}
{@render header()}
{:else}
<!-- default header content -->
{/if}
</header>
<main>{@render children()}</main>
<footer>
{#if footer}
{@render footer()}
{:else}
<!-- default footer content -->
{/if}
</footer> ...we could make it work? |
Find a solution in https://stackoverflow.com/a/71672937/4037224 |
That's a very nice solution. But that makes me think, why wouldn't the same thing work in Svelte 4/earlier as well? You can even pass props using that same context! Although I think this still suffers from SSR issues. |
Will snippets in Svelte 5 solve this issue? |
I think they should, but I'm not sure how. Without anything special needed, a layout can do something like this: // +layout.svelte
<script>
const slots = $state({
header: null,
footer: null,
})
setContext("layoutSlots", slots)
</script>
<div class="header">
{@render slots.header}
</div>
<main>
<slot/>
<main>
<div class="footer">
{@render slots.footer}
</div> And: // +page.svelte
<script>
const slots = getContext("layoutSlots")
slots.header = header
onDestroy(() => {
slots.header = null
})
</script>
{#snippet header()}
...
{/snippet} This works, but I would love to see a better mechanism for this, that ensures all slots are loaded/unloaded at the same time, as they normally would in a component. One idea would be that top-level snippets in a page are passed as slots, but I don't think would be too great. Additionally, since slots are now just snippets (plain JS values) passed as props, it would be nice to be able to easily pass any data from the page to the layout. One idea might be a designated // +page.svelte
import type { LayoutProps } from "./$types"
export const layoutProps: LayoutProps = {
header, // snippet
other: 5, // arbitrary value
} Could even be made reactive with |
#627 (comment) Is this solution implemented on the kit code base? |
#627 (comment) Passing snippets from children pages/layouts to parent layouts using a context like this works well in simple cases, but it quickly becomes trickier if you want to properly handle reverting to parent snippets when unmounting nested routes/layouts. For example, in a dashboard layout where children routes can set different sidebar / header / footer content, the context could look more like: <!-- /(dashboard)/+layout.svelte -->
<script lang="ts">
let header = $state<Snippet[]>([]);
let sidebar = $state<Snippet[]>([]);
let footer = $state<Snippet[]>([]);
setContext('dashboard', {
setHeader(snippet: Snippet) {
header.push(snippet);
return onDestroy(() => {
header.pop();
});
},
setSidebar(snippet: Snippet) {
sidebar.push(snippet);
return onDestroy(() => {
sidebar.pop();
});
},
setFooter(snippet: Snippet) {
footer.push(snippet);
return onDestroy(() => {
footer.pop();
});
},
});
let { children }: { children: Snippet } = $props();
</script>
<div>
{#if header.length}
<header>
{@render header[header.length - 1]?.()}
</header>
{/if}
{#if sidebar.length}
<nav>
{@render sidebar[sidebar.length - 1]?.()}
</nav>
{/if}
<article>
{@render children()}
</article>
{#if footer.length}
<footer>
{@render footer[header.length - 1]?.()}
</footer>
{/if}
</div> This context can then be used simply like so: <!-- /(dashboard)/projects/+page.svelte -->
<script lang="ts">
import { getContext } from 'svelte';
let { data } = $props();
const { setHeader, setSidebar } = getContext('dashboard');
setHeader(header);
setSidebar(sidebar);
</script>
{#snippet header()}
<div>Some header</div>
{/snippet}
{#snippet sidebar()}
<section>Some links</section>
{/snippet}
<h1>{data.project.title}</h1> |
Based on #627 (comment), I implemented a wrapper for the Context API that allows you to get & set only the last element of a property, and supports dynamic properties. <!-- /(dashboard)/+layout.svelte -->
<script lang="ts">
import { setStackContext } from '$lib/stackContext.svelte.js';
const dashboard = setStackContext('dashboard', {});
let { children }: { children: Snippet } = $props();
</script>
<div>
<header>
{@render dashboard.header?.()}
</header>
<nav>
{@render dashboard.sidebar?.()}
</nav>
<article>
{@render children()}
</article>
<footer>
{@render dashboard.footer?.()}
</footer>
</div> <!-- /(dashboard)/projects/+page.svelte -->
<script lang="ts">
import { getStackContext } from '$lib/stackContext.svelte.js';
let { data } = $props();
const dashboard = getStackContext('dashboard');
dashboard.header = header;
dashboard.sidebar = sidebar;
</script>
{#snippet header()}
<div>Some header</div>
{/snippet}
{#snippet sidebar()}
<section>Some links</section>
{/snippet}
<h1>{data.project.title}</h1> Internally, it works by keeping an internal object with stacks, and by returning a Proxy for end user consumption. // src/lib/stackContext.svelte.ts
import { getContext, onDestroy, setContext } from 'svelte';
type RecordStack<V> = { [P in string]: Array<V> }
export function setStackContext<V>(key: string, context: Record<string, V>) {
const internalRecordStack = $state(createRecordStackFrom<V>(context));
const proxy = setContext(Symbol.for(key), createPublicProxyFor<V>(internalRecordStack))
return proxy;
}
function createRecordStackFrom<V>(context: Record<string, V>) {
return Object.entries(context).reduce((acc, [k, v]) => {
acc[k] = [v];
return acc;
}, {} as RecordStack<V>);
}
function createPublicProxyFor<V>(internalRecordStack: RecordStack<V>) {
return new Proxy(internalRecordStack, {
get(target, propertyKey, receiver) {
if (typeof propertyKey === 'symbol') return Reflect.get(target, propertyKey, receiver);
return Reflect.get(internalRecordStack, propertyKey, receiver)?.at(-1);
},
set(target, propertyKey, value, receiver) {
if (typeof propertyKey === 'symbol') return Reflect.set(target, propertyKey, value, receiver);
if (!(propertyKey in target)) {
Reflect.set(target, propertyKey, [], receiver);
}
const stack = Reflect.get(target, propertyKey, receiver);
stack.push(value);
onDestroy(() => stack.pop());
return true;
},
}) as Record<string, V>;
}
export function getStackContext<V>(key: string) {
return getContext<ReturnType<typeof setStackContext<V>>>(Symbol.for(key))
} |
sveltejs/svelte#12713 (comment) If svelte supports exporting snippets in the future, I think it can solve this problem with SSR in a very elegant way! |
Let's say I have a
_layout.svelte
like the following:And now I have a child view (let's call it
homepage.svelte
which inherits that layout, which puts some stuff into the named slot, and the rest into the default slot:Right now this is not allowed, and the following error is thrown:
Element with a slot='...' attribute must be a descendant of a component or custom element
. Are there technical or usage reasons why we wouldn't want to treat a layout like any other component with respect to slots?The text was updated successfully, but these errors were encountered: