Skip to content

$$slots with conditional/reactive content? #5312

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

Closed
icalvin102 opened this issue Aug 26, 2020 · 10 comments
Closed

$$slots with conditional/reactive content? #5312

icalvin102 opened this issue Aug 26, 2020 · 10 comments
Labels
compiler Changes relating to the compiler feature request

Comments

@icalvin102
Copy link

icalvin102 commented Aug 26, 2020

The new $$slots feature (thanks to @tanhauhau) provides information if a slot is loaded/filled or empty.
However this only works with "static" content. As soon as there is a condition {#if} or {#await} involved the
the output of $$slots is not really useful anymore or in case of a named slot a error is thrown.

In case of an unnamed slot in combination with {#if} the slot is considered filled and $$slots.default results to true
even if the condition passed to the slot is false. Additionally the fallback wont be rendered.

In case of a named slot value inside a {#if} (example below) the line <div slot="a">Content</div> will throw
ValidationError: Element with a slot='...' attribute must be a child of a component or a descendant of a custom element

Example (tested with commit 8adb47401e7f7b420ffabf9752a8236114aaecfc)

// App.svelte

<script>
    import SlotWrapper from './SlotWrapper.svelte';
    let show = false; 
</script>

<SlotWrapper>
    {#if show}
        <div slot="a">Content</div>  <!-- throws error -->
    {/if}
</SlotWrapper>

<button on:click="{()=> show = !show}">
    { show ? 'Hide' : 'Show' }
</button>
// SlotWrapper.svelte

<script>
    $: console.log('slots', $$slots)    
</script>

<slot name="a">Fallback Content</slot>

The behaviors that I would expect, want and need are:

  1. Properties of $$slot are only set to true if the content passed to a slot is renderable.
  2. Conditionally pass content to named slots like in the example above. (but that one is not as important me as the first one)

Why would I want that?

One usecase (besides many others) is implicitly setting the state of a component with a slot.

Example:
I have a Card component that displays data coming from a fetch request. The Card component should have a
loading state while it is fetching and go to its default state if the data is fetched and rendered.
At the moment the only way i konw of is to explicitly create a loading variable, set it to true before fetching, set it to false
afterwards and pass it down to the Card component.
This is okay if you have to do it one or twice but not if you have many components with similar structure.

With a properly reactive $$slots property this could be much cleaner and could look something like this:

// Card.svelte
<script>
  $: loading = !$$slots.default;
</script>

<div class="card" class:loading>
   <slot />
</div>

// App.svelte
// ... script, imports. etc...
<Card>
  {#await fetch(url).then(responseToJSON) then data}
  { data.name }
  {/await}
</Card>

Thats it.
Thanks you for providing such an awesome tool.

@icalvin102 icalvin102 changed the title $$slots with conditional/reactive content $$slots with conditional/reactive content? Sep 8, 2020
@hungtcs
Copy link

hungtcs commented Mar 7, 2021

Hello everyone!

I am writing a component library for mobile phones. This is an important function, is there any temporary solution at the moment?

This is my situation,the leader icon cannot be rendered correctly:

<ListItem>
  {#if item.active }
    <Icon name="active_icon" slot="leading" />
  {/if}
  <h4>{ item.title }</h4>
  <p>{ item.desc }</p>
  <Icon slot="trailing" name="more_vert" />
</ListItem>

Thank you!


I currently convert the dom structure, but there is one more level of nesting:

<div slot="leading">
  {#if item.active }
    <Icon name="active_icon" />
  {/if}
</div>

Update, no extra nesting, but $$slots.leading not working

<svelte:fragment slot="leading">
  {#if item.active }
    <Icon name="active_icon" />
  {/if}
</svelte:fragment>

@ecstrema
Copy link
Contributor

What about `$$slots`` containing the textContent of the slot?

Pros:

  • Ability to check precisely the content of a slot;
  • Ability to be reactive to to the content of a slot.
  • An empty slot can easily be checked with $$slots.slotName === ""

Cons:

  • Since the content of a slot can rapidly grow huge, it would have to be greatly optimized.
  • one-way binding: the $$slots.slotName would only be readable.

@ecstrema
Copy link
Contributor

What about having references to the passed objects in the $$slots variable?. For example, in

<MyComponent>
  <div id="my-div" slot="slot1">Some content</div>
</MyComponent>

$$slots.slot1 would contain the div object. The same as typing document.getElementById("my-div")

@the0neWhoKnocks
Copy link

Just ran into this issue myself. Spent far too much time debugging this only to eventually come across this issue. Doesn't seem this issue is getting much (if any) attention. At the least, the current documentation should be updated to call out that

{#if $$slots.description}
  <!-- This <hr> and slot will render only if a slot named "description" is provided. -->
  <hr>
  <slot name="description"></slot>
{/if}

will only work if the element with slot="description" doesn't contain any logic.

Currently, instead of being able to utilize $$slots.default I have to run some logic in the parent component to determine if there will be any content (result stored in hasContent), and then use that in an inner if and pass that as a prop to the component that has a default slot.

@chanced
Copy link

chanced commented Sep 17, 2021

Here's a simple spike demonstrating the problem: https://svelte.dev/repl/94f5623ac9e54ce4918362492c6e35f1?version=3.42.6

I'm not sure if it would be possible to check the content length of the slot but that would solve my current situation.

This really limits the ability to have fallback content, especially at any sort depth beyond 1.

@Valexr
Copy link

Valexr commented Nov 20, 2021

$$slots not conditionally & don't working in runtime, just 1 check in compiletime...
@tanhauhau - any plans/ideas for rectivity $$slots ?

in malinajs it's solved by #fragment - https://malinajs.github.io/repl/#/share/TJLjJATAvKX?version=0.6.39

@dummdidumm dummdidumm added compiler Changes relating to the compiler feature request labels Nov 20, 2021
@pooledge
Copy link

pooledge commented Jun 1, 2022

I guess, this also has a side effect of a slot fallback not being shown for:

<ComponentWithASlotFallback>{#if false}This prevents slot fallback{/if}</ComponentWithASlotFallback>

@icalvin102
Copy link
Author

icalvin102 commented Jun 11, 2022

Forwarding slots as empty with <slot name="forward" slot="forward" /> is not possible either.

https://svelte.dev/repl/cc6ab0a9bb714232b8b3af335db03792?version=3.48.0

This one of the details about svelte that severely limits the reuseability and composeability of components because the $$slots prop does not provide any usable information with a forwarding depth > 1 as @chanced already mentioned.

@Ahmed-Ali
Copy link

Ahmed-Ali commented Nov 29, 2023

Here is a work around that works fine for me with Svelte 4.
It can get ugly in case of too many levels, but for 3 layers, it should serve the purpose.

Deepest component too-deep.svelte

<script lang="ts">
  export let backgroundEnabled: boolean;
  export let hoverBackgroundEnabled: boolean;
</script>

{#if backgroundEnabled} // we are checking a boolean flag here, not the 
  <div class='entryBackground'>
    <slot name="background" />
  </div>
{/if}

{#if hoverBackgroundEnabled} // same
  <div class='entryHoverBackground'>
    <slot name="hoverBackground" />
  </div>
{/if}

Intermediate component boss.svelte

<script lang="ts">
  import ConditionalContent from './too-deep.svelte'
</script>
<div>
This is the boss div content
<!-- Notice how we map the slot checks to the boolean flags in this layer -->
<ConditionalContent backgroundEnabled={$$slots.background} entryHoverBackground={$$slots.hoverBackground}>
  <slot name="background" slot="background" />
  <slot name="hoverBackground" slot="hoverBackground" />
</ConditionalContent>
</div>

Top level component top-layer.svelte

<script lang="ts">
	import Boos from './boss.svelte'
</script>
<div>

<Boos>
  <svelte:fragment slot="background">
    My background content
  </svelte:fragment>
  <svelte:fragment slot="hoverBackground">
    My hover background content
  </svelte:fragment>
</Boos>

<Boos>
  <svelte:fragment slot="hoverBackground">
    My second hover background content
  </svelte:fragment>
</Boos>

<Boos>
  <svelte:fragment slot="background">
    My second background content
  </svelte:fragment>
</Boos>
</div>

That will give you the result you would expect. For slots that hasn't been provided in the top-layer, they won't be rendered in the too-deep layer

@dummdidumm
Copy link
Member

Closing since $$slots is deprecated in Svelte 5

@dummdidumm dummdidumm closed this as not planned Won't fix, can't repro, duplicate, stale Oct 29, 2024
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
Projects
None yet
Development

No branches or pull requests

9 participants