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.
Maybe one way is to make internal gubbins Nonenumerable instead of remove them?
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>
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?
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.
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())
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.
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}
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.
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}
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.
I really like this variant:
<slot bind:this={slotEl}></slot>
@Rich-Harris What do you think? Is it possible?
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
}
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.
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>
@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.
@pngwn oh perfect! Thanks!
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>
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>
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}
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:
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.
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.
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).
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?
The magic global $$slots (an object of booleans) is available in 3.25.0.
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.
@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
@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}
@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?
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.
Most helpful comment
I really like this variant:
@Rich-Harris What do you think? Is it possible?