Skip to content
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

Open
brunnerh opened this issue Dec 5, 2023 · 13 comments
Open

Svelte 5: Support passing of snippets or components? #9774

brunnerh opened this issue Dec 5, 2023 · 13 comments
Milestone

Comments

@brunnerh
Copy link
Member

brunnerh commented Dec 5, 2023

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.

<!-- Snippet allows for adding additional content besides component/s -->
<List {items}>
	{#snippet item(data)}
		<ListItem {...data} />
	{/snippet}
</List>

<!-- Passing just a component -->
<List {items} item={ListItem} />

Describe the proposed solution

There would have to be a way to either:

  • Render a snippet or component regardless of what it is, e.g. make @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.
  • Add built-in functions to determine if something is a component/snippet. E.g.
    {#if isSnippet(item)}
      {@render item(args)}
    {:else if isComponent(item)}
      <svelte:component this={item} {...args} />
    {/if}

Alternatives considered

Always require the use of snippets, which can just use the desired component internally.
Just a bit more verbose.

Importance

nice to have

@dummdidumm
Copy link
Member

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>

@Conduitry
Copy link
Member

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.

@mimbrown
Copy link
Contributor

mimbrown commented Dec 5, 2023

It’s only an additional thing to do if snippets and components work differently, but currently they are pretty similar under the hood.

@brunnerh
Copy link
Member Author

brunnerh commented Dec 5, 2023

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.)

@Rich-Harris
Copy link
Member

It’s only an additional thing to do if snippets and components work differently, but currently they are pretty similar under the hood.

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.

@mimbrown
Copy link
Contributor

mimbrown commented Dec 6, 2023

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 snippets and components became closer over time, it would be more consistent, and indicate that you've got some kind of natural contract emerging that is the right one for Svelte.

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. Array.map is not a "loosey-goosey API" for not caring where/how the passed function was defined.

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 Child component in the way args are passed. No duplicate creation of the passed object, for one.

And I have to go here because it's the natural next question: do you need @render at all? Could it just be:

{#snippet MySnippet({ arg1, arg2 })}
  <div class={arg1}>{arg2}</div>
{/snippet}

<MySnippet arg1="foo" arg2="Bar" />

@mimbrown
Copy link
Contributor

mimbrown commented Dec 6, 2023

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.

@dummdidumm
Copy link
Member

In #9903 it was asked to not only differentiate between snippets and components, but also between those two and regular functions - i.e. isComponent and isSnippet functions.

@zhuzhuaicoding
Copy link

{#snippet actionsDom()}
{#if props.actionsRender === false}
return
{/if}
dom = <Actions {actions} {editing} {placement} {type} />;
{props.actionsRender(restProps, dom) || dom}
{/snippet}

can snippet support this syntax?

@jacob-8
Copy link

jacob-8 commented Apr 15, 2024

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 {@render foo()} syntax isn't able to accept a component if it makes Svelte too complicated, I'm fine with that but would at least love a solution like this:

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)} />

Then I could build up a variant like this:

import MockAvatar from './MockAvatar.svelte'

export const myVariant = {
  name: "Bob",
  image: asSnippet(MockAvatar),
}

Rich-Harris added a commit that referenced this issue May 15, 2024
* 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 <rich.harris@vercel.com>
@sheijne
Copy link

sheijne commented Jun 17, 2024

I would love to have isSnippet and isComponent utilities as a "low-level" api.

{#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 Slot component from radix-ui. It accepts an as prop, which can be a Snippet or a string, I have not been able to figure out a way to differentiate between snippets and components, otherwise it would accept a component as well. It would be great if it could also accept a component as well. A little dumbed down, it looks like this:

<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}

@bdmackie
Copy link

+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.

@Bishwas-py
Copy link

Bishwas-py commented Aug 2, 2024

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>

icon.length === 1 for Snippet and icon.length === 2 for Component

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

No branches or pull requests