-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Passing values from slot to parent #3617
Comments
Hi, The part of the documentation you mention doesn't say what you interpreted. Binding values is different from exposing a property from the slot to the parent. It will be helpful to said what are slot are and use for. Slots are just placeholder:
The slot tag will be "replace" with the html markup that you put inside your Component tags. A use case can be... Let's said you want to create a DataTable component, on the DataTable component you create all your table html markup, style and javascript. So to use the component i just need to pass a dataset, where this dataset is an array of objects. How you display each one of those items and in which order? You don't know, because when you created the component you din't have the data and you what to use this component for different type of objects. So instead of creating complex configuration for the component you just expose each object (item) of the dataset to the parent. So here you will use a slot, so future developers and you can said how the markup of a row will be, and with the expose item you have the data of your dataset that you pass to your component available. So svelte documentation saids:
Continuing the documentation after the previous line it saids:
This means that to expose a property from the slot child to the parent you need to do:
And to use that property on the parent you need to do:
Check the svelte example on this part of the documentation for better understading. If you don't do that, the item on the parent will be undefined. Thus not having access to the data you which. On the other hand... Binding In a nutshell, the purpose of this is to update the variable programmatically or when the DOM element/components updates it. So if you have an input that his value is bind to a variable X when the input change it will change the value of the variable X but also if you change the value of X programmatically it will also change on the input and any place you reference the variable X. From svelte tutorials we can bind to components properties too:
But to components props, and to create a prop you need to use the export keyword. More on component bidings. The extended explanation is in case you miss something and for future devs that find this issue, have a better understanding of slot and binding, and more if they're beginners to Svelte. From your REPL I edit it a little, maybe this was what you were trying to do.. It binds to the components and also expose the value to which it binds. Making it posible to update the given value from inside the component but also from outside the component while also exposing the value from the slot to the parent. Or, are you asking for the functionality that one can specified future bidings to the slot? |
Hi, I came across a similar/relevant issue where I'm wanting to use slot props to pass a value to a parent component so that it can be bound to a specific element in the slot. This would allow the component containing the slot to access the DOM element in For example, <Field let:input={input}>
<label>X</label>
<input bind:this={input}/> <!-- Cannot bind to a variable declared with the let: directive -->
</Field> Here's a REPL that illustrates the same issue: https://svelte.dev/repl/94dfa039dfd645ad8265609377873457?version=3.20.1 My intention is for the I've tried passing a variable Is it possible for Svelte to support this type of binding? |
I would love to see this features as well, since it makes component composition and reuse a lot easier. |
I also wanted to use this in a way to provide a context for child components, where the parent component contains a bunch of boilerplate that the child components shouldn't have to worry about, and the parent provides two-way bindable variables that children can take as props. Slots seem like these magical things that easily break when you play around with them too aggressively, I guess the benefits of a compiler don't matter when Svelte treats slots as though literally anything might fill them. |
I happened upon this issue while investigating a slightly different usage. May I ask why it isn't possible to do the following. Am I misunderstanding the question? I get that the <!-- App.svelte -->
<script>
import Component from './Component.svelte';
let item = 'hello, world';
</script>
<Component {item}>
<p>
Inside the item is {JSON.stringify(item)}
</p>
<input type="text" name="name" bind:value={item} />
</Component> <!-- Component.svelte -->
<script>
export let item;
</script>
<p>
Outside the item is {JSON.stringify(item)}
</p>
<slot /> |
@kjmph Seems to be working in the REPL: https://svelte.dev/repl/72d0ab2799a04343bc8054176ee4208d?version=3.25.0 |
Thanks for placing it in the REPL. It seems that the |
@kjmph Well that won't work since you can't redeclare |
Ahah, I think you showed the problem. If we change Component.svelte to: <script>
export let item;
</script>
<p>
Outside the item is {JSON.stringify(item)}
</p>
<slot foo={item+"nana"} /> Then the output is: Instead, if we really want two way mutation between parent and child, then it is best to represent it like this: <!-- App.svelte -->
<script>
import {item} from './stores.js';
import Component from "./Component.svelte";
$item = 'hello, worl';
</script>
<Component>
<p>
Inside the item is {JSON.stringify($item)}
</p>
<input type="text" name="name" bind:value={$item} />
</Component> <!-- Component.svelte -->
<script>
import {item} from './stores.js';
$: $item = $item.replace('world', 'cow');
</script>
<p>
Outside the item is {JSON.stringify($item)}
</p>
<slot /> // stores.js
import {writable} from 'svelte/store';
export const item = writable('hello, world'); Just type the letter |
This question subtly brought up a few points about Svelte I didn't understand. Which is why I have commented and looked into it at a greater depth. @trenta3, your link to show the problem results in a <!-- App.svelte -->
<script>
import Component from "./Component.svelte";
let items = ["foo", "bar", "mung"];
</script>
{#each items as item}
<Component bind:item>
<p>
Inside the item is {JSON.stringify(item)}
</p>
<input type="text" name="name" bind:value={item} />
</Component>
{/each} <!-- Component.svelte -->
<script>
export let item;
</script>
<p>
Outside the item is {JSON.stringify(item)}
</p>
<slot {item} /> As well, people also want to have references go back to the component. However, at the time the component is mounted, the slot isn't mounted. So, there would have to be a callback to the component when the slot is mounted. Something like this: <!-- App.svelte -->
<script>
import Component from "./Component.svelte";
import { onMount } from "svelte";
let input;
let comp;
onMount(() => {
comp.onSlotMount(input);
});
</script>
<Component bind:input bind:this={comp}>
<input bind:this={input} />
</Component>
<!-- Component.svelte -->
<script>
export let input = undefined;
export function onSlotMount(input) {
input.value = 'insert value';
}
</script>
<slot {input} /> It all has to do with how binding works. It is very non-intuitive, yet makes sense once grokked. |
@kjmph I can see my image correctly when visualizing it in GitHub: https://user-images.githubusercontent.com/10828817/71427308-93471000-26b7-11ea-84fd-b5b1a970fd5a.png |
Ah, I was referencing the REPL link that was shared. If you would like to re-post that, maybe we could coerce a solution to your liking. I think the point I've been circling, and why I got involved on the issue thread is that the Thus, with that understanding, it should be hopefully clearer that the item needs to have a location/variable that the compiler can see in the App for the slot to bind to, so that the data flows reactively. Thus, the storage should be somewhere the compiler can see it, either through a Technically, in my example above, this means that when mounting the slot in the Component, e.g. Now, I'm not a Svelte dev, so I'm sure I misused some of that terminology. Does this help? |
I did some experiments:
Code: So, if I pass store (slot prop) as prop of an inner component, it works. I think the issue is that the compiler is unwilling to use store not defined at top level, despite it can compile the code successfully. |
Hello @locriacyber, Yes, as I attempted to address with my fumbling paragraph: Your example is still passing a reference to the writable instance through to the slot, and out again to the Inner component. The Inner component needs to be written in a way that knows to subscribe to updates to the storable. i.e. with the dollar sign. Although, I will comment, the passing of the reference as a prop of an inner component gets around the "Stores must be declared at the top level of the component (this may change in a future version of Svelte)" error. So, that is pretty interesting. I never heard back from @trenta3, so I'm guessing what his usage is, but the fundamental problem that flows oddly for people is that slots get a copy of the variable when it is a discrete type. They can edit that copy, and would only see local changes. Thus, this is why the compiler won't allow us to bind variables that have been declared with a let. Now, if the compiler subtly noticed that someone wants to bind to a let, and added an instance of a storable behind the scenes, maybe that would work. Haha, silly. |
@locriacyber, if you want a laugh, change your App.svelte component to: <script>
import DataContainer from './DataContainer.svelte'
import Inner from './Inner.svelte'
export let texty;
</script>
<div>
<DataContainer let:text={text}>
<Inner text={text}/>
<p>App Truth: {text.subscribe(t => texty=t)&&texty}</p>
</DataContainer>
</div>
<style>
div {
border: solid 2px black;
}
</style> I think that goes a long way to show why slots behave contrary to developer expectations. |
Seems like we need slots to be compiled as anonymous components to fix slot-related issues like #5720, but then I don't know about which syntax to use. |
Can you explain how an anonymous component would fix this? The contents of the parent inside the slotted area is treated as a component that is passed to the child component, and the child component is responsible for copying the parent's slotted component into the slot. Thus, the compiler has restrictions on what it can do when generating the parent slotted component.. |
The |
Now this is a real show stopper, we need to address this asap... |
@JacobZwang I agree, this feels more like a missing feature than an intentional limitation. Perhaps it's just an honest oversight 😅 I was struggling with this for a while. I was trying to build a form component that abstracts things like loading state, error state, dirty state, submit & reset buttons, etc. Binding to a slot variable felt like something that should intuitively work for the inputs that end up being children of the form. The solution/workaround you posted worked perfectly for me. For those curious, here's a working repl to demonstrate: |
Hello, I know I've clumsily tried explaining.. I'll try again. The let directive is a one way binding. See the corresponding Svelte code: svelte/src/compiler/compile/nodes/Binding.ts Lines 51 to 54 in b5aaa66
This is because when the component mounts, it doesn't see the slot until it is mounted later. In fact, the reason formState needs to be defined as an empty object, is otherwise the first time it executes it would be undefined and throw an error. Then, later when onMount is called in the Form component, it tries to rebind the formState. This will rewrite the formState variable in the App with the new formState, thus re-rendering the slot. This is why bind is required, the App's formState needs to be a variable that can be referenced/written. Let won't work because it is intentionally a one-way bind. Does this help? What you wrote is precisely correct @crowdozer; that's the only way to pass back from slot to parent to grandparent. |
Could the maintainers clarify, is the lack of support for two-way binding to variables declared with the let directive due to technical limitations in Svelte's current reactivity system, some design decision or something else? To me this seems like it would be a very useful feature, especially when designing reusable containers for forms and input elements. |
Please add this to new features list |
You can use a store |
Its not a bug and works exactly as described, however the more i am developing components the more it is starting to bother me. I have some workarounds in place, but they make the code far more confusing. I have added a REPL with a super simple example of how I would love it to work. The REPL is of course broken |
Curious thing, |
I dont think there is a good way to slove this problem rn. Store might help but a component's state should not be globle, store in a context also does not help because you will need to create another wrapper just to access to the context. The best solution I can think of rn is just event dispatcher |
I haven't tried with runes yet . I hope the compiler will allow us to bind to a state rune / signal which was received by a let: statement in the slot |
I”m a little late to the party, but how about using a slot prop to pass the state in question and an additional slot prop to pass a function to change that state when needed? Something like this: <!-- App.svelte -->
<Component let:myState let:setMyState>
<p><code>myState</code> is <code>{myState}</code></p>
<button on:click={() => setMyState(!myState)}>Click me!</button>
</Component>
<!-- Component.svelte -->
<script>
let myState = false;
const setMyState = (value) => myState = value;
</script>
<h2>Child component</h2>
<slot {myState} {setMyState} /> It might not be what everyone is looking for, but it solved the problem in my use case. And credit where credit is due, I got the idea from this article written by our good friend Tan Li Hau :) |
In Svelte 5 you can also pass a state object and directly bind to it, though this will generate a warning (which I do not really agree with). <Child>
{#snippet children(data)}
<label>
In parent: {data.value}
<input type="checkbox" bind:checked={data.value} />
</label>
{/snippet}
</Child> <!-- Child.svelte -->
<script>
const { children } = $props();
const data = $state({ value: false });
</script>
{@render children(data)}
<div>In child: {data.value}</div> |
I am surprised to see this ticket is still getting attention. To reiterate my understanding of the problem, when the parent component is the source of state, and the child component mutates the state, the slot can't bind to the mutated state. There are lots of other workarounds when the component owns the state. If there are no mutations, then it is best represented by binding the parent state in the slot: <!-- App.svelte -->
<script>
import Component from "./Component.svelte";
let items = ["foo", "bar", "mung"];
</script>
{#each items as item}
<p>
Parent sees the item: {JSON.stringify(item)}
</p>
<Component bind:item>
<p>
Inside the item is {JSON.stringify(item)}
</p>
<input type="text" name="name" bind:value={item} />
</Component>
<br />
<br />
{/each}
<!-- Component.svelte -->
<script>
export let item;
</script>
<p>
Outside the item is {JSON.stringify(item)}
</p>
<slot /> However, when we want the component to mutate state, we had to do some tricks in Svelte 4: <!-- App.svelte -->
<script>
import Component from "./Component.svelte";
import { writable } from "svelte/store";
let items = ["foo", "bar", "mung"];
let shadow = writable([...items]);
function handleUpdate(index, derivedValue) {
shadow.update((current) => {
current[index] = derivedValue;
return current;
});
}
</script>
{#each $shadow as shadowItem, i}
<p>
Parent sees the item: {JSON.stringify(shadowItem)}
</p>
<Component
bind:item={items[i]}
on:derivedUpdate={(e) => handleUpdate(i, e.detail)}
shadowValue={$shadow[i]}
>
<p>
Inside the modified item is {JSON.stringify($shadow[i])}
</p>
<input type="text" bind:value={$shadow[i]} />
</Component>
<br />
<br />
{/each}
<!-- Component.svelte -->
<script>
import { createEventDispatcher, onMount } from "svelte";
export let item;
export let shadowValue;
const dispatch = createEventDispatcher();
$: derivedItem = item && `${item}-derived`;
onMount(() => {
dispatch("derivedUpdate", derivedItem);
});
$: {
if (derivedItem) {
dispatch("derivedUpdate", derivedItem);
}
}
</script>
<p>
Outside the item is {JSON.stringify(shadowValue)}
</p>
<slot /> However, in Svelte 5 the above is dramatically improved via the following: <!-- App.svelte -->
<script>
import Component from "./Component.svelte";
let items = ["foo", "bar", "mung"];
</script>
{#each items as item}
<p>
Parent sees the item: {JSON.stringify(item)}
</p>
<Component bind:item>
{#snippet input()}
<p>
Inside the modified item is {JSON.stringify(item)}
</p>
<input type="text" name="name" bind:value={item} />
{/snippet}
</Component>
<br />
<br />
{/each}
<!-- Component.svelte -->
<script>
let { input, item = $bindable() } = $props();
item = item + "-derived";
</script>
<p>
Outside the item is {JSON.stringify(item)}
</p>
{@render input()} With snippet parameters, it is really tempting in Svelte 5 to use The essential thing to realize is that the slot/snippet is compiled in the context of the parent, and can't dynamically bind based on the parameters from the component that mounts the slot/snippet. I think that's what surprises people, is that by declaring a |
Of course, now that I looked at the problem again, I see that the Svelte 4 workaround was needlessly complex. Using the Svelte 5 modification of the prop would have also worked in Svelte 4. Oh well, I'm not going back to that Svelte 4 code at this time: <!-- App.svelte -->
<script>
import Component from "./Component.svelte";
let items = ["foo", "bar", "mung"];
</script>
{#each items as item}
<p>
Parent sees the item: {JSON.stringify(item)}
</p>
<Component bind:item>
<p>
Inside the item is {JSON.stringify(item)}
</p>
<input type="text" name="name" bind:value={item} />
</Component>
<br />
<br />
{/each}
<!-- Component.svelte -->
<script>
export let item;
item = item + '-derived';
</script>
<p>
Outside the item is {JSON.stringify(item)}
</p>
<slot /> |
This bugged me because while I know derived state is supposed to be read only, I couldn't find a good way to write an example to show the weird use case.. However, I've written a lot more Next.js since the start of this ticket, and I think that their setState functions actually work well here. This isn't such a bad paradigm: <!-- App.svelte -->
<script>
import Component from "./Component.svelte";
let items = ["foo", "bar", "mung"];
</script>
{#each items as item}
<p>
Parent sees the item: {JSON.stringify(item)}
</p>
<Component bind:item>
{#snippet input(derivedItem, setDerivedItem)}
<p>
Snippet sees the item: {JSON.stringify(derivedItem)}
</p>
<input type="text" value={derivedItem} oninput={(e) => setDerivedItem(e.target.value)} />
{/snippet}
</Component>
<br />
<br />
{/each}
<!-- Component.svelte -->
<script>
let { input, item = $bindable() } = $props();
let derivedItem = $state("");
$effect(() => {
derivedItem = item + "-derived";
});
function setDerivedItem(value) {
derivedItem = value;
}
</script>
<p>
Component sees the item: {JSON.stringify(derivedItem)}
</p>
{@render input(derivedItem, setDerivedItem)}
<input type="text" bind:value={derivedItem} /> Pass the setter to the slot/snippet.. That seems to work for my issues, and is pretty clean. (I would prefer a bind in the snippet, but this isn't so bad.) |
Ah, I should delete my comments now.. @brunnerh and @rodrigodagostino; I finally caught up to your thinking.. Apologies for working this out in the issue. Now I see how I re-stumbled on the setState pattern that @rodrigodagostino suggested, and also see that @brunnerh's suggestion is even cleaner than setState: <!-- App.svelte -->
<script>
import Component from "./Component.svelte";
let items = ["foo", "bar", "mung"];
</script>
{#each items as item}
<p>
Parent sees the item: {JSON.stringify(item)}
</p>
<Component bind:item>
{#snippet input(derivedItem)}
<p>
Snippet sees the item: {JSON.stringify(derivedItem.value)}
</p>
<input type="text" bind:value={derivedItem.value} />
{/snippet}
</Component>
<br />
<br />
{/each}
<!-- Component.svelte -->
<script>
const { input, item = $bindable() } = $props();
const derivedItem = $state({ value: item });
$effect(() => {
derivedItem.value = item + "-derived";
});
</script>
<p>
Component sees the item: {JSON.stringify(derivedItem.value)}
</p>
{@render input(derivedItem)}
<input type="text" bind:value={derivedItem.value} /> Thanks @brunnerh, that's actually really helpful, and quite clean. |
I still am not 100% satisfied with using <!-- App.svelte -->
<script>
import Component from "./Component.svelte";
let items = ["foo", "bar", "mung"];
</script>
{#each items as item}
<p>
Parent sees the item: {JSON.stringify(item)}
</p>
<Component bind:item>
{#snippet input(derivedItem)}
<p>
Snippet sees the item: {JSON.stringify(derivedItem)}
</p>
<input type="text" bind:value={derivedItem} />
{/snippet}
</Component>
<br />
<br />
{/each}
<!-- Component.svelte -->
<script>
const { input, item = $bindable() } = $props();
let derivedItem = $state(item + "-derived");
function opaque() {
throw new Error("This object can only be accessed or updated through specific actions.");
}
const proxyDerivedItem = new Proxy(opaque, {
get(target, prop, receiver) {
return () => derivedItem;
},
apply(target, thisArg, argumentsList) {
if (argumentsList.length !== 1) {
throw new Error("This object only accepts a single argument to update its value.");
}
derivedItem = argumentsList[0];
return argumentsList[0];
}
});
$effect(() => {
derivedItem = item + "-derived";
});
</script>
<p>
Component sees the item: {JSON.stringify(derivedItem)}
</p>
{@render input(proxyDerivedItem)}
<input type="text" bind:value={derivedItem} /> By changing the derivedItem to a Proxy, and passing the Proxy to the snippet, it can read via a get, and write via a call. So, I removed the snippet_parameter_assignment errors and changed the phase 3 compiler pass in Svelte with a brutal hack: // This is in: packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js:BindDirective
case 'value': {
if (parent?.type === 'RegularElement' && parent.name === 'select') {
call = b.call(`$.bind_select_value`, context.state.node, get, set);
} else {
// Hack for example purposes
if (set?.body?.left?.callee?.name === "derivedItem") {
set.body = {
type: "CallExpression",
callee: set.body.left,
arguments: [{type: "Identifier", name: "$$value"}],
optional: false
}
}
call = b.call(`$.bind_value`, context.state.node, get, set);
}
break;
} Now the example code is nice and clean (minus the Proxy which can be shadowed with support code), and the snippet can bind to a snippet parameter. Is this a good thought starter? |
I do not think this a good idea. Svelte 5 has moved closer to regular JS semantics/behavior and this would go against that. Snippets are structured like functions and reassigning a normal function argument would have no effect on the outside. Diverging from that would be an unexpected and inconsistent piece of magic. |
Fair enough. It still feels like an exception to the rule to not have reactivity follow through to a snippet. It seems the compiler should know enough to figure that out. And from the developer perspective, it is $state that can be tracked from the child component and passed to the snippet/slot so why can't the snippet/slot bind to that $state? (Or, in other words, the parent component can pass $state to a child component which can bind to it, so why can't the child component pass $state to the snippet/slot which is a "child" of the child component in a logical sense.) |
Parent components pass state to child components via properties. Semantically it is consistent that properties can be changed but function arguments cannot. |
From the documentation of slots it seems it should be possible to bind values of a component to a slot:
but it seems that the real situation is different: this REPL triggers the error
Cannot bind to a variable declared with the let: directive (10:32)
.Expected behavior
Binding a variable in a slot, which is bound to a variable in the parent component, should work normally as it would if I manually substituted the slot content inside the container.
Severity
This underpins the possibility of developing a lot of components that take care of boilerplate code for my application, so in my case this effectively blocks my usage of Svelte for the project.
The text was updated successfully, but these errors were encountered: