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

Allow slots to forward events #7881

Closed
theetrain opened this issue Sep 19, 2022 · 6 comments
Closed

Allow slots to forward events #7881

theetrain opened this issue Sep 19, 2022 · 6 comments

Comments

@theetrain
Copy link
Contributor

theetrain commented Sep 19, 2022

Describe the problem

When it comes to event forwarding via createEventDispatcher, I understand events do not bubble on purpose (citation: #149). A component can fire an event, and consumers of that component can forward the event via on:eventname.

When events are fired from a component within a slot, I don't see a way for the slot or wrapping component to receive that event (or forward it further).

<!-- ComponentOne.svelte -->
<p>This is a page.</p>
<slot on:customevent /> <!-- doesn't forward event -->

<!-- ComponentTwo.svelte -->
<script>
import { createEventDispatcher } from 'svelte'
import ComponentOne from './ComponentOne.svelte'

const dispatch = createEventDispatcher()

const runCustomEvent = () => dispatch('customevent')
</script>
<ComponentOne>
  <button on:click={runCustomEvent}>Click me!</button>
</ComponentOne>

<!-- Page.svelte -->
<script>
import ComponentTwo from './ComponentTwo.svelte'
</script>

<ComponentTwo />

Above example as a REPL: https://svelte.dev/repl/5b85c9289d1b4c55baa09a4fc1a8b93c?version=3.50.1

The use case I'm facing is in SvelteKit: I have a layout that has an interactive element, the element dispatches an event and the layout has logic to handle that. I want one of the layout's nested pages to dispatch the same event so that the event handler in the layout can receive the dispatched event.

Describe the proposed solution

If there was a way for slotted components to forward their events, then parent components or pages could listen to those dispatched events. Something like <slot on:customevent /> could bring a couple of benefits:

  1. Wrapping components can intentionally listen to custom events
  2. SvelteKit layouts can listen to events forwarded from pages or page components

Alternatives considered

The proposed solution is yet another way for children to communicate with their parents, in addition to the alternatives:

  1. Stores consumed by the child and parent. I'm guessing the child would set a flag in the store, and the parent would react to that flag and then unset it
  2. Svelte Context API
  3. The standard browser CustomEvent constructor that can bubble events up the DOM

Importance

nice to have

@tanhauhau
Copy link
Member

From the code snippets you showed above, the parent component of ComponentTwo is Page.svelte, as you can see it is Page.svelte that have <ComponentTwo />.

So in this case, the customevent event is dispatched to Page.svelte and you can listen the event from there:

<ComponentTwo on:customevent />

On the other hand, ComponentTwo.svelte is ComponentOne.svelte', evidently you have <ComponentOne /> in ComponentTwo.svelte.

To call a function within ComponentOne.svelte when a button within ComponentTwo.svelte is clicked (even though within the component tree, the button is within ComponentOne, but it is ComponentTwo.svelte that the button is specified.), there are a few options:

  1. you can export a function as component method and call it
<!-- ComponentOne.svelte -->
<script>
  export function customEvent() {}
</script>
<p>This is a page.</p>
<slot />

<!-- ComponentTwo.svelte -->
<script>
  import ComponentOne from './ComponentOne.svelte'
  let component;
  const runCustomEvent = () => component.customEvent();
</script>
<ComponentOne bind:this={component}>
  <button on:click={runCustomEvent}>Click me!</button>
</ComponentOne>
  1. you can pass a function into the slot from ComponentOne
<!-- ComponentOne.svelte -->
<script>
  function customEvent() {}
</script>
<p>This is a page.</p>
<slot {customEvent} />

<!-- ComponentTwo.svelte -->
<script>
  import ComponentOne from './ComponentOne.svelte'
</script>
<ComponentOne let:customEvent>
  <button on:click={customEvent}>Click me!</button>
</ComponentOne>

@theetrain
Copy link
Contributor Author

Thanks @tanhauhau for sharing those ideas, those techniques should come in handy!

I guess I'm searching for "Svelte thinking" when it comes to forwarding events, and cross-component communication. There's no official protocol, but maybe we can establish one for ideological purposes. How about this:

  • Components can forward events, but no more than 1 component up (though you have the option to forward indefinitely) because bubbling events is considered difficult to track without an explicit interface. This is currently not possible in the slotted scenario, hence this ticket.
  • When components want to send a signal to parents more than 1 level up the DOM tree, they should make use of Stores.
  • When many child components need to react to a config set by a wrapper, use Context.

I hope we can refine this protocol so that it can make its way to the docs to better advise developers like me.

As for component library maintainers, how would they best allow parent components up the DOM tree to react to their events? They can set up event dispatchers, but the "slot forwarding" conundrum may appear again when the component you're consuming doesn't have a defined prop to receive a callback function. Should all component library maintainers be setting up callback props?

@anxpara
Copy link

anxpara commented Feb 23, 2023

+1 for allowing slots to handle and/or forward events. i don't see why the approach should be any different than handling/forwarding events from directly-referenced components

i have a layout for my documentation route which has a slot for the individual pages, and it also directly references a navigation component. the navigation component can trigger a navigation event. i want the pages to be able to trigger a navigation event as well, but unfortunately "<slot> cannot have directives"

i'm probably going to use a simple 3rd party events library instead since i can't do this nicely the svelte way

@humanist-bean
Copy link

I really like Svelte so far, but I'm annoyed by the limitations on the "slot". It would be much simpler if we could simply forward dispatches and handle events from it, and I don't see why that would be a problem.

@anxpara
Copy link

anxpara commented Oct 6, 2023

since this hasn't even been acknowledged yet--@Conduitry @Rich-Harris

@theetrain
Copy link
Contributor Author

I think it's fair to say this is being solved with event props in Svelte 5.

<!-- Button.svelte -->
<script>
	const {children, ...props} = $props()
</script>

<button {...props}>{@render children()}</button>
<!-- App.svelte -->
<script>
	import Button from './Button.svelte'
</script>

<Button onclick={() => console.log('stew')}>Make stew</Button>

Demo (hopefully this link ages well).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants