Is it possible to dispatch a svelte custom event with a target object?

Viewed 2879

Is it possible to dispatch a svelte event (created with createEventDispatcher) with a target object like a native browser event?

I.e. receiving on the handler side event.target.value instead of event.detail.

4 Answers

Yes, it's possible but involve some hacking.

You can take a look into the svelte source code to see how event dispatching works. I'll outline the key parts as below.

Let's say we have two components, Inner and Outer.

<!-- Inner.svelte -->
<script>
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
const dispatchFoo = () => dispatch('foo', 'bar')
</script>

<button on:click={dispatchFoo}>send</button>

<!-- Outer.svelte -->
<script>
import Inner from './Inner.svelte'
const handleFoo = (e) => { console.log('receive foo', e) }
</script>

<Inner on:foo={handleFoo}></Inner>

If you think about it, the event handler handleFoo is created in Outer, but it's actually register on Inner.

Check the compiled JS code you'll see:

  1. inner.$$.callbacks["foo"] holds the event handler [src]
  2. when you click the button and call dispatch, it just read the inner.$$.callbacks["foo"] for potential handlers, and if found, call them with CustomEvent as argument [src]

The only way to set customEvent.target is by dispatch that custom event using element.dispatchEvent(customEvent). But element.dispatchEvent is not used through the whole process.

Solution (hack)

Write your own createEventDispatcher.

import { get_current_component } from 'svelte/internal'

function createEventDispatcher() {
  const component = get_current_component();
  return (type, target, detail) => {
    const callbacks = component.$$.callbacks[type];
    if (callbacks) {
      const event = new CustomEvent(type, { detail });
      // the key is to call `dispatchEvent` manually to set `event.target`
      target.dispatchEvent(event);
      callbacks.slice().forEach((fn) => {
        fn.call(component, event);
      });
    }
  };
}

Svelte REPL

Modified from hackape's answer

import { get_current_component } from 'svelte/internal'

function createEventDispatcher() {
  const component = get_current_component(bubbles = true);
  return (type, target, detail) => {
    const callbacks = component.$$.callbacks[type];
    if (callbacks) {
      const event = new CustomEvent(type, { bubbles, detail });
      // the key is to call `dispatchEvent` manually to set `event.target`
      target.dispatchEvent(event);
      /* You have already raised an event, you should not repeat the callbacks to avoid duplication
        callbacks.slice().forEach((fn) => {
          fn.call(component, event);
        });
      */
    }
  };
}

user5354671 Commenting out fn.call(...) wont invoke the callback on the outer component.

The target.dispatchEvent() only serves for the purpose of adding the target on that event object

You may want to pass the target explicitly:

// Component.svelte
<script>
  import { createEventDispatcher } from 'svelte'
  const payload = 'foo'

  function onClick (event) {
    const { target } = event
    dispatch('myEvent', { target, payload })
  }
</script>

<button on:click={ onClick }></button>

And then, somewhere:

<script>
  import Component from 'Component.svelte'

  function handler (event) {
    const { target, payload } = event.detail
  }
</script>

<Component on:myEvent={ handler }/>
Related