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

Feature: Let slots be wrapped in if statements to avoid "must be a child of a component" error. #5604

Closed
ghost opened this issue Oct 28, 2020 · 46 comments
Labels
compiler Changes relating to the compiler feature request popular more than 20 upthumbs
Milestone

Comments

@ghost
Copy link

ghost commented Oct 28, 2020

Is your feature request related to a problem? Please describe.

When I do something like the below, I get an error of:

Element with a slot='...' attribute must be a child of a component or a descendant of a custom element

<MyComponent>
  {#if something()}
    <a slot="right-container">Hi</a>
  {/if}
</MyComponent>

Describe the solution you'd like
I want to be able to do the first example and wrap my optional slot's in if statements. The reason for this is I actually have default slot text in my component that I want to show.

Describe alternatives you've considered
The alternatives is doing something like this which has a lot of duplicate code.

{#if something()}
  <MyComponent>
  {#if something()}
    <a slot="right-container">Hi</a>
  {/if}
  </MyComponent>
{:else}
  <MyComponent />
{/if}

How important is this feature to you?
This is a big hassle for me and it would make for a lot cleaner code if I could wrap this slots in if statements. However, it would not affect my ability to code and use svelte.

@ghost ghost changed the title FeatureL Let slots be wrapped in if statements to avoid "must be a child of a component" error. Feature: Let slots be wrapped in if statements to avoid "must be a child of a component" error. Oct 28, 2020
@s0me0ther
Copy link

s0me0ther commented Feb 16, 2021

I would enjoy this feature.

Is there a workaround for now, to have conditional slots? (Except of copy paste..)

@ajschmidt8
Copy link

+1. this would help make my code less verbose in some cases.

@ash0080
Copy link

ash0080 commented May 17, 2021

+1, Especially useful for slot forwarding!!!
Otherwise there is no way to use the deepest slot fallbacks

https://svelte.dev/repl/7941b94f6c6f42df93aba4d5ef543917?version=3.38.2

@thislooksfun
Copy link

I agree, this is a must-have for complex components. Code duplication is one of the largest sources of bugs, please don't make us do it.

@lukeed
Copy link
Member

lukeed commented Jun 8, 2021

Here's the workaround I'm using:

{#if $$slots.header}
  <header>
    <slot name="header" />
  </header>
{/if}

<style>
  header {
    padding: 16px;
    /* ... */
  }
  header:empty {
    display: none;
  }
</style>

This means that when you have something like this:

<Demo>
  <svelte:fragment slot="header">
    {#if condition}
      <p>HELLO</p>
    {/if}
  </svelte:fragment>
</Demo>

You will only see the <header> – and its padding – when condition is true.

@gmanfredi
Copy link

gmanfredi commented Dec 1, 2021

We just ran into the same thing -- trying to conditionally pass in a slot. Being able to do this makes sense.... In our case, we are trying to conditionally render a slot named "body" within an expandableCard component. If there's no body slot given to expandableCard, just render the header without expander body.

I guess our workaround would be to pass in a Boolean prop to expandableCard called "hasBody", and then conditionally render the expander div that has the <slot name="body">... clunky but doable.

@harshmandan
Copy link

harshmandan commented Dec 10, 2021

Hey. It'd be great to get this as a feature.

Even with <svelte:fragment> the $$slots.name resolves to true

@gustavopch
Copy link

Issues like this one, the lack of a <svelte:element> analogous to <svelte:component>, lack of a simple way to forward all events, impossibility of using actions on components, etc. are some of the rough edges that must be fixed so Svelte can actually be said to be a really mature framework. These things should just work.

@e0
Copy link

e0 commented Mar 6, 2022

Appreciate the CSS tip @lukeed. For anyone using tailwindcss you can do this with the following:

<header class="p-4 empty:p-0">

@zdenda-online
Copy link

Well, I currently use this ugly workaround with CSS display instead of if:

<header slot="header" style="display: {data.header.length > 0 ? 'inline' : 'none'}">

(use display depending on your element type, e.g. table-row for table rows etc.)

@madeleineostoja
Copy link

Also just ran into this, assumed it was a bug rather than a missing feature because it seems like a no brainer. Is there any input from the svelte team on this? Is it due to technical limitations of the compiler? Or do we need an RFC for it? Would love to move it forward.

@boian-ivanov

This comment was marked as off-topic.

@whatwhywhenandwho

This comment was marked as off-topic.

@Saibamen
Copy link

Saibamen commented Jul 26, 2022

This will be really helpful when creating custom component with predefined options and pass fragments to child component.

For example:
I have MyTableComponent with some classes for SvelteTable component. To be able to fully use SvelteTable from my MyTableComponent, I need to pass 3 slots from MyTableComponent into MyTableComponent. Sometimes 0 or only 1 fragment will be overrides.

@bigonha

This comment was marked as off-topic.

@GeoffCox
Copy link
Contributor

I think have a workaround. I created a Svelte component that I named SlotFragment.svelte. It conditionally renders a default slot.

{#if $$slots.default}
    <slot />
{/if}

For example when composing components, I have component A with a header slot. The header slot has some HTML around it that is rendered conditionally on the slot being filled.

I have component B that that renders component A plus some other stuff. Component B provides a header slot that should be forwarded to component A, plus its own footer slot.

This syntax in component B works, but causes component A to always think the slot is filled and it renders the additional HTML.

<slot name="header" slot="header" />

So you try to put an #if around it, but the slot must be filled within a component.

{#if $$slots.header}
  <slot name="header" slot="header" />
{/if}

Then you try to wrap it in a svelte:fragment, but it can't be in the #if either:

{#if $$slots.header}
  <svelte:fragment>
    <slot name="header" slot="header" />
  <svelte:fragment>
{/if}

However, using the SlotFragment component works:

{#if $$slots.header}
  <SlotFragment slot="header">
    <slot name="header" />
  <SlotFragment>
{/if}

@AlbertMarashi
Copy link

Also ran into this issue. Conditional slots would be a very useful feature

@N00nDay
Copy link

N00nDay commented Sep 23, 2022

I ran into this issue yesterday and spun up a repl to reproduce - https://svelte.dev/repl/b089c2c379e9404596445c16311bd1b9?version=3.50.1.

@Rolands-Laucis

This comment was marked as off-topic.

@LowArmour
Copy link

Please add this feature, it is deeply important for us to fully use the slot concept. I had to waive the slot feature and fully integrate the child component into the parent one, because of this, which goes against the concept of extracting logic into components.

Now I have to duplicate code in case I would need a similar child component in the future. Which unfortunately is bad practice.

In my case the if and each tag are generating the error.

Thanks

@hugo-t-b
Copy link

hugo-t-b commented Dec 21, 2022

This limitation is particularly annoying when using transitions, since the simple workaround is to hide the element when the slot shouldn't be rendered, rather than removing it from the DOM entirely. Could a key block work for this?

@uranderu
Copy link

uranderu commented Jan 2, 2023

At the very least add an error message or something. I have just been debugging for two hours questioning everything I knew about Svelte bindings because I was trying to conditionally insert a component into a slot.

For people who are in a similar situation. You can wrap you component in a div and assign the slot there.
So while this unfortunately currently doesn't work:

	{#if errorVisibility}
		<ErrorMessage slot="errorMessage" value="Incorrect username or password" />
	{/if}

This does:

<div slot="errorMessage">
	{#if errorVisibility}
		<ErrorMessage value="Incorrect username or password" />
	{/if}
</div>

As you'll probably reckon it is a band-aid solution because empty divs are far from ideal.

@percybolmer
Copy link

percybolmer commented Jan 29, 2023

@uranderu thanks for that tip, I just came across this issue when trying to do the same.
This solution does however not work with Fallback in the slots since $$slots.NAME will be true.

So far I've been super impressed with Svelte since its amazing, this is the first trouble I run into tbh..
Conditional slots Would be amazing

As a work around for making Fallbacks work I added an else to pass In the EM , But id rather have the em in the child comp tbh..

		<div slot="content">
			{#if post.content !== undefined}
				<textarea>{post.content}</textarea>
			{:else}
				<em>No post content was found</em>
			{/if}
		</div>

@Not-Jayden
Copy link
Contributor

Not-Jayden commented Feb 6, 2023

For those who are running into this issue and require the element to actually be removed from the DOM rather than just hidden with CSS or wrapped in a slotted parent element, I came up with this action as a workaround.

function hideOrRemove({
	node,
	parent,
	nextSibling,
	shouldHide,
}: {
	node: HTMLElement;
	parent: HTMLElement | null;
	nextSibling: ChildNode | null;
	shouldHide: boolean;
}) {
	if (shouldHide) {
		node.remove();
		return;
	}

	if (parent?.contains(nextSibling)) {
		parent.insertBefore(node, nextSibling);
		return;
	}

	parent?.appendChild(node);
}

export function hideElement(node: HTMLElement, condition: boolean) {
	const nodeParent = node.parentElement;
	const nodeNextSibling = node.nextSibling;

	hideOrRemove({
		node,
		parent: nodeParent,
		nextSibling: nodeNextSibling,
		shouldHide: condition,
	});

	return {
		update(newCondition: boolean) {
			hideOrRemove({
				node,
				parent: nodeParent,
				nextSibling: nodeNextSibling,
				shouldHide: newCondition,
			});
		},
	};
}

Which you can then use like so:

<div slot="header" use:hideElement={$shouldShowHeader} />

I would caution to consider this a dangerous approach and to use this at your own risk, as I've only used this for one specific use case, and have not given it much testing or investigation into how it might interact with svelte internals.

rouilj pushed a commit to rouilj/command-pal that referenced this issue Feb 8, 2023
New CommandPal option footerText. If set to null (default) no
footer is displayed. If set to a string the string is displayed.

The way I handle conditionally showing the footer is kind of ugly.
Svelte doesn't have conditional slots. Once:

    sveltejs/svelte#5604

is implemented, the whole mess can be cleanly rewritten.

I would have like to use :where(.footer) in the css for styling. But
there are a lot of mobile browsers on caniuse that have unknown status
even 2 years after their release.

Using :where would allow ".footer" in user's css to work. As it is
you have to make ".footer" more specific using e.g. "#id .footer" or
".footer[slot=footer]".

README.md, and cp-advanced files updated to show/use.
@TristanBrotherton
Copy link

This requires ugly hacks to work around when using slot forwarding to child components. Its a greatly needed feature.

@tanhauhau tanhauhau mentioned this issue Feb 23, 2023
5 tasks
@B-Esmaili
Copy link

A feature with this level of importance should have given more attention than this IMHO.

@davidsavoie1
Copy link

While this issue still exists, I do have a workaround to propose. Conditionnal slots work fine with default slots, but not with named ones. What I do is just pass an extra prop to the components declaring the named slot that will enable/disable its display from within. It's not as elegant, but it works fine. Here's a REPL representing the use case : https://svelte.dev/repl/dda911c0804c43bd8d6d035ed0660e22?version=3.58.0

@alexamy
Copy link

alexamy commented Jun 23, 2023

This feature is required. My use case is:
I am making a game, and level has a tutorial with pause / overlay / help messages. One of messages must be in place of game element (score board) to explain it.
My solution is to create Tutorial component, which renders Level component, and to wrap elements from level in slots with fallbacks, and providing tutorial messages as content for this slots.
But there is a problem, I want at some point to see element from a game (fallback content), not a message from a tutorial, so I guess if-slot content or null in slot content will be sufficient to show a fallback, but it isn't.

My current solution to add extra prop with visibility flags, and show fallback if flag is true, and slot if flag is false. It is not a declarative way to solve this problem, sadly.

@benjaminpreiss
Copy link

For anyone having a hard time following up here:

This may be fixed by #8304 as mentioned above but as Svelte 5 changes a lot of stuff, they are waiting with merging it.

@stefan-winkler-diconium
Copy link

For anyone having a hard time following up here:

This may be fixed by #8304 as mentioned above but as Svelte 5 changes a lot of stuff, they are waiting with merging it.

That is very good news indeed. Any info if this will also address the related issue that a svelte:fragment also triggers the "must be direct child" error message within conditional or loop statements?

Also, has anyone any information what is going on with #8535? Dynamic slot names are also sorely missing.
If all three of these issues were addressed when Svelte5 releases, I would finally be able to do this:

<svelte:component this={importedComponent} {...componentProps}>
    {#if componentSlots && componentSlots.length}
        {#each componentSlots as compSlot}
            <svelte:fragment slot={compSlot.name}>
                {#if compSlot.currentContent.type  === 'text' || (compSlot.currentContent.type === 'html' && !compSlot.currentContent.tag)}
                    {@html compSlot.currentContent.content}
                {:else if compSlot.currentContent.type  === 'html' && compSlot.currentContent.tag}
                    <svelte:element this={compSlot.currentContent.tag}>
                      {@html compSlot.currentContent.content}
                    </svelte:element>
                {:else if compSlot.currentContent.type  === 'component'}
                    <svelte:component this={compSlot.currentContent.content} {...compSlot.currentContent.props} />
                {/if}
            </svelte:fragment>
        {/each}
    {/if}
</svelte:component>

@dummdidumm
Copy link
Member

dummdidumm commented Nov 13, 2023

This will be more ergonomic in Svelte 5 using the new snippets feature: preview playground link. Slots will be deprecated, as such this feature won't be implemented in slots, but as shown in the preview playground it's easily achievable using snippets.

@igalil
Copy link

igalil commented Dec 13, 2023

Hi,
I ended up not checking if the $$slots.header is true, and moved the logic to a prop like hasHeader.

Demo.svelte

export hasHeader: boolean = false;

{#if hasHeader} <------- instead of $$slot.header
  <header>
    <slot name="header" />
  </header>
{/if}

This means that when you have something like this:

<Demo hasHeader={condition}>
  <svelte:fragment slot="header">
    {#if condition}
      <p>HELLO</p>
    {/if}
  </svelte:fragment>
</Demo>

Hope it helps

@Not-Jayden
Copy link
Contributor

@dummdidumm Is there any outlook of whether this syntax will end up being supported?

It's cool that snippets enable it to be achieved via props, but being able to handle it inside the component body would be nice as well.

@dneubauer-iteratec
Copy link

+1

@opensas
Copy link
Contributor

opensas commented Mar 19, 2024

I'm adding here my workaround, in case anybody find it useful

I'm trying to conditionally forward a slot (meaning, if the slot hasn't been specified, I don't want to forward any slot at all)
But as you already know

  • can't put a named slot inside an {#if $$slots.named_slot}....
  • if you call <slot slot="named_slot" name="named_slot /> it will ALWAYS pass a slot, even if it hasn't been specified

image

the work around I found (ugly in deed but it seems to work and to be general enough) is to allow to receive a slots prop to override the child's $$slot variable, like this:

like this:

<!-- Title.svelte -->
<script>
	export let slots = $$slots // allow to override $$slots
</script>

<div>
	{#if slots.title}
		<h3><slot name="title" /></h3>
	{/if}
	<slot />
</div>

And this is how I use it to define an Input.svelte component that may optionally receive a title slot that I want to forward to Title.svelte

<!-- Title.svelte -->
<script>
	import Title from './Title.svelte'
	export let name = ''
</script>

<!-- override $$slots -->
<Title slots={$$slots}>
	<slot slot="title" name="title" />
	<input {name} placeholder={name} />
</Title>

In this case the name of the slot (title) is the same in the child (Title) and parent (Input) component, if they differ we could map them like this:

<!-- if the slot name in the child component is not the same as the parent, I can map them like this: -->
<Title slots={{child_slot: $$slots.parent_slot}}>

It's clunky, but I hope this one gets merged soon so I can get rid of this ugly workaround

here's a working repl: https://svelte.dev/repl/3c82d89f3b564bce82760aba3f5c9b44?version=4.2.12

@apokaliptis
Copy link

apokaliptis commented Apr 20, 2024

Based on the closing of #6059 and #8304, this issue should be closed as well, since the Svelte team has already decided not to move forwarded with adding this or any other feature to slots since they will be deprecated in favor of snippets, which provide this functionality and so much more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler Changes relating to the compiler feature request popular more than 20 upthumbs
Projects
None yet
Development

Successfully merging a pull request may close this issue.