Search code examples
javascriptsveltesvelte-3svelte-component

Listening to a dispatched event from a Svelte component


I am looking for a way to listen to a dispatched event from a Svelte component within another component from JavaScript (and not from the on: syntax).

Here is the code I am trying to achieve on REPL.

The expected behaviour would be to have 0 displayed in the console when the button Close 0 is clicked, and so on for the other ones.


Solution

  • I went through some digging in svelte's compiled code, and I found a solution to listen on svelte's handled event, but it's not pretty :)

    You can (and should) dispatch your own custom event when calling onClose, but here's the solution:

    on Nested.svelte

    <script context="module">
        let counter = 0
    </script>
    
    <script>
        import { createEventDispatcher, onMount } from 'svelte';
        // add this
        import { get_current_component } from 'svelte/internal'; 
        let _this;
        const id = counter++
      const dispatch = createEventDispatcher()
        /*********
         and add this reactive statement
        **********/
        $: {
            if (_this){
                _this.parentNode.hosts = (_this.parentNode.hosts || []);
                _this.parentNode.hosts.push(get_current_component());
            }
        } 
        /*********
         end
        **********/
        function onClose() {
            dispatch('close', id)
        }
    </script>
    <!-- bind this -->
    <button bind:this={_this} class='nested-button' on:click={onClose}>
        Close {id}
    </button>
    

    Then in your App.svelte

    <script>
        import { onMount } from 'svelte'
        import Nested from './Nested.svelte'
    
        let element
    
        onMount(() => {
            // requestAnimationFrame is required!
            requestAnimationFrame(() => element.hosts.forEach(nestedButton => {
            nestedButton.$on('close', (e) => {
                    console.log(e.detail)
                })
            }));
        })
    </script>
    
    <ul bind:this={element}>
        <Nested/>
        <Nested  />
        <Nested />
    </ul>
    

    Explanation -

    the only way to bind to a svelte event is by getting a reference to the calling component and calling the $on method, but currently there's no easy way of getting a component's reference.

    so what I did was calling the svelte's internal get_current_component, which will return the current component (but for some reason won't work when called inside onMount).

    I appended the reference to the parentNode.hosts of the top most element in the component, which in this case is the ul element.

    then you can just call the $on method for each reference in element.hosts.

    The appropriate solution however will be dispatching you own event like this:

    function onClose() {
            dispatch('close', id)
            this.dispatchEvent(new CustomEvent('close', {detail: id}));
        }
    

    And by that achieving almost the exact same thing without messing with svelte's internals