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

Passing values from slot to parent #3617

Open
trenta3 opened this issue Sep 24, 2019 · 30 comments
Open

Passing values from slot to parent #3617

trenta3 opened this issue Sep 24, 2019 · 30 comments

Comments

@trenta3
Copy link

trenta3 commented Sep 24, 2019

From the documentation of slots it seems it should be possible to bind values of a component to a slot:

Slots can be rendered zero or more times, and can pass values back to the parent using props. The parent exposes the values to the slot template using the let: directive.

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.

@JazielGuerrero
Copy link

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 HTML element—part of the Web Components technology suite—is a placeholder inside a web component that you can fill with your own markup, which lets you create separate DOM trees and present them together.

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:

Slots can be rendered zero or more times, and can pass values back to the parent using props. The parent exposes the values to the slot template using the let: directive.

Continuing the documentation after the previous line it saids:

The usual shorthand rules apply — let:item is equivalent to let:item={item}, and <slot {item}> is equivalent to <slot item={item}>.

This means that to expose a property from the slot child to the parent you need to do:

<slot {item} /> or <slot item={item}></slot>

And to use that property on the parent you need to do:

let:item or let:item={item}

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:

Just as you can bind to properties of DOM elements, you can bind to component props.

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?

@trenta3
Copy link
Author

trenta3 commented Dec 25, 2019

Thank you very much for the explanation: I though I knew slots but I didn't.
Now that I got a clearer picture, I still don't understand why that error message (cannot bind to variable declared with let:) is there, in the sense that for me it would make a lot of sense to both bind (which connects bidirectionally the App#item variable with the Component#item variable) and also let (which connects the slot#item variable with the Component#item variable, allowing data to flow from slot to Component, and thus to the top-level App via the bind syntax.

I have seen your example and it works, but I think that this new REPL better shows the problem, having multiple objects that I would like my internal template to bind to.
I think this problem cannot be solved, as of now, in svelte.

I definitely think that I'm asking for future bindings to the slot, if that means to bidirectionally bind the slot#item variable to the Component#item variable.
I also add an image to better explain my understanding of the current situation:

svelte-flow-data

@tanhauhau tanhauhau added the slot label Mar 16, 2020
@stefan-pdx
Copy link

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 onMount.

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 Field component to be able to access the input DOM element in onMount.

I've tried passing a variable input into Field as a prop and also binding it to the input, however when onMount is called in Field, it appears that the reference to the input element is null.

Is it possible for Svelte to support this type of binding?

@Stazer
Copy link

Stazer commented May 25, 2020

I would love to see this features as well, since it makes component composition and reuse a lot easier.

@marcusnewton
Copy link

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.

@kjmph
Copy link

kjmph commented Sep 14, 2020

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 let:item directive is a nice shorthand, yet isn't this still possible?

<!-- 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 />

@marcusnewton
Copy link

@kjmph
Copy link

kjmph commented Sep 15, 2020

Thanks for placing it in the REPL. It seems that the let:item directive doesn't work, yet using it verbosely works? Unless I misunderstand the issue.

@marcusnewton
Copy link

@kjmph Well that won't work since you can't redeclare item, but you can do this: https://svelte.dev/repl/0133298870654061a2fb8ed5b8a8f65e?version=3.25.0

@kjmph
Copy link

kjmph commented Sep 16, 2020

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:

Screen Shot 2020-09-15 at 11 14 50 PM

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 d in the input box, and watch the input value change.

@kjmph
Copy link

kjmph commented Sep 16, 2020

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 Non-Image content-type returned response; so it is hard to figure out what the issue is. I realize two things, using stores is a bit boilerplate. So, it seems people want to do things like this:

<!-- 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.

@trenta3
Copy link
Author

trenta3 commented Sep 20, 2020

@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
Concerning all the updates, I didn't have time to read them yet because of quite busy days.
I will come back to it though in a few days.

@kjmph
Copy link

