Skip to content

Svelte 5: Avoiding {#snippet} boilerplate with the ability to pass parameters #10678

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

Open
katriellucas opened this issue Mar 1, 2024 · 27 comments

Comments

@katriellucas
Copy link

katriellucas commented Mar 1, 2024

Describe the problem

As of now, if we want to pass a Snippet as props to a component, it has to be without parameters, this is due to the Snippet props expecting a snippet function to be passed. if we want parameters on our Snippet function, it needs be nested inside the component as a child.

This process can get boring and boilerplatey very quickly, specially with more complex cases were different kinds of Snippets are used inside components.

Describe the proposed solution

Solution 1

It would be nice if Svelte let us pass parameters directly as props, using the example above, maybe:

// Option 1
<Chip label="Test" media={icon('blue')} />

or

// Option 2
<Chip label="Test" media={() => icon('blue')} />

or even

// Option 3
<Chip label="Test" media={(() => icon('blue'))()} />

Personally, Option 2 feels more natural as this is how we already pass functions on Svelte 5 using events such as onclick.

Solution 2

Some kind of new sintax for such cases might be interesting to think about.

<Chip label="Blue">
  {@render icon('blue') on media} // where Chip must have a "media" prop, show an error if doesn't
</Chip>

Solution 3

Do nothing. There is an argument on using {#each} loops but it feels somewhat overkill for 3 or 4 components.

Importance

would make my life easier

@katriellucas katriellucas changed the title Svelte 5: Avoiding {#snippet} boilerplate with the ability to pass parameters. Svelte 5: Avoiding {#snippet} boilerplate with the ability to pass parameters Mar 1, 2024
@katriellucas
Copy link
Author

katriellucas commented Mar 1, 2024

You can already trim this down by using children:

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAA7WVYW_bNhCG_8pVLWAbsE0nxYpWs4O1-zRgn7ah-1AVBS3S0tUUSZBUFEPwf9-RspM48QJ3wADD1onvezw-pI99tkElfZZ_6TPNG5nl2Udrs2kWdjYG_laqICn2pnVlfLP0pUMbbgoNgI01LsCvNVrYONPAaM5iMB9so0Iv2b280P1rr9FaGQBLo8elUcbBCorMSVFkkz1pwtLfVnCLsvtk7lZFtoAFXL2jT5HBXaO0p3d1CDZnrOu6efd2blzFrheLBSMjiToUoSZRdMRJA8CyRFcqCWVM-J405e7w4A6_xECt-lTPniXXMqajp54dSt6fLsA6E8GNW6fSCmJNnopqrTJczDvcYiMF8lRejGyMWGmaxmjPrtjVmv2ZIH373VRmnoo_EMCmAu_KVU_J9ycLglpiVYdjxFV8tBtbZOxZrctXsxkwBn_XPMBvEGrUW8Aw8oBCcpXDeN0GEEZ6PQrQGbedJFvaTMXXUlHuv6QPNFHMfjrwSbWSBtIaV33azsn-ueyPuLEnqtGanKNz2gHHg_yI-Jz2c3tGOPrBTfjAPvzEKNP8u0-b8O06bsOhttns5jHFj1Sa0zwgFbmbwvfWB1C4lVCbDobTDpZ7D5tWlwEpff6faY4nsLqBy5g-0l5OdjBdzPdU_n9STme1k6ClFBAMnU76FnxHh3WLWnBYczGBo-nf2J6nm_7W_S9OaiHdKbHYpqLjjDfBPmN90XTkfep74P2C9fPzUo--H24y58kPbeZxAT7slExz0vAUYvvpY1QEgd6qiB-1Qi1nGyXvfh6GGu4q1DMX-1EO7-3wPmUe8tGd0RiBG5Qiy4Nr5X56f8XEuS-9YxR1237gM4WyRiWIC-yp674hMtaPJ09umaXAWygV_RmJJxnsU6DHJOOh3_Ypd6qcnE-RzGOGIw-usNIzDLLxOZRSU0844FgbR6lz8EahgCt7B5Xju5PBmeMCW_8A60W-Q6fP4e31vdpyIVBXOV2JF_H-uv8HWgimot4HAAA=

But nevertheless an argument could be made for a snippet.bind() function.

That is correct for when you have only one children, what if you have two and need the label to be between then? What if you need three? A Card component that uses header(), body() and footer(), we might be able to take out body() with children but any styling or elements in between might make the other two cumbersome.

image
image

@dummdidumm
Copy link
Member

What people have wanted elsewhere is the ability to decorate components. This feels like a use case for decorating snippets, i.e. take an existing snippet and return an augmented snippet. Maybe something like this (and similar for components).

<script>
  import { decorateSnippet } from 'svelte';
  const decorated_foo = decorateSnippet(
    foo,
    (snippet /* the original snippet */, args /* the arguments passed to the snippet */) =>
      snippet('my own argument; ignoring the args')
  );
</script>

{#snippet foo(prop)}
  ..
{/snippet}

<Component foo={} />

@brunnerh
Copy link
Member

brunnerh commented Mar 1, 2024

That seems unnecessarily verbose for simple cases.
I like @Prinzhorn's suggestion of having a bind function.

<Chip label="Test" media={icon.bind('blue')} />

Could be called something else, but given that snippets are declared like other functions, this would logically match (even if the generated arguments work differently); Svelte would need to override the existing bind - not sure if that causes trouble - or declare a separate function.

@katriellucas
Copy link
Author

katriellucas commented Mar 5, 2024

To add to the discussion, here is another powerful pattern (in my opinion) that would be possible with Snippet parameter/bindings:

Svelte 5 Repl

This case is even worse because I can't even use the nested children, it's specially bad if I have different components in the same stack, (sometimes this might be beneficial, like showing a modal on top of a fullscreen one) due to how and where the {@render} functions might be inside of each component.

PS: I'm not sure if I should have created a new issue for this, if so, please tell me.

@Rich-Harris
Copy link
Member

What's wrong with this?

<script>
  let { label, snippet, data } = $props()
</script>

<div class="chip">
  {@render snippet(data)}
  {label}
</div>
<Chip label="Test" />
<Chip label="Blue" snippet={icon} data="blue" />
<Chip label="Red" snippet={icon} />
<Chip label="Svelte" snippet={profile} />
<Chip label="Vue" snippet={profile} data="https://upload.wikimedia.org/wikipedia/commons/9/95/Vue.js_Logo_2.svg" />

@brunnerh
Copy link
Member

brunnerh commented Apr 4, 2024

  • It pushes the burden of making snippets more usable onto every single component accepting snippets.
  • For every snippet there would be a need to have (at least) two properties to keep them flexible; it's bloat.
  • Neither the existence of these properties nor their names are enforced.
    Component libraries will probably not have them by default (which you cannot change locally) and there will be naming inconsistencies across codebases.
  • You cannot easily use more than one argument and need to potentially add more properties, rewrite snippets if you can or wrap them in another snippet if you can't.

I would find a bind function for snippets extremely useful.
It makes programmatic usage of snippets a lot more flexible/easy (I did not find a way to do this in userland so far).

Example: REPL

Right now this is needlessly complicated; at multiple levels the APIs need to be aware of snippet args.
Of course one could create new components, but then we are back to Svelte 4 levels of overhead for what could be a very simple thing.

Effect that snippet.bind would have on example above

App.svelte

  async function onEditName(user) {
  	const data = $state({ name: user.name });
- 	const result = await showModal(nameEditor, data);
+ 	const result = await showModal(nameEditor.bind(data));
  	if (result == 'ok')
  		user.name = data.name;
  }

Dialog.svelte

- import { mountSnippet } from './MountSnippet.svelte';
  ...
- export function showModal(snippet, args) {
+ export function showModal(snippet) {
	return new Promise(resolve => {
		const dialog = mount(Dialog, {
			target: document.body,
			props: {
+				children: snippet,
				onclose(e) {
					resolve(e.target.returnValue);
					unmount(dialog);
				}
			}
		});
-		mountSnippet(dialog.ref, snippet, args); // hacky
		dialog.ref.showModal();
	});
}

MountSnippet.svelte

- <script context="module">
- 	import { mount } from 'svelte';
- 	import MountSnippet from './MountSnippet.svelte';
- 
- 	export function mountSnippet(target, snippet, args) {
- 		return mount(MountSnippet, { target, props: { snippet, args }});
- 	}
- </script>
- 
- <script>
- 	const { snippet, args } = $props();
- </script>
- 
- {@render snippet(args)}

@Rich-Harris
Copy link
Member

I'm going to need a more convincing example I'm afraid — perhaps I'm being dense, but why wouldn't you just write Dialog.svelte like this?

-<svelte:options accessors />
<script context="module">
  import { mount, unmount } from 'svelte';
- import { mountSnippet } from './MountSnippet.svelte';
  import Dialog from './Dialog.svelte';

  export function showModal(snippet, args) {
    return new Promise(resolve => {
      const dialog = mount(Dialog, {
        target: document.body,
        props: {
+         snippet,
+         args,
          onclose(e) {
            resolve(e.target.returnValue);
            unmount(dialog);
          }
        }
      });
-     mountSnippet(dialog.ref, snippet, args); // hacky
-     dialog.ref.showModal();
    });
  }
</script>

<script>
- let { ref, children, ...rest } = $props();
+ let { snippet, args, onclose } = $props();

+ function show(node) {
+   node.showModal();
+ }
</script>

-<dialog bind:this={ref} {...rest}>
- {@render children()}
+<dialog use:show {onclose}>
+ {@render snippet(args)}  
</dialog>

If you needed to also do <Dialog>...</Dialog> in places (and expose an API like export function showModal or whatever), then that's also very easy to do.

Adding things like snippet.bind(...) pushes us into uncanny valley territory — it really implies that snippet is Just A Function. If we're going to do that, then we should consider whether we want a true programmatic API for snippets...

mount(App, {
  target,
  props: {
    mySnippet: (firstname, lastname) {
      const element = document.createElement('h1');
      $effect.pre(() => {
        element.textContent = `Hello ${firstname()} ${lastname()}!`;
      });
      return element;
    }
  }
});

...and what challenges that would entail. (At the very least, we'd have to find a different solution to #10800.)

@brunnerh
Copy link
Member

brunnerh commented Apr 4, 2024

why wouldn't you just write Dialog.svelte like this?

It comes down to the points I listed at the start of my comment.
You have to design components around this specific use, which is just not good.

Dialog itself should not need to care about anything except that it can receive children.
Imagine it's a component from a library and I want to write a separate showModal utility function on my side (REPL where it's split).

If you fear that the name could lead to confusion, a separate function could be used or a different name. E.g.

snippet.withArgs(...args)
bindSnippet(snippet, ...args) // separate => not quite a "regular" bind

@Rich-Harris
Copy link
Member

Imagine it's a component from a library and I want to write a separate showModal utility function on my side

You'll always need cooperation from the underlying <Dialog> component because you need either a reference to the element (so you can call element.showModal()) or an API for that use case, like a modal prop or an exported function. But assuming you have that, you can do this, which seems... fine? Maybe a new API would make that very slightly more convenient, but the bar for new API is very high.

It occurs to me that the Just A Function approach I sketched above doesn't really work, because of SSR.

@brunnerh
Copy link
Member

you can do this, which seems... fine?

That still adds a lot of overhead and boilerplate.

To use it, the snippet can only have one argument.
If you have an existing snippet that has multiple arguments, the snippet has to be changed to make it conform to this.

It's really not great. Snippets being so much of a black box really limits their usefulness in code as soon as arguments are involved. I see a lot of unused potential here when it comes to more dynamic UI composition.

It occurs to me that the Just A Function approach I sketched above doesn't really work, because of SSR.

A utility function that just converts DOM elements/function returning DOM elements to a snippet could be useful.
Though that can be somewhat worked around in userland code.

@pauldemarco
Copy link

pauldemarco commented Sep 12, 2024

why wouldn't you just write Dialog.svelte like this?

It comes down to the points I listed at the start of my comment. You have to design components around this specific use, which is just not good.

Dialog itself should not need to care about anything except that it can receive children. Imagine it's a component from a library and I want to write a separate showModal utility function on my side (REPL where it's split).

If you fear that the name could lead to confusion, a separate function could be used or a different name. E.g.

snippet.withArgs(...args)
bindSnippet(snippet, ...args) // separate => not quite a "regular" bind

I am for something like this. The alternative will be users realizing that when they change their snippet to take arguments, they can no longer simply pass it as an attribute, and will have to move it to a full {#snippet } block just to pass their args:

<AppBar leading={mySnippet} />

"woops i'd like an arg"

<AppBar>
    {#snippet leading()}
      {@render mySnippet({newArg: "hello"})
    {/snippet}
</AppBar>

vs

<AppBar leading={mySnippet.with({newArg: "hello"})} />

@7nik
Copy link
Contributor

7nik commented Sep 12, 2024

Btw, since #12507 (svelte@5.0.0-next.196) snippets aren't signed anymore and you can try to pass any function as a snippet (though you still can get a typing error in TS).

Parameters are getters, so it will look like

<Chip label="Red" media={(anchor) => icon(anchor, () => 'blue')}/>

REPL

@pauldemarco
Copy link

pauldemarco commented Sep 12, 2024

Btw, since #12507 (svelte@5.0.0-next.196) snippets aren't signed anymore and you can try to pass any function as a snippet (though you still can get a typing error in TS).

Parameters are getters, so it will look like

<Chip label="Red" media={(anchor) => icon(anchor () => 'blue')}/>

REPL

Super interesting. This works, but now I can see it "pop in" after the initial render, while the ones using the full {#snippet } block are rendered and ready to go.

@adiguba
Copy link
Contributor

adiguba commented Oct 15, 2024

Hello,

Btw, since #12507 (svelte@5.0.0-next.196) snippets aren't signed anymore and you can try to pass any function as a snippet (though you still can get a typing error in TS).

Based on that, it's possible to make a function to wrap the boilerplate :

	function snip(fn, ...args) {
		return (node) => {
			fn(node, ...args.map(a => ()=>a ));
		}
	}
-<Chip label="Red" media={(node) => icon(node, () => 'blue')}/>
+<Chip label="Red" media={snip(icon, 'blue')}/>

REPL

@brunnerh
Copy link
Member

brunnerh commented Oct 15, 2024

I would say that a userland solution like this is only acceptable if we can get guarantees on the snippet types.

Otherwise this is the equivalent of building on top of what used to be svelte/internal which is not intended and can break at any point.

A first step would probably be that the Snippet type does not lie about the actual shape of the function.

Also, SSR/CSR complicate things — the functions behave slightly differently in each environment.

@timephy
Copy link

timephy commented Oct 15, 2024

Hey, i wanted show what this problem leads me to do.

I want to show this case to underline the usefulness having a feature solving this issue could/would bring.


I have a TabBar that switches the content of the view above it. I support desktop and mobile, and some of the tabs can be shown in a Sidebar or the Main view. These differentiations require me to define many snippets to work.

<!-- General Shape of a TabBar Item (button in the TabBar) -->
{#snippet item(item: TabItem, selected: boolean, select: SelectTab)}
    <button
        class="{selected ? 'text-blue' : 'text-step-700'}
            {LAYOUT.platform === 'desktop' ? 'min-w-[5.5rem]' : 'flex-1'}"
        onclick={select}
    >
        <Icon data={item.icon} />
        <p>{item.title}</p>
    </button>
{/snippet}

<!-- Here I have to define the snippets of the tabs, because of the problem this issue discusses -->
{#snippet itemGroup(selected: boolean, select: SelectTab)}
    {@render item(
        {
            title: "Group",
            icon: people_fill,
        },
        selected,
        select,
    )}
{/snippet}
<!-- Here I have to create versions of the same snippet, because again, we cannot bind parameters to snippets in code -->
{#snippet viewGroup_Sidebar()}
    <GroupTab {group} safearea={safeareaSidebar} />
{/snippet}
{#snippet viewGroup_Main()}
    <GroupTab {group} safearea={safeareaMain} />
{/snippet}

<!-- Now imagine that I have 5+ different tabs, not only "Group"... -->

As one can see, this creates VERY redundant code/definitions, and gets out of hand rather quickly.

If LAYOUT.platform was not "global", but could differ in different parts of the application, then I would need to create EVEN MORE snippets to account for this, such as for the difference in Sidebar and Main.

@timephy
Copy link

timephy commented Oct 26, 2024

I would like to add another feature that would make things easier.

It would be great to not only allow users to use "prepopulated" snippets in-place, but also allow the same for components.

<script lang="ts">
    import Text from "./Text.svelte"
    import Child from "./Child.svelte"
</script>

{#snippet text(a: string)}
    <p>{a}</p>
{/snippet}

<Child snippet={text("text")} />
<Child snippet={Text({a: "text"})} />
<!-- syntax for this is up for debate -->

I believe this would greatly improve Svelte's composability.

@Ocean-OS
Copy link
Contributor

Ocean-OS commented Dec 2, 2024

I made a wrapSnippet function that you can find here. Documentation and code for the function is in Hoverable.svelte.
I'm not sure if this is the best possible solution, but it's probably a good solution for most people's needs.

@yustarandomname
Copy link

@Ocean-OS How would you type such a function in typescript?

@Ocean-OS
Copy link
Contributor

Ocean-OS commented Dec 5, 2024

@Ocean-OS How would you type such a function in typescript?

Probably something like this, but as stated here, this relies on the internal snippet structure, so I wouldn't recommend this in a production application.
I'm not too good at Typescript, so this might not be typed the best, but it should work for most things.

import type {Snippet} from 'svelte';
function wrapSnippet<S extends Snippet, A extends any>(snippet:S, args:(A|undefined|never)[]):Snippet<A[]> {
    function fillArgs(arg1:any[], arg2:any[]):any[] {
        for (let index = 0; index < arg1.length; index++) {
            if (arg1[index] === undefined) arg1[index] = arg2.shift();
        }
        if (arg2.length > 0) arg1.push(...arg2);
        return arg1;
    }
    if (snippet.length-1 === args.length) {
        return function ($$anchor:Node) {
            //@ts-expect-error
            return snippet($$anchor, ...args);
        }
    } else {
        return function ($$anchor:Node, ...arg:any[]) {
            let fullArguments = fillArgs(args, arg);
            //@ts-expect-error
            return snippet($$anchor, ...fullArguments);
        }
    }
}

@webJose
Copy link
Contributor

webJose commented Dec 25, 2024

Hello, @Rich-Harris . I see that you weren't too convinced with the use cases provided so far, so here's mine: #14830. I need, from an exported function from a .svelte file, to create a snippet out of another snippet. Maybe this is more compelling?

Thanks in advance for the time invested.

@walker-tx
Copy link
Contributor

walker-tx commented Mar 14, 2025

Howdy! I've been working with the TanStack folks on the Svelte 5 adapter for Table.

TanStack Table is a headless library, and thus much of the table is constructed within the <script> tags. See an example of defining a component-rendering table here. I've built a utility function, renderComponent to make it easy for library users to render their components. A user has recently requested the ability to pass children to renderComponent (among other snippets). I think this is a sensible ask, since component libraries may require a children prop, or additional props (like icon for an icon button).

I wound up here in my research. Using wrappers like @Ocean-OS, @adiguba, and @7nik describe works, but TypeScript is dissatisfied since the snippet must receive 1 more param (the node/anchor) than what the compiler expects.

The ability to pass snippets in a way that satisfies TS would be very useful for TanStack Table's purposes!

@adiguba
Copy link
Contributor

adiguba commented Mar 14, 2025

Why not use createRawSnippet() ?

	const children = createRawSnippet(() => {
		return { render: () => "Hello World" };
	});

@brunnerh
Copy link
Member

brunnerh commented Mar 14, 2025

createRawSnippet does not allow you to easily use components (it is not really meant for user code either specifically because it is so barebones). Interaction with other snippets is likely similar, you probably need to mess with the internals to integrate them.

@walker-tx
Copy link
Contributor

The issue isn't so much creating snippets, but moreso passing them as a prop to a component-rendering function outside of markup context:

renderComponent(
    ButtonComponent, 
    { 
        children: /* snippet */
    }
)

@timephy
Copy link

timephy commented May 3, 2025

I am running into this every few weeks...
It is limiting composability for me, at least the ease of composability

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests