sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
77.8k stars 4.06k forks source link

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

Closed ghost closed 2 months ago

ghost commented 3 years ago

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.

s0me0ther commented 3 years ago

I would enjoy this feature.

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

ajschmidt8 commented 3 years ago

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

ash0080 commented 3 years ago

+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 commented 3 years ago

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 commented 3 years ago

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 commented 2 years ago

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 commented 2 years ago

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

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

gustavopch commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

`

` (use display depending on your element type, e.g. table-row for table rows etc.)
madeleineostoja commented 2 years ago

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 commented 2 years ago

Is there any progress on getting this over the line? I really don't think that having workarounds is the best way.

whatwhywhenandwho commented 2 years ago

+1

Saibamen commented 2 years ago

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 commented 1 year ago

+1

GeoffCox commented 1 year ago

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 commented 1 year ago

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

N00nDay commented 1 year ago

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

Rolands-Laucis commented 1 year ago

Jup, just ran into this as well. Would love to see this work as the suggested feature. Please add this, Svelte team! 😭💕

LowArmour commented 1 year ago

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

sebmor commented 1 year ago

I add my support for the svelte team to add this feature. It's a common problem that currently needs very elaborate workarounds.

mikerowe81 commented 1 year ago

+1

hinex commented 1 year ago

+1

boian-ivanov commented 1 year ago

It has been more than 2 years that this feature request has been here, with quite a lot of community backing. Is there a timeline for it or is it even something planned to be implemented? I know that there's quite the push to get SvelteKit v1 out, but is there some hope at least of including this in Svelte at any point in the not too distant future @Conduitry @dummdidumm @benmccann ?

x4fingers commented 1 year ago

I'm just another guy who ran into this issue. Please fix it.

HeimMatthias commented 1 year ago

+1

madeleineostoja commented 1 year ago

Please don't spam all subscribers to this issue with +1 or the equivelant

whatwhywhenandwho commented 1 year ago

Please don't spam all subscribers to this issue with +1.

I don't think this is spamming. It's upvoting, as it's a feature we really really need :)

benmccann commented 1 year ago

It is spamming. Please use thumbs up to upvote the issue. I will be hiding your comment as it just adds noise and doesn't help figure out how to implement this

fvjupiter commented 1 year ago

<div hidden={yourCondition} slot="slotName">hide if yourCondition is true else show</div>

This is only working with standard HTML-Elements out of the box. If the child is a custom Component, you could wrap it in a div or modify it to accept a hidden parameter.

The mentioned example would look like this: <MyComponent> <a hidden={something()} slot="right-container">Hi</a> </MyComponent>

🙃

hugo-t-b commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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.

TristanBrotherton commented 1 year ago

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

B-Esmaili commented 1 year ago

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

davidsavoie1 commented 1 year ago

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 commented 1 year ago

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 commented 8 months ago

For anyone having a hard time following up here:

This may be fixed by https://github.com/sveltejs/svelte/pull/8304 as mentioned above but as Svelte 5 changes a lot of stuff, they are waiting with merging it.

stefan-winkler-diconium commented 8 months ago

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 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 commented 8 months ago

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 commented 7 months ago

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 commented 7 months ago

@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 commented 5 months ago

+1

opensas commented 4 months ago

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 commented 3 months ago

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.