Closed ekawatani closed 3 years ago
Usage of getContext
and setContext
may help to solve your problem. These functions are more powerful than you know.
In this you can use getContext to set the index value of children inside slot without using onMount.
Parent.svelte
let startIndex = -1;
setContext('SetIndex', {
index: () => {
startIndex += 1;
return startIndex;
}
})
Child.svelte
const { index } = getIndex('SetIndex');
value = index();
Now the result would be like:
<Parent>
<Child /> // has value set as 0
<Child /> // has value set as 1
<Child /> // has value set as 2
</Parent>
This is one way to iterate over child components in svelte. But I have no solution for component styles. Hope this helps, this is just an example to help you understand the usage of setContext and getContext.
@TheComputerM is correct. You can see the mapbox example on the site to understand how this could work.
The reason that frameworks like React can do this is because they do computation at runtime, vs Svelte where this work is done at compile time. Iterating over the contents of a slot would require a runtime computation, which is not really what Svelte is about.
Svelte however encourages you to hold state and allow Svelte to render based on that state. This means you have a single source of truth (state) and aren't trying to determine state by inspecting the dom.
I think the modification that needs to be done here is how you're using Svelte, rather than an actual modification to Svelte itself.
Sorry for piggybacking here,
looking for solutions to similar problem, and ending up back here regardless of where I start googling =P
The issue everyone has is that the semantics themselves should be sufficient without bothering the children to "register".
So, Iterating over the contents of a slot would require a runtime computation
.
Maybe then, if possible:
slots
somehow should provide a hook into the runtime (<= displaying my lack of insight here, the point being "just make it work").
As a fan service ;-) Could someone knowledgeably enough provide an example of an "encapsulated" way of solving this while keeping the semantics "pure", and add that as a tutorial? - Regardless of how "hacky" it is =)
Cheers!
Edit: The Mapbox example has children with a specific need from the parent, whereas here the issue is general purpose components like a button or text-inputs, which, say, should be manipulated from the parent container...
Edit2: "Cannot be done"
I'm making a slider component, which should ideally be able to take any arbitrary child, an image, a container, an SVG component, and so on.
This should be in theory followed by other display components: a grid, a masonry layout, and so a few others. All those layout components should be able to receive any arbitrary children, and have each wrapped with the right child wrapper component and receive the right styles and behavior.
In particular, passing an image should automatically trigger the children wrapper to apply CSS filters, and to open a lightbox on click. If alt text is passed, it can be automatically extracted for the lightbox description, and so on.
I've built such components in other frameworks, and the API can be made exceedingly easy for the consumer.
<script>
import Gallery from 'gallery'
</script>
<Gallery>
<img src="blah.jpg"/>
<div>
<h2>This is a slide</h2>
</div>
</Gallery>
The API surface becomes completely nullified, and the consumer can just use the component like a native HTML tag, while all the complexity is entirely abstracted away.
Other simple usages may be, for example, a list of todo items that distributes todos on two or more columns depending on the amount of children; or any wrapper with fixed size children needing to grow proportionally (instead of relying on onMount
to determine children amount).
In React, where the children object is an arbitrary blob, a Children.map
helper is provided to loop over children. Would something like that be achievable in Svelte?
To be clear, I read and understood the arguments about misusing Svelte, and Svelte working differently than React. I feel however that this ease of use, warrants a second look to see if something like that could work with Svelte.
Even if I wanted to do things "the svelte way", I couldn't find any way to fulfill those requirements:
The context solution proposed in this issue allows to count children, if the children are prepared to be counted, so it doesn't answer the requirements posed in the opener post (or mine)
Any update on this? This is very useful when elements need to be moved around in different layouts.
Workarounds:
(Disclaimer: none of the below is tested or working code. I just tried to provide examples with enough detail to not be trivial, but without all the boilerplate necessary.)
Works for simple cases
.slider {
--height: 400px;
width: 100%;
height: var(--height);
overflow: hidden;
}
.slider-pane {
display: flex;
overflow-x: scroll;
position: relative;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
}
.slider-pane > :global(*) {
flex-shrink: 0;
width: 100%;
height: var(--height);
scroll-snap-align: center;
}
<script>
import { onMount, afterUpdate } from "svelte"
let slider;
const countChildren = () => {
let totalWidth = 0;
const navigation = []
for (let i = 0; i < slider.children.length; i++) {
totalWidth += slider.children[i].offsetWidth;
navigation.push({index: i, slide: slider.children[i]}
}
}
// get size of static descendants here
onMount(countChildren);
// get slot size (and static descendants) here
afterUpdate(countChildren);
</script>
<div bind:this={slider}>
<slot />
{#each navigation as dot (i)}
<SliderNavigationDot target={dot.slide}/>
{/dot}
</div>
// slider.svelte
<script>
export let slides;
</script>
<div class="slider">
{#each slides as slide}
<Slide><slot {item} /></Slide>
{/each}
</div>
<script>
const slides = [
{alt: "hello", src: "image1.png" },
{alt: "hello 2", src: "image2.png" },
]
</script>
// App.svelte
<Slider slides={slides} let:slides>
<figure>
<img {src} {alt}>
<figcaption>{alt}</figcaption>
</figure>
</Slider>
<script>
import {Slide, Gallery} from './displayComponents'
</script>
<div>
<Gallery>
{#each items as item}
<Gallery.Item src={item.src}/>
{/each}
</Gallery>
<Slide>
{#each items as item}
<Slide.Item src={item.src}/>
{/each}
</Slide>
</div>
None of those solutions satisfy me personally; nonetheless, the workarounds above are workable and should cover nearly all use cases, barring two (that I can think of):
onMount
. For example: SVG to PNG, PDF renderers, automated CSS testing, etc (that last one can be worked around with a headless browser)Not a diss on Svelte, which I enjoy using a lot. Any framework will necessarily make a choice of trade-offs.
This said, even more than the lack of access to children, I deplore that there isn't an officially endorsed way; no matter how cumbersome, it'd be alright if there was an accepted convention for all wrapper components. I hope the Svelte team decides to implement at least a documentation page to address this common problem.
note: for future readers, if you didn't read the whole thread, be aware that if your children are not arbitrary, the mapbox example, which uses context, should work for you.
Just another vote to add support for this. While the context solution may be helpful for tightly coupled parent-child relationships, iterating over any slot children and inserting additional markup is incredibly useful when building reusable components made for layout.
For example, imagine a <Stack>
component (inspired by the Braid design system) that is solely responsible for dealing with the vertical space between its children with an additional setting to insert visual dividers between them. You want to be able to wrap each child with a div that has some class/styling, and insert divider elements (hr/div/etc) between them if necessary.
Using the component looks like:
<Stack spacing="xl" dividers>
<h1>A title!</h1>
<div>some stuff...</div>
<SomeCustomThing />
</Stack>
and the rendered output is something like
<div class="stack stack--spacing-xl">
<div class="stack__child"><h1>A title!</h1></div>
<hr class="stack__divider" />
<div class="stack__child"><div>some stuff...</div>
<hr class="stack__divider" />
<div class="stack__child"><div class="custom-thing">the custom thing</div></div>
</div>
While there are good workarounds (or better/sveltier ways) for some cases, there are definitely very valid use cases for this kind of functionality. I have found these kinds of components with very simple DX (rather than needing to also use tightly coupled child components) to be extremely useful.
Just another vote to add support for this. While the context solution may be helpful for tightly coupled parent-child relationships, iterating over any slot children and inserting additional markup is incredibly useful when building reusable components made for layout.
For example, imagine a
<Stack>
component (inspired by the Braid design system) that is solely responsible for dealing with the vertical space between its children with an additional setting to insert visual dividers between them. You want to be able to wrap each child with a div that has some class/styling, and insert divider elements (hr/div/etc) between them if necessary.Using the component looks like:
<Stack spacing="xl" dividers> <h1>A title!</h1> <div>some stuff...</div> <SomeCustomThing /> </Stack>
and the rendered output is something like
<div class="stack stack--spacing-xl"> <div class="stack__child"><h1>A title!</h1></div> <hr class="stack__divider" /> <div class="stack__child"><div>some stuff...</div> <hr class="stack__divider" /> <div class="stack__child"><div class="custom-thing">the custom thing</div></div> </div>
While there are good workarounds (or better/sveltier ways) for some cases, there are definitely very valid use cases for this kind of functionality. I have found these kinds of components with very simple DX (rather than needing to also use tightly coupled child components) to be extremely useful.
I am in this exact situation: I have a component that displays cards, and its sole purpose is displaying a title and arranging the cards inside it, choosing which size and position they should have. I am very disappointed to see there is no "logic" way to do this. The "context" workaround would force me to introduce some logic in the cards that is not expected for the card business; I mean, there is no reason for the cards to have this context/index, except that being able to be laid out by another component they do not know/care about :-(
Why can't we just iterate over a slot? Even if it's not possible with a default slot, maybe with a named slot, using $$slots.name
?
Similar issues have been raised and they have been closed, but I'm going to try this again because I believe a lot of people would also love to see this feature.
A svelte component should be able to iterate over child components in
<slot />
.Is your feature request related to a problem? Please describe. I constantly get hit by this problem when I want to create a component used for layout purposes. This type of component is solely for creating a specific layout, so it does not care what child component it has. You can think of a relationship between flex container and flex items.
4455 is the issue that comes up when I google for similar issues, but this one assumes that every child is related to the parent component. The solution proposed in this issue was to pass data of children as an array, but in my case, the child can be anything, it gets cumbersome to achieve this.
Describe the solution you'd like I'd like to do something like this. Also, note that the number of children or what children is rendered can be dynamic.
App.svelte
FluidLayout.svelte
Describe alternatives you've considered Solution 1 I could still pass an array of props to the parent component to achieve this.
This should work, but, I think most people could agree that my proposed solution feels a lot more natural.
But, there's one big problem to this. When using TypeScript, you can't get nice type information because every prop can be a completely different type. You'd need to defined a childProps type listing every child component prop, but this can be dynamic.
Solution 2 I can wrap every child component with another layout component (See below). Well, this doesn't work easily because the CSS stylings child component depends on the parent selector (See how
fluid-layout--wide
is used above to affect the stylings of the child), but component stylings are scoped in Svele. One way to make this work is to make the child selector global, and use that in both parent and child component. But, this makes the code cluttered.e.g.
I'm aware of solutions like this one, but this doesn't really apply here because I'd need to pass a global class name to each child component.
There are other ways to achieve the same effect, but all of them I've seen seem hacky because Svelte is not expect to support this scenario properly.
How important is this feature to you? Simply having the child components wrapped inside a parent maintains good HTML semantics, and having to pass them via props is counter-intuitive, and it can quickly get cumbersome. It also helps hide the implementation details. Other frameworks such as React support this and I think Svelte should, too.
Having stylings scoped by component is a good thing in general, but it gets in the way when we have a composite component that expects certain styling on multiple levels of DOM elements. My proposed solution looks a lot more like standard HTML and, if I'm not mistaken, I believe Svelte is trying to provide that feel to developers.
I use TypeScirpt and there doesn't seem to have a nice way to achieve this currently. I wouldn't say this is a deal breaker if I'm not using TypeScript, but I won't enjoy writing in Svelte as much if this doesn't get supported. Is there a technical challenge to achieve this or Svelte has been consciously avoiding this? If latter, that seems a bit odd to me.