-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
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
Comments
{#snippet}
boilerplate with the ability to pass parameters.{#snippet}
boilerplate with the ability to pass parameters
You can already trim this down by using But nevertheless an argument could be made for a |
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 |
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={} /> |
That seems unnecessarily verbose for simple cases. <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 |
To add to the discussion, here is another powerful pattern (in my opinion) that would be possible with Snippet parameter/bindings: Svelte 5 ReplThis case is even worse because I can't even use the nested PS: I'm not sure if I should have created a new issue for this, if so, please tell me. |
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" /> |
I would find a Example: REPL Right now this is needlessly complicated; at multiple levels the APIs need to be aware of snippet args. Effect that
|
I'm going to need a more convincing example I'm afraid — perhaps I'm being dense, but why wouldn't you just write -<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 Adding things like 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.) |
It comes down to the points I listed at the start of my comment.
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 |
You'll always need cooperation from the underlying It occurs to me that the Just A Function approach I sketched above doesn't really work, because of SSR. |
That still adds a lot of overhead and boilerplate. To use it, the snippet can only have one argument. 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.
A utility function that just converts DOM elements/function returning DOM elements to a snippet could be useful. |
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"})} /> |
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. |
Hello,
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')}/> |
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 A first step would probably be that the Also, SSR/CSR complicate things — the functions behave slightly differently in each environment. |
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 <!-- 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 |
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. |
I made a |
@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. 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);
}
}
} |
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. |
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 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! |
Why not use createRawSnippet() ? const children = createRawSnippet(() => {
return { render: () => "Hello World" };
}); |
|
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 */
}
) |
I am running into this every few weeks... |
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 theSnippet
props expecting a snippet function to be passed. if we want parameters on ourSnippet
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:
or
or even
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.
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
The text was updated successfully, but these errors were encountered: