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

A way to see if slot prop is present #2106

Closed
cvlab opened this issue Feb 19, 2019 · 37 comments
Closed

A way to see if slot prop is present #2106

cvlab opened this issue Feb 19, 2019 · 37 comments

Comments

@cvlab
Copy link
Contributor

cvlab commented Feb 19, 2019

v3.0.0-beta.5

Before #2083 it was possible to check if slot prop is present

<svelte:options bind:props/>
<div>
	{#if props.$$slot_icon}
		<div class="Input-Icon">
			<slot name="icon"></slot>
		</div>
	{/if}
</div>

An option to see if a slot prop is preset would be nice to have.

@cvlab
Copy link
Contributor Author

cvlab commented Feb 19, 2019

Maybe one way is to make internal gubbins Nonenumerable instead of remove them?

@cvlab
Copy link
Contributor Author

cvlab commented Feb 19, 2019

There are a workaround.

<div class:with-icon="{ hasIcon }">
	{#if hasIcon}
		<div class="Input-Icon">
			<slot name="icon"></slot>
		</div>
	{/if}
</div>

<script>
	const hasIcon = arguments[1].$$slot_icon
</script>

@Rich-Harris
Copy link
Member

If we decide to expose this, it's something that should have a proper API rather than relying on $$-prefixed variables, since those could change at any time. A binding on <svelte:options> is the most likely candidate.

Do other frameworks expose that information? Any ideas how, if so?

@halfnelson
Copy link
Contributor

Vue does this: https://vuejs.org/v2/api/#vm-slots

Each named slot has its own corresponding property (e.g. the contents of slot="foo" will be found at vm.$slots.foo). The default property contains any nodes not included in a named slot.

@halfnelson
Copy link
Contributor

WebComponents give access via the SlotElement
https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement

let slots = this.shadowRoot.querySelectorAll('slot');
console.log(slots[0].assignedNodes())

@Conduitry
Copy link
Member

Since <svelte:options bind:props> has been removed in favor of $$props, having some sort of $$-prefixed magic global (that's part of the official API) makes more sense to me as a way to expose this.

@evanleck
Copy link

Maybe $$slots like $$props? My use case is that I'd like to wrap a slot's content in an element that applies styling that I'd like absent without the slotted content. Something like this:

{#if $$slots.description}
  <div class="description">
    <slot name="description"></slot>
  </div>
{/if}

@loilo
Copy link

loilo commented May 13, 2019

Another possible way would be to allow binding slots to variables with bind:this.

In reference to the example in the previous comment, it would look like this:

{#if slottedElement}
	<div class="description">
		<slot bind:this="slottedElement" name="description"></slot>
	</div>
{/if}

<script>
let slottedElement
</script>

It would remove the need for a separate API and be reactive without further efforts (if reactivity is something that applies to slots in any way, not sure about that). Also not sure though if it would add too large of an inconsistency since slots cannot have directives otherwise.

@hlobil
Copy link

hlobil commented Jun 11, 2019

I believe this feature already exists ... example https://github.com/kaisermann/svelte-loadable

<script>
const SLOTS = $$props.$$slots
 </script>

{#if SLOTS.success}
  <slot name="success" {component} />
{:else}
  <svelte:component this={component} />
{/if}

@Conduitry
Copy link
Member

Hm I'd tend to think that the presence of $$slots in $$props would actually be a bug, and that it should be removed similar to how $$scope was removed in #2618.

@PaulMaly
Copy link
Contributor

PaulMaly commented Jun 14, 2019

I really like this variant:

<slot bind:this={slotEl}></slot>

@Rich-Harris What do you think? Is it possible?

@rodryquintero
Copy link

Any updates regarding this request? It would be very useful to be able to iterate over <slots /> and modify their props accordingly.

I would like to be able to do the following:

const newSlots = $$slots.map( slot => {
  slot.props.active = true;
  slot.props.onClick = onClick
  return slot
}

@samuelgozi
Copy link
Contributor

This can also be useful when trying to replicate an API similar to the one of the textarea element.
Since you can't write <slot /> inside the text area(it will be converted to text), then if you would like to replicate it, for example to do something like this:

<myTextarea>{text_in_var}</myTextarea>

myTextarea.svelte file content:

<textarea class="mystyles"><slot /></textarea>

Here <slot /> will be converted to text, and there is no other way of accessing the content of the slot, so this is simply not possible right now.

I think that maybe instead of using the prefixed $$ globals, a more "natural" solution could look something like this:

import { slots, props, parent } from '@component';

Svelte will look for imports of @component or any other path or name that doesn't really exist(for example svelte/component), and then do some kind of dependency injection.

@ryanking1809
Copy link

This, or being able to pass props to slot components, would be super useful for a pixi.js wrapper I've been trying to build. Each child component needs to know what canvas to render to. As far as I can tell, the only way to do this is to pass the canvas to each child component, it's a little inconvenient.

<PixiCanvas let:canvas={canvas}>
    <PixiElement pixiCanvas={canvas} />
    <PixiElement pixiCanvas={canvas}  />
    <PixiElement pixiCanvas={canvas}  />
</PixiCanvas>

Ideally, the syntax will look something like this

<PixiCanvas>
    <PixiElement />
    <PixiElement />
    <PixiElement />
</PixiCanvas>

With PixiCanvas.svelte adding a reference to the canvas for each child to render to

<script>
    let canvas = createPixiCanvas()
    onMount(() => renderPixiToCanvas(pixiCanvas))
    onDestroy(() => destroyPixi(pixiCanvas))
</script>
<canvas>
    <slot pixiCanvas={canvas}></slot>
</canvas>

And all PixiElement.svelte elements rendering to the canvas

<script>
    let pixiCanvas
    let pixiElement = createPixiElement()
    onMount(() => pixiCanvas.addElement(pixiElement))
    onDestroy(() => pixiCanvas.destroyElement(pixiElement))
</script>

@pngwn
Copy link
Member

pngwn commented Sep 19, 2019

@ryanking1809 You could achieve the above (assuming I understand it correctly) using the context API. Something like in the Mapbox example should work fine for this case.

@ryanking1809
Copy link

@pngwn oh perfect! Thanks!

@voscausa
Copy link

voscausa commented Oct 1, 2019

Yes very interesting. I used to solve this with contenteditable shown below.
Result (showing both solutions) in the console:

NavPick.svelte:9 slot textContent 2019 
NavPick.svelte:12 slot container <div class=​"hide svelte-19o8tde">​2019​</div>​

The <div bind:this={..} ...> makes it very easy to get and update the slot. In this example NavPick is a year picker: a dropdown menu in a navbar for selecting and updating a year from a year list.

Nav.svelte:

....
<NavBar ....>
     <NavPick list={yearList}>{yearNow}</NavPick>
<NavBar>

And in NavPick.svelte:

<script>
  export let list = [];

  let text;
  let slot;
  let hide = false;
  
  $: if (text) {
    console.log('slot textContent', text);
  }
  $: if (slot) {
    console.log('slot container', slot);
  }

  function collapse(event) {
    hide = true;
    text = event.target.textContent;
    setTimeout(() => {hide = false}, 250);
  };
</script>
 
<li class="nav-pick">
  <button class="pick-btn"> {text || ''} <i class="fa fa-caret-down"/>
    <div class="pick-items" class:hide>
    {#each list as item}
      <a href="javascript:;" on:click="{collapse}">{item}</a>
    {/each} 
  </button>
  <div contenteditable="true" bind:textContent={text} class="hide"><slot/></div>
  <div bind:this={slot} class="hide"><slot/></div>
</li> 

@voscausa
Copy link

voscausa commented Oct 1, 2019

Now with <div bind:this={..} ...> NavPick.svelte becomes:

<script>
  export let list = [];

  let slotObj;
  let hide = false;
  
  function collapse(event) {
    hide = true;
    slotObj.textContent = event.target.textContent;
    setTimeout(() => {hide = false}, 250);
  };
</script>
 
<li class="nav-pick">
  <button class="pick-btn"> <span bind:this={slotObj}><slot/></span> <i class="fa fa-caret-down"/>
    <div class="pick-items" class:hide>
    {#each list as item}
      <a href="javascript:;" on:click="{collapse}">{item}</a>
    {/each} 
  </button>
</li> 

@today-
Copy link

today- commented Nov 26, 2019

Slot element doesn't support bind:this, but it's fallback child does.

This works:

<script>
  let fallbackElement;
</script>

<slot>
  <div bind:this={fallbackElement}/>
</slot>

{#if !fallbackElement}
  HAS SLOT
{/if}

@samuelgozi
Copy link
Contributor

samuelgozi commented Dec 10, 2019

Hi, I wanted to add my 2 cents on this one.

I think that $$slots or $$props.$$slots should be exposed mainly because there currently isn't a way of accessing that data at all, and it's basic functionality when building components.
I think that some access is better than none, even if it just an experimental/temporary solution.

One big use case for this one is for example "portals" which I've been trying to build for a while using some hacks... A "portal" is a pattern of passing the data inside the slots to another component, to be rendered elsewhere.

This pattern of using portals is very common when building mobile apps because if you take a look at native mobile apps, the headers of the apps change based on context.

Here is an illustration of how it works conceptually:

<script>
    import Portal from './Portal.svelte';

    function submitForm() {

    }
</script>

<form>
    stuff here...
<form>

<Portal to="header">
    <button on:click={submitForm}>Save<button>
</Portal>

It is currently impossible to something like this in Svelte(without some very hacky stuff like rendering twice and passing HTML elements instead of render functions etc) while it's completely possible in any other UI framework without any hacks.

There are two things that are necessary in order to achieve such a thing:

  1. Access to props raw data(this is currently possible, but it's certainly a bug).
  2. A way of mutating components props(to replace with data our own), or away or rendering using the "raw" render functions from inside a component.

It is a shame that it's not possible because it's an important feature for the app im building, and after leaving Vue for Svelte, now I have to switch back to Vue and rebuild an entire app just because of this missing feature...

Edit: I just found out that I already commented on this previously, it just goes to show how long I'm waiting for this feature.

@abastardi
Copy link

abastardi commented Dec 16, 2019

Would be great if a component could not only access its own slot content but also manipulate how it is displayed in the template. For example:

Panel.svelte:

<script>
import Wrapper from './Wrapper.svelte'
</script>

<div class="panel">
  {#each $$slots.default as item}
    <Wrapper>{item}</Wrapper>
  {/each}
</div>

The Panel component would be used like this:

<Panel>
  <Component />
  <ComponentWithSlot>
    <div>Hello</div>
    <NestedComponent />
  </ComponentWithSlot>
  <div class="whatever">
    This is a DOM node
  </div>
</Panel>

Notice there is no <slot /> in Panel.svelte. Rather, the slot content is accessed via $$slots.default, which is assumed to be an array of top-level nodes (either DOM nodes or other components) from the slot content provided in the parent. This is similar to how Vue enables access to slot content.

@abastardi
Copy link

abastardi commented Dec 17, 2019

Another use case for programmatic access to slot content is to enable specification of slot content for slots within a nested component. For example, suppose we have a Multiselect component with a couple of slots (with fallback content):

<slot name="label" {label}>
  <span class="label">{label}</span>
</slot>

<slot name="option" {option}>
  <div class="option">{option}</div>
</slot>

Now suppose we want to create a BaseInput component that contains Multiselect as a conditional nested child, and we want to expose the Multiselect slots within BaseInput without having to repeat the slots definitions (including the fallback content). In Vue, this would be achieved in BaseInput as follows:

<multiselect v-if="someCondition>
  <template v-for="slotName in Object.keys($scopedSlots)" v-slot[slotName]="props">
    <slot v-bind="props" :name="slotName" />
  </template>
</multiselect>

In Vue, the above allows the following:

<base-input type="multiselect" :options="options">
    <template v-slot:option="{ option }">
      <div class="custom-option">{{ option }}</div>
    </template>
</base-input>

Via access to $scopedSlots, BaseInput can programmatically re-create all the scoped slots from Multiselect (well, really, it just assumes whatever named slots come from the parent also exist in Multiselect). Any slot not in $scopedSlots is not created, so the fallback content in Multiselect does not get overwritten.

This is an especially important feature when the nested component comes from an external library, as the only alternative is to manually re-create the slot from the nested component, including any fallback content (which you would then need to keep in sync with any updates to the external library).

@tanhauhau tanhauhau added the slot label Mar 13, 2020
@shirotech
Copy link

shirotech commented Mar 13, 2020

I really like this variant:

<slot bind:this={slotEl}></slot>

Would be awesome if we can have this.

It isn’t always guaranteed to be a single root element, in that case it could return an array of elements?

@Conduitry
Copy link
Member

The magic global $$slots (an object of booleans) is available in 3.25.0.

@janosh
Copy link
Contributor

janosh commented Oct 11, 2020

Unfortunately, $$slots doesn't give you a handle on the actual components passed in. It just tells you which named and/or default slots are populated. My use case is a masonry component that should split up its children into several columns. In React, you just grab the children and do something like

const fillCols = (children, cols) => {
  children.forEach((child, i) => cols[i % cols.length].push(child))
}

In Svelte, this seems to be non-trivial.

@wickning1
Copy link

@janosh
The best solution that I found while trying to build a masonry component was to package up a pair of components and place child components inside a wrapper - I chose CardLayout and Card such that users would write something like:

<CardLayout>
  <Card><MyBeautifulCard /></Card>
  <Card><AnotherCard /></Card>
</CardLayout>

The CardLayout creates a store in context and the Card creates a standardized div container and registers it to the store so that the CardLayout has access to that DOM element. Then in afterUpdate you can move the DOM elements into columns and Svelte will not try to put them back where they go. It's a bit messy but it works.

You can see mine at https://github.com/wickning1/svelte-components/blob/master/src/CardLayout.svelte

Moving DOM elements around made me anxious and I wanted to preserve natural tab order without resorting to setting tabindex, so I also made a flexbox version that never moves DOM elements around. I think it's the superior solution, at least for the layouts I was going for. https://github.com/wickning1/svelte-components/blob/master/src/FlexCardLayout.svelte

@PaulMaly
Copy link
Contributor

PaulMaly commented Oct 11, 2020

@janosh Hm, React-way is really hacky... When we talking about lists, masonry, or any other table-style components, first of all, we talk about arrays and iteration through them. If you iterate over the children in the Masonry component, somewhere (in parent component I guess) you also iterate over the actual items. Over and over again, in all places you use this component, you perform almost the same iteration twice. Why we should do this? I believe the interface of this kind of components should look like this:

<Masonry {items} {colsNum} let:item>
   <SomeItemComponent>{item}</SomeItemComponent>
</Masonry>

So, we just don't need to iterate through children and do the same work twice in all places we need it. We just delegate repetitive work to the component which should be responsible for that work. In <Masonry> you iterate not over the children nodes/components, but over the actual items and only once.

<Masonry> component implementation could be something like this (very drafty):

<script>
  export let items = [];
  export let colsNum = 3;

  $: cols = items.reduce(...);
</script>

{#each cols as col}
  {#each col as item}
    <slot {item} />
  {/each}
{/each}

@janosh
Copy link
Contributor

janosh commented Oct 16, 2020

@PaulMaly Interesting approach. What's the best way in Svelte to get the height of all children in order to distribute them across the columns in a way that balances their height?

@AlexGalays
Copy link
Contributor

AlexGalays commented Nov 3, 2020

It seems being able to bind:this={slotEl} directly on a slot element is a popular request. I'll add my +1 as adding div wrappers just to get dom references gets old really fast (and is not free, performance wise)

@Nick-Mazuk
Copy link

It seems being able to bind:this={slotEl} directly on a slot element is a popular request. I'll add my +1 as adding div wrappers just to get dom references gets old really fast.

I like this idea.

Though there may be some issues with the new <svelte:fragment>. I'm not sure you can guarantee that there will actually be a DOM element in a slot. Here's an example:

<!-- Component.svelte -->

<script>
  let this
</script>

<slot bind:this />
<!-- App.svelte -->

<Component>
  <svelte:fragment>
    <p>Just some content.</p>
    <p>Just some more content.</p>
  </svelte:fragment>
</Component>

Which DOM element should we bind this to?

@dummdidumm
Copy link
Member

This turned into two feature requests which both are implemented now:

@Florinstruct
Copy link

@dummdidumm Should that first link be https://svelte.dev/docs#template-syntax-slot-$$slots now?

Also, am I understanding this correctly: the only way to check if a slot has content, is if that slot is named?

@dummdidumm
Copy link
Member

Yes that first link should be that now. The default slot is present as $$slots.default

@TristanBrotherton
Copy link

@dummdidumm I apologize if I am not understanding correctly, but i thought that this issue was for a way to cleanly access the contents of a slot when provided. I'm still not seeing a way to do that, other than something kludgy like a hidden element: Repl

<script lang='ts'>
    let slotObj;
    let slotContents;

    $: if (slotObj) {
        slotContents = slotObj.textContent;
    }
</script>

<span class="hidden" bind:this={slotObj}><slot /></span>
<br/>
I am the slot contents: {slotContents}

@tborychowski
Copy link

tborychowski commented Jun 27, 2023

@TristanBrotherton
Same here.
Cannot reliably make the component render html based on slot contents presence.
What's more $$slots.default would be true even if my component has no text, e.g.:

<Div>{noText}</Div> -- will have $$slots.default = true
<Div />             -- will have $$slots.default = false

@gregg-cbs
Copy link

gregg-cbs commented Feb 15, 2024

how do you see the slot content?

Cant get it with:

import { get_current_component } from 'svelte/internal'
let component = get_current_component();
console.log(component);

@patricknelson
Copy link

patricknelson commented Apr 9, 2024

Both links in the docs have changed no, so the updated version of @dummdidumm's comment above is:

This turned into two feature requests which both are implemented now:

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