2
votes

I have a hierarchy of the nested components (Svelte 3.35.0), which looks like this:

Component A
    Component B (third-party)
        Component C

Component C wants to dispatch an event using createEventDispatcher and Component A is listening. The documentation says:

If the on: directive is used without a value, the component will forward the event, meaning that a consumer of the component can listen for it.

The problem is.. default behavior restricts event forwarding to "Child -> Parent" and I don't have access to the Component B's code (it is a separate library). According to the rules, I need to "forward" that event up on the defined hierarchy, i.e. from B to A.

So, if I want to properly "bubble" an event from Component C to the handler, which is defined in the Component A, I need to build the following logic:

Component A have an implementation for the "onMyEventHandler" function and
it doesn't know anything about Component C (it shouldn't, for decoupling purposes).

Component A ---------------------- on:my-event={onMyEventHandler}
    Component B ------------------ on:my-event    <-- just to forward up to the parent's parent
        Component C -------------- dispatch('my-event')

The question is - how to properly pass an event from the Component C to the Component A using on:eventname directive? I've seen a bubble method in the lifecycle code (https://github.com/sveltejs/svelte/blob/v3.35.0/src/runtime/internal/lifecycle.ts#L64), but I don't understand how to properly utilize that logic. Or is there a more convenient approach?

Update: there are also "magic" props like $$props and $$restProps, which are used to forward values for the component properties via element attributes, but there are no similar "tricks" for on:-like directives.

Update 2: so, I should provide an example how it is possible (also, at this point i'm considering to use a writable store and contexts instead).

I'm using a svelte-routing library and my code looks like this:

./Application.svelte <--- Component A

<Menu items={menuItems} />

...

<Router>
    {#each menuItems as menuItem}
        {#if menuItem.component !== MenuDivider}
            <Route    <--- Component B
                path={menuItem.path}
                component={menuItem.component}    <--- Component C
                on:app.event.page.shown={onPageShown}
            />
        {/if}
    {/each}
</Router>

./Navigation/Menu.svelte

<Router>
    <ul class="menu">
        {#each items as item}
            {#if item.component === MenuDivider}
                <MenuDivider label={item.title} />
            {:else}
                {#if item.url?.length > 0}
                <li class="menu-item">
                    <Link to={item.url} getProps={onLinkUpdate}>
                    {item.title}
                    </Link>
                </li>
                {/if}
            {/if}
        {/each}
    </ul>
</Router>

./Page/AboutPage.svelte

<div> page contents... </div>

Route is a "third-party" component from the external library that provides API to define client-side routing between different "virtual" pages (i.e. page components, see the line component={menuItem.component}). The Page have an onMount with some business logic to inform the main application components about different state changes (e.g. title appends). So it emits events and the top-level application component will try to catch them (and it doesn't know how much pages we have and their names).

Route from the svelte-routing has the following signature:

{#if $activeRoute !== null && $activeRoute.route === route}
  {#if component !== null}
    <svelte:component this="{component}" location={$location} {...routeParams} {...routeProps} />
  {:else}
    <slot params="{routeParams}" location={$location}></slot>
  {/if}
{/if}

and if I place a forwarding on:onMyEventHandler directive to it like that:

<svelte:component this="{component}" location={$location} {...routeParams} {...routeProps} on:onMyEventHandler />

It will propagate an event using a path: AboutPage (project-level) -> Route (library) -> Application (project-level). But, obviously, I'm not able to do that (and there are no "$$restDirectives"/"$$forwardMePlease" trick to do this).

I think it is just a bad way to do things, thought that an "event" concept is a global thing (coming from java/php-style event dispatchers).

1
Curious, however, when I started my project (at 3.24.0) - it was possible (don't know how). After upgrading to the 3.35.0 - my code with event dispatching logic doesn't work anymore :)itnelo
How do you get C to be a child of B if B is a third-party library ? If this is through the use of slots you can directly add the event on the slotted B.Stephane Vanraes
An example is added. Yes, i can (at least it is truth for the version 3.24.0), but B is just a "bridge" between routing library and a "page component" (C), which emits events for the main application component (A), e.g. to change a shared page title.itnelo
You specifically asked about how to do it using Svelte's on: style events. If this is not set in stone, you could use custom dom events like in my example at: svelte.dev/repl/f1b23c6b95be4dfb9cad18dbba20d1f1?version=3.35.0Christian
Wow! This works exactly as I wanted, thank you for sharing, sir! So, the actual solution can be - to use some sort of "global" events on DOM or another levers like contexts/stores.itnelo

1 Answers

2
votes

This does not seem very trivial, but I found two ways you could solve this, both involve the ContextApi.

(Note that in the code below a lot is stripped out, like importing components)

using a wrapper component

The first and cleanest approach is to construct an intermediate component that will capture the event and dispatch them to the parent.

<!-- Parent -->
<EventHandler on:click="{() => console.log('click')}">
  <ThirdParty component={MyComponent} />
</EventHandler>
<!-- EventHandler -->
<script>
    import { createEventDispatcher, onMount, setContext } from 'svelte'
    setContext('context-dispatch', createEventDispatcher())
</script>
<slot />
<!-- MyComponent -->
<script>
    import { getContext } from 'svelte' 
    const dispatch = getContext('context-dispatch') 
    const handleClick = ev => dispatch('click', ev)
</script>
<button on:click={handleClick}>Click here</button>

Adding callbacks to self

The second approach is a bit dirty and requires digging into some internal workings of Svelte, these are undocumented so can change in the future (albeit unlikely they will). It somehow involves adding an event callback to the parent component from inside itself.

<script>
    import { createEventDispatcher, setContext } from 'svelte'
    import { get_current_component } from 'svelte/internal'

    const dispatch = createEventDispatcher()
    setContext('context-dispatch', dispatch)
    get_current_component().$$.callbacks['click'] = [
        () => console.log('callback')
    ] 
</script>
<ThirdParty component={MyComponent} />

I will be honest though, I didn't even know the second approach was possible and I would highly recommend against it, consider it more of an academic thought...