-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Svelte 5: Support passing of snippets or components? #9774
Comments
I'm wondering how common this really is that it requires an ergonomic shortcut. Snippets are strictly inferior to components, so one could just do this <List {items}>
{#snippet item(data)}
<ListItem {...data} />
{/snippet}
</List> |
I agree. This feels like an additional thing to do at runtime (affecting everyone's components in all apps) for the sake of a weird use case, when the alternative is to just write a little bit more of more-explicit code. |
It’s only an additional thing to do if snippets and components work differently, but currently they are pretty similar under the hood. |
Another compromise would be a utility function for converting a component to a snippet so you could do something like: <List {items} item={asSnippet(ListItem)} /> This should not affect anything else. (Tried to make that work in user land but could not quite manage to do so.) |
The key word here is 'currently'. Things will doubtless evolve in future (#9672 is an example of a change we might make, albeit one that probably wouldn't affect this sort of interoperability), and it's very likely that we'd come to regret doing this. That's enough of a reason for me to oppose this proposal, but beyond that I don't think it's desirable anyway — I've often regretted these kinds of loosey-goosey APIs. Prefer clarity and consistency over convenience, in almost all cases. |
Totally fair to want to preserve the ability to iterate. But over time I wouldn't be surprised if they naturally converged. I would argue that if In my head, the difference between a Component and a snippet is pretty similar to the difference between a function defined at the top-level and one defined in some nested scope. Here's my REPL with a component and snippet side-by-side. Looking at the compiled output, it looks like the only difference is the way args are passed, and I would say it kinda looks like the snippet could actually benefit from being treated more like the And I have to go here because it's the natural next question: do you need {#snippet MySnippet({ arg1, arg2 })}
<div class={arg1}>{arg2}</div>
{/snippet}
<MySnippet arg1="foo" arg2="Bar" /> |
To be clear: if the only way to implement this proposal is to have code that looks like this: return isSnippet(thing) ? renderSnippet(thing) : renderComponent(thing) then please don't do it. It works only if they're naturally interoperable. That is a loosey-goosey API. |
In #9903 it was asked to not only differentiate between snippets and components, but also between those two and regular functions - i.e. |
{#snippet actionsDom()} can snippet support this syntax? |
For the purposes of my component mocking tool, it would be great to be able to pass in a component as the argument for a snippet prop, because the props are written in Typescript. If
Then I could build up a variant like this: import MockAvatar from './MockAvatar.svelte'
export const myVariant = {
name: "Bob",
image: asSnippet(MockAvatar),
} |
* feat: provide isSnippet type, deduplicate children prop from default slot fixes #10790 part of #9774 * fix ce bug * remove isSnippet type, adjust test * fix types * revert unrelated changes * remove changeset * enhance test * fix * fix * fix * fix, different approach without needing symbol --------- Co-authored-by: Rich Harris <[email protected]>
I would love to have {#if isSnippet(item)}
{@render item(args)}
{:else if isComponent(item)}
<svelte:component this={item} {...args} />
{/if} As an example I currently have a component which is an abstraction similar to the <script>
let { as, children, ...props } = $props();
</script>
{#if typeof as === 'function'}
{@render as(props, children)}
{:else if typeof as === 'string'}
<svelte:element this={as}>{@render children?.()}</svelte:element>
{:else}
{@render children?.()}
{/if} Being able to turn the above into something like this would be amazing: <script>
let { as, children, ...props } = $props();
</script>
{#if isSnippet(as)}
{@render as(props, children)}
{:else if isComponent(as)}
<svelte:component this={as} {...props}>{@render children?.()}</svelte:component>
{:else if typeof as === 'string'}
<svelte:element this={as}>{@render children?.()}</svelte:element>
{:else}
{@render children?.()}
{/if} |
+1 for an isSnippet / isComponent function to support more flexible reusable components. With filename disappearing, I started looking at function names and even tempted by scanning fn.toString but of course that's brittle.... appreciate advice on a less brittle workaround if this won't make the v5 cut. Cheers. |
Here's a simple work around I've found. type Props = {
icon: SvelteComponent | Snippet
};
let {
icon = ChevronDown
}: Props = $props();
<button class="dropdown-title" {onclick}>
{#if icon && icon.length === 1}
{@render icon()}
{:else if icon}
<svelte:component this={icon} class="w-4" />
{/if}
<span>{title}</span>
</button>
|
Assuming this continues to hold true and with the addition of createRawSnippet and Rich's demo of using it to turn a component into a snippet I believe this issue can be closed. Here are all the helpers we need: function isSnippet(item: Component | Snippet) {
return item.length === 1
}
function isComponent(item: Component | Snippet) {
return item.length === 2
} import { createRawSnippet, hydrate } from 'svelte';
import { render } from 'svelte/server';
function componentToSnippet(Component: Component) {
return createRawSnippet((props_function) => {
const props = props_function ? props_function() : {};
return {
render: () => `<div>${browser ? '' : render(component, { props }).body}</div>`,
setup(target) {
hydrate(component, { target, props })
}
}
});
} See them demoed here. <<- edited to use the Component type and |
@Bishwas-py , @jacob-8 See:
Have you found something that works with the new Component type? |
Sorry, my naive oversight. I only did that demo in the repl to explore something I want to use in the future. I don't have any Svelte 5 projects yet due to an incompatibility I'm waiting on so I'll not dig further at the moment. But I'm sure it wouldn't be hard for someone using Svelte 5 in VS Code with intellisense running to dig into and show us the proper way to type that helper function. |
OK so the length check does work in Svelte 5 (runes mode)... the runtime check being more important than the type system of course. It also works both in SSR and client render. I also used the fancy new way of rendering a component directly, note the upper case first character of the prop name which is required for this to work. export const isComponent = (value: Component | Snippet): value is Component =>
value.length === 2;
export const isSnippet = (value: Component | Snippet): value is Snippet =>
value.length === 1; <script lang="ts">
import { isComponent, isSnippet } from './helpers'
// Props
type Props = {
name: string
Icon: Component | Snippet;
};
let { name, Icon }: Props = $props();
</script>
{#if isComponent(Icon)}
<p>{name} Component:</p>
<Icon />
{/if}
{#if isSnippet(Icon)}
<p>{name} Snippet:</p>
{@render Icon()}
{/if} Thanks for the suggestion. |
I have a component called |
What is the use case for allowing any of these things? Can these problems be solved by designing the component API in question differently, for example requiring components and snippets be passed through different props? |
For those from React you may remember many of us had helpers to check if a Prop was a React Component, React Element, string etc. Now this is just my .02, but it's early. Perhaps a bit more time should pass before adding in these helpers officially? Maybe a little documentation/examples would suffice? That said I made a Conditional component which works similar to what @bdmackie has done. I feed it the prop from the parent and it sorts this out. So its a one time helper component, use as needed. I think where you run into this most is when you're trying to make things Generic or its in a Lib and you want to allow the user to pass in their own Snippet/Component or something. |
Yes the use case is for library / design systems. My motto for most abstractions is to "make the easy things easy and the harder things possible." Using the example of my stackblitz link above, imagine an button component that can display some extra content on the side. Typically that would be an icon, and in that case passing an icon component directly has the best ergonomics: <Button LeftIcon={MyCoolIcon} /> However if more content control is required, a snippet works well: {#snippet MyBigIcon()}
<MyBigIcon size="xl" />
{/snippet}
<Button LeftIcon={MyBigIcon} /> The alternative might be to have separate properties for components and snippets, which gets bloated pretty quickly in a reusable component. The other alternative would of course be to just always use snippets. Unfortunately snippets add a lot of code to a page when they are being used for both common case and escape hatch requirements. An aside: while writing this I thought of another alternative which is to penalise the snippet form using an object property type. That'd be more resilient to svelte framework changes but degrades snippet form ergonomics: {#snippet MyBigIcon()}
<MyBigIcon size="xl" />
{/snippet}
<Button LeftIcon={{snippet: MyBigIcon}} /> I feel conceptually snippets and components meet overlapping use cases enough to warrant an explicit check, however if no helpers appear and I get back to that area I may just test the approach in the aside above. |
I agree @bdmackie. Working through same concepts. My point to saying it's "early" was more or less to suggest that at some point some use case usually comes along and forces the hand one way or another. Building out a Notifications/Snackbar controller component at the moment. I could make a case for either pattern to be fair. While it is more verbose, using a snippet is certainly more flexible and gives more latitude. Snippets don't look as clean, particularly for a lib, but perhaps we should just get over that. Not campaigning either way here just speaking out loud... Of course those feelings could change after I build this out LOL. |
This is where I land too. Obviously it's possible to come up with use cases for
...but I've always found that to be questionable advice at best and actively harmful when it comes to API design. It's always possible to design a more explicit API. To take the icon example: rather than allowing it to be either a component or a snippet, you could expose two separate props (and perhaps throw an error if both are provided): <Button Icon={MyCoolIcon}>
button text
</Button>
<Button>
{#snippet icon()}
<MyBigIcon size="xl" />
{/snippet}
button text
</Button> The alternative involves adding things like private symbol properties to functions and checking for them — this adds overhead, and makes things less composable (or forces us to create new APIs for programmatically creating components, when today you can do wacky things with function composition). As such it feels unlikely that we'd go down this road, so I'm personally inclined to close this issue but at the very least I'm going to move it off the list of issues blocking a 5.0 release. |
My resolve waned when I realised there were alternatives in my post so I'm good personally. Your team has "bigger fish to fry". (edit: a bit presumptuous of me the OP may want to respond). I appreciate the time you took in commenting on API design. I'm still coming to grips with Svelte's 'leadings' on API boundaries. Yes there's a choice to many-property however that smelt a bit on both sides (inside and out) to me, perhaps it is because JavaScript/TypeScript embraces polymorphism so it feels odd to step away from it in API design. Totally agree re Postel's law, to borrow a cooking analogy, "you can add more salt but you can't take it away" (well my Nan had a trick with potatoes but I digress). Ironically if the API gives freedom to put any kind of content in children content then the library author has the same problem with little control over rendering if semantics are present. I've wondered if being able to analyse children content would help with more explicit composable APIs, which might mean a win for the lovers of declarative style. I can imagine there have been thoughts on that in the team. Since I first posted I've also come to learn more about svelte actions and how they break out into objects-as-attributes, which I think is why upon returning I thought of a similar pattern here. That seems to smell a bit too, however organisation of that can be pulled up into the script section if it gets unwieldy, which is still kinda declarative (slightly looks to the side). |
As the symbol for the snippet was removed, now the only choice is to do a {#snippet item()}
some adhoc content
{/snippet}
<MenuComponent items={[
{ type: "header", content: SomeRepeatedComponent }
{ type: "item", content: item }
]} /> Ultimately I would only need a way to render either a component or a snippet coming from a single prop as it's not used for anything else. Having to inspect the function is a crutch, but I like separating |
I would consider the mechanics of how UI gets passed around a pretty big fish for Svelte 5. |
To be honest I had no real idea about the size of fish involved :) Perhaps it was better to say it felt like 'the ship had sailed' given snippets had been built for a while and it felt the release of Svelte 5 was/is imminent. Perhaps they missed a trick by not unifying when they designed Snippets, or have other constraints we don't know about. I imagine the design of Snippets was considered to be "the new slots" rather than something all-encompassing. Overall I agree it's strange the concepts are separate and that we bear the burden of differentiating them. I suspect the end result will be a fragmentation of extensibility approaches used in the ecosystem. I notice there is a createRawSnippet function so I wonder if there's a way to build your own 'render either' utility? the component render function only works on the server I think so it could blow up on the client side? |
I already provided all the code needed for that above - especially look inside the demo at lines 61-67. You just need to grab the pieces needed from there for your use case. There is no render blow up on client because the example usage (first given by Rich and then modified to work with props) uses render on the server and hydrate on the client. |
@jacob-8 I again circled back to this (it's the gift that keeps on giving) but I noticed your function inserts an additional |
Haha, yes we are. Try the |
Describe the problem
This came up in Discord:
It might be useful for library components to accept either a snippet or a regular component as input.
Describe the proposed solution
There would have to be a way to either:
@render
accept components.The first argument (in case Svelte 5: Variadic snippets #9672 is implemented) to the called function would then be considered the props in case of a component.
Alternatives considered
Always require the use of snippets, which can just use the desired component internally.
Potentially add a conversion utility from component to snippet:
Importance
nice to have
The text was updated successfully, but these errors were encountered: