sveltejs / kit

web development, streamlined
https://svelte.dev/docs/kit
MIT License
18.78k stars 1.96k forks source link

Named layouts thingy #12928

Open rChaoz opened 1 month ago

rChaoz commented 1 month ago

Describe the problem

This has been discussed a lot in the past (for example #627), but now with Svelte 5 released, I think we can take a look at this from a different angle, considering all the new Svelte 5 toys.

Describe the proposed solution

Let's consider a rather simple usecase - header that changes based on page. Most pages would want to use the same (default) header, while some can customize it. This could be achieved with snippets or with additional +page-<name>.svelte files, for example +page-appbar.svelte.

With snippets (demo)

Allow pages to export snippets:

<script>
  export const snippets = { appbar }
</script>

{#snippet appbar()} ... {/snippet}

that can then be used in layouts:

<script>
  const { children, appbar } = $props()
</script>

<aside> {@render appbar?.()} </aside>
<main> {@render children()} </main>

Pros: Code sharing between app bar and page, easy to use Cons: Not SSR-friendly. The page needs to first render before the snippet is created, which then needs to be sent to the layout for re-rendering.

With additional files (demo)

Allow placing additional +page files that can be used in layouts:

<script>
    const { children, appbar } = $props()
</script>

<aside> {@render appbar()} </aside>
<main> {@render children()} </main>

Pros: SSR-friendly, intuitive (+page goes to children, +page-x goes to x) Cons: Need a communication mechanism to synchronize the page and named slots, as they don't share code.

Both of these method can sort-of be achieved in user land, but neither of them is great.

With magic (not demo)

Syntactically I think this is great, but it would need a bit (more) help from the Svelte compiler to work.

<!-- +page.svelte -->
<script>
    const { data, Header, Footer } = $props()
</script>

<Header> This is a header </Header>
Main content
<Footer> This is a footer </Footer>

<!-- +layout.svelte -->
<script>
    const { children, header, footer } = $props()
</script>

<header> {@render header()} </header>
<main> {@render children()} </main>
<footer> {@render footer()} </footer>

For this to work, a div with display: contents would need to be created that would hold the header/footer content, which is rendered inside the snippet, and a virtual component with this div as its target would be passed to the page:

<script>
  import Layout from "./+layout.svelte"
  import Page from "./+page.svelte"

  const headerDiv = createDivDisplayContents()
  const footerDiv = createDivDisplayContents()
  const Header = createComponent({ target: headerDiv })
  const Footer = createComponent({ target: footerDiv })
</script>

<Layout>
  {#snippet header()}
    {@insert headerDiv}
  {/snipper}
  {#snippet footer()}
    {@insert footerDiv}
  {/snipper}
  <Page {Header} {Footer} />
</Layout>

Conclusions

This is pretty unhinged, but would it be doable?

brunnerh commented 1 month ago

This would be something you can do already:

// $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