kjmph commented Sep 22, 2020

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 let:item is confusing, and also not needed. I'm going to mess up an explanation since I don't know Svelte internals. However, it appears the problem is that the slot's bind doesn't have a direct reference to the variable stored in the component itself. If we disable the warning message in Svelte, and allow us to bind to the let:item we find that we don't receive updates from the component. I'm guessing this is because let:item is a copy, which explains why the let directive can accept a different variable name in the slot. Thus, even if input is bound to the let:item, it is bound to a local copy in the slot, which isn't re-copied until the slot is re-hydrated.

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 writable via svelte/store or through variables in the App <script> section. This means that you can bind the item, and pass it direct to the Component, e.g. <Component bind:item>. Also, the slot can bind item directly, since the storage is in a known location.

Technically, in my example above, this means that when mounting the slot in the Component, e.g. <slot item={item} />, the slot props do not require the item being passed back to the slot, since the slot can reference it directly. It could be written as <slot />.

Now, I'm not a Svelte dev, so I'm sure I misused some of that terminology. Does this help?

@iacore
Copy link

iacore commented Jan 5, 2022

I did some experiments:

  1. Cannot bind to store (slot prop) value inside slot; only the initial value is used
  2. Can set to store (slot prop) with store.set(...)
  3. Compiler refuse to compile bind:value={$store}

Code:
https://svelte.dev/repl/d8122e3e66a24df6b14706c70fce5cd8?version=3.44.3

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.

@kjmph
Copy link

kjmph commented Jan 6, 2022

Hello @locriacyber, Yes, as I attempted to address with my fumbling paragraph: Thus, the storage should be somewhere the compiler can see it, either through a writable via svelte/store or through variables in the App <script> section. This means that you can bind the item, and pass it direct to the Component, e.g. <Component bind:item>. Also, the slot can bind item directly, since the storage is in a known location.

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.

@kjmph
Copy link

kjmph commented Jan 6, 2022

@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.

@iacore
Copy link

iacore commented Jan 6, 2022

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.

@kjmph
Copy link

kjmph commented Jan 6, 2022

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..

@iacore
Copy link

iacore commented Jan 6, 2022

The bind: directive can be used if slot content has the all features of normal components.

@JacobZwang
Copy link

I'm confused why this issue never went anywhere. This seems like a bug to me. If I make a component like this:

<script>
	let value = "text";
</script>

<input bind:value> {value}

everything works as expected. The input's value is bound to the value variable.

However, if I do the exact same thing using a slot, it throws the error Cannot bind to a variable declared with the let: directive.

<!-- Component.svelte -->
<script>
	export let value = "text";
</script>

<slot {value}/> {value}
<script>
	import Component from "./Component.svelte";
</script>

<Component let:value>
	<input bind:value>
</Component>

And it's not like this binding is impossible, because it can easily be solved by using some boilerplate to create an intermediary variable between the 2 components.

<script>
	import Component from "./Component.svelte";
	let value;
</script>

<Component bind:value>
	<input bind:value>
</Component>

So from my understanding, what's happening is something like this.
svelte let_value bind_value drawio
Why is it that using a slot requires creating an intermediary variable? Is there something I'm missing?

If you only have 1 component, this isn't too bad, but if you have lots of components you have to create a variable for each one. let value, value2, value3, value4 ...

If I can be of any help in fixing this let me know. I would be happy to submit a PR if someone could point me in the right direction. This issue makes building composable components very impractical since in order to consume them you must define your own variable to bind to.

@sourcegr
Copy link

Now this is a real show stopper, we need to address this asap...
it makes it very hard to reuse components :(

@crowdozer
Copy link

crowdozer commented May 26, 2022

@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:
https://svelte.dev/repl/f4d7f40f5f9b41d0b8a6218900b017ae?version=3.48.0

@kjmph
Copy link

kjmph commented May 26, 2022

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:

// make sure we track this as a mutable ref
if (scope.is_let(name)) {
component.error(this, compiler_errors.invalid_binding_let);
return;

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.

@FeldrinH
Copy link

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.

@matheusbenedet
Copy link

Please add this to new features list

@iacore
Copy link

iacore commented Mar 14, 2023

You can use a store

@whishkid
Copy link

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
https://svelte.dev/repl/b94918ce32bc4a5ba5c42f50020c3ee3?version=4.2.0

@MeroVinggen
Copy link

Curious thing, let: directive works with slots just fine in svelte3 (checked in 3.57.0) but doesn't in svelte4

@KaiyuanMa
Copy link

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

@whishkid
Copy link

whishkid commented Mar 7, 2024

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

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