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

Add support for slotted components #1037

Closed
elcobvg opened this issue Dec 22, 2017 · 38 comments · Fixed by #4556
Closed

Add support for slotted components #1037

elcobvg opened this issue Dec 22, 2017 · 38 comments · Fixed by #4556

Comments

@elcobvg
Copy link

elcobvg commented Dec 22, 2017

It would be nice if instead of only html elements, we could put components in named slots as well. Like this: <MyComponent slot="title">Title here</MyComponent>

@SilvanCodes
Copy link

Is there any progress on this one? What's the status? :)

@stanf0rd
Copy link

Agree with the proposal, it could be a usefull feature!

@cve

This comment has been minimized.

@Conduitry
Copy link
Member

Just closed #2080 in favor of this one, and a brief summary of the conversation there is:

  • We already are reserving a prop called slot so that we can do <Foo slot='bar'/> someday
  • Another possible syntax is {#slot bar}<Foo/>{/slot}, which would also allow a bunch of DOM nodes and components inside the slot, without them needing to be from a single component

@pngwn raised a concern about syntax 2, which is that it would be an entirely different way of specifying slots, which I hear, and which I'm not sure what to do about.

@jerriclynsjohn
Copy link

jerriclynsjohn commented Oct 16, 2019

Just closed #2080 in favor of this one, and a brief summary of the conversation there is:

  • We already are reserving a prop called slot so that we can do <Foo slot='bar'/> someday
  • Another possible syntax is {#slot bar}<Foo/>{/slot}, which would also allow a bunch of DOM nodes and components inside the slot, without them needing to be from a single component

@pngwn raised a concern about syntax 2, which is that it would be an entirely different way of specifying slots, which I hear, and which I'm not sure what to do about.

If this is getting implemented, I think I'll love to see both implemented. I can see a lot of use cases where I would like to encapsulate the component with additional wrappers and in another scenarios I would like to just use the component. Now i work around this using empty div but then at times it breaks the structure because of the div element and I'll have to add more class utilities to make it work. This will be a great addition for Svelte.

@Tbutnyakov
Copy link

Tbutnyakov commented Oct 29, 2019

I think this is very important feature to implement.
Because for now we need to wrap target components by useless wrapper nodes.
Anyway, thank you for awesome stuff, guys.
Looking forward to.

@ghost

This comment has been minimized.

@nickmanning214

This comment has been minimized.

@mattpilott
Copy link
Contributor

mattpilott commented Nov 23, 2019

I run into this on almost every project and end up doing this as a workaround:
:global([slot="content"])

This allows me to style that extra div in the component that contains the slots but it would be super nice to have <MyComponent slot="content"/> and eliminate that extra div

@qur2
Copy link

qur2 commented Jan 10, 2020

The second proposed syntax is very useful for clarity. In vue, I often end up using quite a lot to separate actual props from the rest. Just an opinion of course.

@guntribam

This comment has been minimized.

@ghost

This comment has been minimized.

@RentecTravis
Copy link

RentecTravis commented Apr 8, 2020

Hi, first time contributing to this project but I'm using Svelte (I love it!) for my own project and I bump up often against the limitation of not being able to pass arbitrary fragments to named slots without a wrapper element. So I've come to this thread a few times now.

The two syntaxes that seem to be preferred are namespaced-component syntax (<svelte:fragment slot="foo">...</svelte>) and block syntax ({#slot "foo"}...{/slot}).

The discussion around those syntaxes has been folded into this discussion about adding a slot attribute to components.

To me the component syntax seems ideal.

  1. If there's a slot attribute that works for elements and (eventually) components, when the desire to pass a component or multiple nodes into a named slot without a wrapper inevitably arises then this syntax seems like a natural extension.

  2. This syntax easily provides all the features of components, like let: bind: and on:. <svelte:fragment /> is just a component with a special name.

  3. Again, since <svelte:fragment /> is basically just a shortcut for creating your own component with a naked default slot, this syntax seems easiest to implement.

Block syntax has a few drawbacks

  1. Block syntax is only used for control structures so far. {#if ...}, {#each ...}, and {#await ...} (the last one blurs the line a bit). Passing fragments to child components using a syntax that's otherwise reserved for a completely different kind of logical operation adds mental burden.

  2. This syntax is not flexible and is not future proof. Directives and attributes/parameters can be given to elements and components. How would that work with this syntax?

  3. And seems harder to implement.

Workaround using slot attribute

<!-- Fragment.svelte -->
<slot />

If components gain the slot attribute, then it would be possible to implement the proposed behavior of <svelte:fragment /> by creating a component that has a default slot with out any wrappers. However, I think it's still a good idea to add <svelte:fragment /> so everyone who encounters this common use case doesn't have to come up with their own slightly different solutions.

@jacwright
Copy link
Contributor

Another possible syntax is {#slot bar}<Foo/>{/slot}, which would also allow a bunch of DOM nodes and components inside the slot, without them needing to be from a single component

The original syntax allows for multiple DOM nodes to be used while still keeping with standards:

<Foo slot='bar'/>
<div slot='bar'>content</div>
<Bar slot='bar'/>

@sheijne
Copy link

sheijne commented Jun 13, 2020

Another possible syntax is {#slot bar}<Foo/>{/slot}, which would also allow a bunch of DOM nodes and components inside the slot, without them needing to be from a single component

The original syntax allows for multiple DOM nodes to be used while still keeping with standards:

<Foo slot='bar'/>
<div slot='bar'>content</div>
<Bar slot='bar'/>

As of yet this will not work inside blocks like {#each}.

@melMass
Copy link

melMass commented Aug 25, 2020

What is the state of this issue? Is there a workaround yet?

@PierBover
Copy link

An alternative I've found is passing component references as props:

<script>
  import Container from './Container.svelte';
  import Child from './Child.svelte';
<script>

<Container child={Child} childProps={{message: 'Hellow'}}/>

And then on Container.svelte:

<script>
  export let child, childProps;
<script>

<svelte:component this={child} {...childProps}/>

It won't work in all use cases, but it's better than the div soup.

@AlexGalays
Copy link
Contributor

AlexGalays commented Nov 3, 2020

This is (still) one area where the VDOM approaches are so much better. The extra div soup is two fold:

  • A component reserving a slot but needing to perform manipulation on its content dom reference will need to add an extra element wrapper on the receiving site (some people mention it here: A way to see if slot prop is present #2106).

  • A big app will have lots of components compared to regular html elements and these need to be wrapped before being fed to a slot, every single time on the call site (this issue)

A lot of classical components will hit both problems at once (e.g: a Dropdown that needs to measure its content to position itself properly and where the content is to be defined on the call site)

So we either have div soup (and lack of fun at reading it) or just make our components non configurable and copy paste all their code for each usage variation.

The above workaround is not acceptable, especially when using TypeScript; two props for a single thing is awkward and there's no way to prove the passed props type match the component's props at this point.

@PierBover
Copy link

This is (still) one area where the VDOM approaches are so much better.

This is not an issue related to using a virtual DOM. Plenty of non VDOM libraries have solved this. Imba and Solid come to mind.

@AlexGalays
Copy link
Contributor

AlexGalays commented Nov 3, 2020

This is (still) one area where the VDOM approaches are so much better.

This is not an issue related to using a virtual DOM. Plenty of non VDOM libraries have solved this. Imba and Solid come to mind.

Absolutely, but it comes easily and naturally with the VDOM approach whereas it's a special case that must be coded here.
It's just the frustration talking, after having taken this feature for granted.

@Prinzhorn
Copy link
Contributor

Prinzhorn commented Nov 4, 2020

This is (still) one area where the VDOM approaches are so much better.

but it comes easily and naturally with the VDOM approach

React didn't support rendering arrays without a wrapper for most of its existence

16.0.0 (September 26, 2017): Components can now return arrays and strings from render. (Docs coming soon!)

https://github.com/facebook/react/blob/master/CHANGELOG.md#1600-september-26-2017

Edit:

facebook/react#2127

We know this is an issue and we know exactly what set of problem can be solved. We want this too but it is a hard problem with our current architecture. Additional comments expressing desire for this feature are not helpful. Feel free to subscribe to the issue (there's button in the right hand column) but do not comment unless you are adding value to the discussion. "Me too" and "+1" are not valuable, nor are use cases that have already been written in the comments (e.g., we know that you can't put <tr> or <dd> elements with a <div>).

emphasis mine

@ShadowfeindX
Copy link

Sorry I'm new here, but is there a problem with just using another slot instead of a wrapper node workaround?
I've used this in my own projects and it doesn't seem to have any issues with html/css generation or reactivity that I can tell.. I made an example here

@milkbump
Copy link

milkbump commented Nov 24, 2020

Interesting 👀 . That feature (<slot slot="..."/>) was only recently added in #4295. It wasn't primarily intended to be used that way, but I guess it's a good workaround for this issue.

I'm yet to find caveats to slotting components that way, other than it's inconvenient, as opposed to <Component slot="..."/>.

@AlexGalays
Copy link
Contributor

<slot slot=""> seems good enough for the specifying side, it's probably better/more explicit than setting a fake attribute on a dom node/component anyway.

Just need a way to get at the slotted dom node from within a component now!

@ShadowfeindX
Copy link

<slot slot=""> seems good enough for the specifying side, it's probably better/more explicit than setting a fake attribute on a dom node/component anyway.

Just need a way to get at the slotted dom node from within a component now!

What do you mean "get at the slotted dom node"?

@AlexGalays
Copy link
Contributor

<slot slot=""> seems good enough for the specifying side, it's probably better/more explicit than setting a fake attribute on a dom node/component anyway.
Just need a way to get at the slotted dom node from within a component now!

What do you mean "get at the slotted dom node"?

From a component's script that has a , provide an easy way to read the element that was slotted in without any unnecessary wrapper.

@ShadowfeindX
Copy link

<slot slot=""> seems good enough for the specifying side, it's probably better/more explicit than setting a fake attribute on a dom node/component anyway.
Just need a way to get at the slotted dom node from within a component now!

What do you mean "get at the slotted dom node"?

From a component's script that has a , provide an easy way to read the element that was slotted in without any unnecessary wrapper.

I'm not sure I understand the point of what you're trying to do. Components inherently have no root node so there isn't just one "node slotted" that you could possibly reference. <slot slot=""> doesn't create a wrapper around your component in the DOM. It just injects your component as Svelte usually would.

If for some reason you did need access to a top level dom node for your component you'd have to wrap the component yourself and then use something like bind:this={self} and access the DOM node in onMount.

Forgive me if I'm misunderstanding you here.

@AlexGalays
Copy link
Contributor

Just access the slotted node, whether it's the default one or a named one.

Right now, we can only know if a slot was provided (I think the API is a Record<slotName, boolean> but we can't access the DOM references.

@PatrickG
Copy link
Member

Sorry I'm new here, but is there a problem with just using another slot instead of a wrapper node workaround?
I've used this in my own projects and it doesn't seem to have any issues with html/css generation or reactivity that I can tell.. I made an example here

I don't think this is intended to work 😅
But good to know, until #4556 will be merged eventually.

@jacwright
Copy link
Contributor

Just access the slotted node, whether it's the default one or a named one.

Right now, we can only know if a slot was provided (I think the API is a Record<slotName, boolean> but we can't access the DOM references.

Grabbing the DOM references would be challenging. E.g. the following may or may not have elements and may have multiple nodes when it does depending on the contents of AnotherComponent.

<Component>
  {#if somevalue}
    <div>my slotted info</div>
    <AnotherComponent/>
  {/if}
</Component>

If Component puts the slot inside an #each, that additionally complicates the issue.

We have let for passing data into the slot. Perhaps we could have something to pass data back to the parent?

<Component let:value give:node={myNode}>
  {#if value}
    <div bind:instance={myNode}>my slotted info</div>
    <AnotherComponent/>
  {/if}
</Component>

You couldn't use bind:this since that would bind it in the scope of the containing component, but maybe a special bind:instance for this case could be used.

It seems like a lot of added API in the framework would be needed to meet the desire to access slot contents.

@antony
Copy link
Member

antony commented Dec 1, 2020

I also want to add to @jacwright 's comment to say that if you're trying to access DOM elements like this, it's hightly likely that you're using Svelte in a way which isn't a good fit for it, and your mental model of how a Svelte application works is incorrect. Svelte is a reactive UI language. It allows you to easily create things which react to state changes automatically.

In Svelte, you manipulate a model of data, and Svelte handles the required DOM manipulation to reflect that state. If you edit the DOM, you are doing Svelte's job.

If you want to modify a DOM element, you should do so by updating the state. You can do this for slotted content by declaring a context at the topmost level of your subtree, and changing elements within that context. Your slotted content can then read that content and react to changes which happen there.

Often when an API appears "missing" from Svelte, it's actually instead absent, and often for a very good reason.

@AlexGalays
Copy link
Contributor

I also want to add to @jacwright 's comment to say that if you're trying to access DOM elements like this, it's hightly likely that you're using Svelte in a way which isn't a good fit for it, and your mental model of how a Svelte application works is incorrect. Svelte is a reactive UI language. It allows you to easily create things which react to state changes automatically.

In Svelte, you manipulate a model of data, and Svelte handles the required DOM manipulation to reflect that state. If you edit the DOM, you are doing Svelte's job.

If you want to modify a DOM element, you should do so by updating the state. You can do this for slotted content by declaring a context at the topmost level of your subtree, and changing elements within that context. Your slotted content can then read that content and react to changes which happen there.

Often when an API appears "missing" from Svelte, it's actually instead absent, and often for a very good reason.

While the previous comment was spot on, your point is a harder sell. Getting a DOM ref is pretty standard and svelte has an API for it (bind:this). I'm only saying it should ideally be available for slotted elements as well.

@antony
Copy link
Member

antony commented Dec 1, 2020

While the previous comment was spot on, your point is a harder sell. Getting a DOM ref is pretty standard and svelte has an API for it (bind:this). I'm only saying it should ideally be available for slotted elements as well.

It's there because it's possible at compile time since the presence of it is guaranteed. Since slotted content is not guaranteed, it would be a runtime concern.

@AlexGalays
Copy link
Contributor

AlexGalays commented Dec 1, 2020

While the previous comment was spot on, your point is a harder sell. Getting a DOM ref is pretty standard and svelte has an API for it (bind:this). I'm only saying it should ideally be available for slotted elements as well.

It's there because it's possible at compile time since the presence of it is guaranteed. Since slotted content is not guaranteed, it would be a runtime concern.

No, it's not any more guaranteed than a slot's content.

https://svelte.dev/repl/07012a40613f49709479088be103681a?version=3.30.1

Let's not pretend it's missing for good practices reasons. Convoluted api? sure, maybe!

@antony
Copy link
Member

antony commented Dec 3, 2020

No, it's not any more guaranteed than a slot's content.

https://svelte.dev/repl/07012a40613f49709479088be103681a?version=3.30.1

Let's not pretend it's missing for good practices reasons. Convoluted api? sure, maybe!

I'm not really enjoying your tone. An if statement which is either true or false based on a condition is very different from slotted content where the contents can be completely calculated based on a runtime condition such as data being fetched or an async import.

@PatrickG
Copy link
Member

PatrickG commented Dec 3, 2020

Just access the slotted node, whether it's the default one or a named one.
Right now, we can only know if a slot was provided (I think the API is a Record<slotName, boolean> but we can't access the DOM references.

Grabbing the DOM references would be challenging. E.g. the following may or may not have elements and may have multiple nodes when it does depending on the contents of AnotherComponent.

<Component>
  {#if somevalue}
    <div>my slotted info</div>
    <AnotherComponent/>
  {/if}
</Component>

If Component puts the slot inside an #each, that additionally complicates the issue.

We have let for passing data into the slot. Perhaps we could have something to pass data back to the parent?

<Component let:value give:node={myNode}>
  {#if value}
    <div bind:instance={myNode}>my slotted info</div>
    <AnotherComponent/>
  {/if}
</Component>

You couldn't use bind:this since that would bind it in the scope of the containing component, but maybe a special bind:instance for this case could be used.

It seems like a lot of added API in the framework would be needed to meet the desire to access slot contents.

You can do this already, kinda
https://svelte.dev/repl/69d03236b3174957adb7434ba47eaf39?version=3.30.1

@AlexGalays
Copy link
Contributor

AlexGalays commented Dec 4, 2020

No, it's not any more guaranteed than a slot's content.
https://svelte.dev/repl/07012a40613f49709479088be103681a?version=3.30.1
Let's not pretend it's missing for good practices reasons. Convoluted api? sure, maybe!

I'm not really enjoying your tone. An if statement which is either true or false based on a condition is very different from slotted content where the contents can be completely calculated based on a runtime condition such as data being fetched or an async import.

Nor do I like yours (you started insinuating that if I need this feature it's because I don't use svelte properly which is very ridiculous) but I will leave it at that; it's fine if you don't get it as far as I'm concerned. Please stop replying to me.

<Component>
  {#if somevalue}
    <div>my slotted info</div>
    <AnotherComponent/>
  {/if}

You can do this already, kinda
https://svelte.dev/repl/69d03236b3174957adb7434ba47eaf39?version=3.30.1

Ohh, didn't know register, that's good enough for the occasional cases when we need it. Is that API documented? It looks a bit like actions but not quite.
Edit: Ahh ok, it's a regular let. It's too bad it has to be so intrusive on the call sites.

I think the best we have right now is go from a div-soup to a well-named-elements-soup :D (https://github.com/sveltejs/svelte-virtual-list/blob/master/VirtualList.svelte#L165)

@jacwright
Copy link
Contributor

@AlexGalays register is an action created and passed in from the parent node (Wrapper) which allows the child to register with it. Not builtin to svelte.

That's very clever @PatrickG. Nice one. I was a bit confused when first looking at it to understand what was going on, but I think that will be a handy tool in the toolbox.

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

Successfully merging a pull request may close this issue.