Svelte: Events are not emitted from components compiled to a custom element

Created on 26 Jun 2019  路  17Comments  路  Source: sveltejs/svelte

The native Svelte syntax for listening events on:mycustomevent doesn't works with events dispatched by a Svelte component exported to Custom Element.

May be related to this ? https://github.com/sveltejs/svelte/blob/a0e0f0125aa554b3f79b0980922744ee11857069/src/runtime/internal/Component.ts#L162-L171

Here is a reproduction repository :

https://github.com/vogloblinsky/svelte-3-wc-debug

svelte3-raw

Example using just Svelte syntax. Inner component dispatch a custom event 'message'. App component listen to it using on:message

It works !

//Inner.svelte
<script>
    import { createEventDispatcher } from 'svelte';

    const dispatch = createEventDispatcher();

    function sayHello() {
        console.log('sayHello in child: ', 'Hello!');
        dispatch('message', {
            text: 'Hello!'
        });
    }
</script>

<button on:click={sayHello}>
    Click to say hello
</button>
//App.svelte
<script>
    import Inner from './Inner.svelte';

    function handleMessage(event) {
        console.log('handleMessage in parent: ', event.detail.text);
    }
</script>

<Inner on:message={handleMessage}/>

svelte3-wc

Example using just Svelte syntax and exporting component to Web Components. Inner component dispatch a custom event 'message'. App component listen to it using on:message

Same syntax doesn't work.

//Inner.svelte
<svelte:options tag="inner-btn"/>
<script>
    import { createEventDispatcher } from 'svelte';

    const dispatch = createEventDispatcher();

    function sayHello() {
        console.log('sayHello in child: ', 'Hello!');
        dispatch('message', {
            text: 'Hello!'
        });
    }
</script>

<button on:click={sayHello}>
    Click to say hello
</button>
//App.svelte
<svelte:options tag="my-app" />
<script>
    import Inner from './Inner.svelte';

    function handleMessage(event) {
        console.log('handleMessage in parent: ', event.detail.text);
    }
</script>

<inner-btn on:message={handleMessage}/>

Vanilla JS works fine in public/index.html

const button = document
                    .querySelector('my-app')
                    .shadowRoot.querySelector('inner-btn');

                button.$on('message', e => {
                    console.log('handleMessage in page');
                });
custom element bug

Most helpful comment

I face the same issue and it's a dealbreaker :(

I confirm what @TehShrike said and his solution proves that it could work

import { createEventDispatcher } from "svelte"
import { get_current_component } from "svelte/internal"

const component = get_current_component()
const svelteDispatch = createEventDispatcher()
const dispatch = (name, detail) => {
    svelteDispatch(name, detail)
    component.dispatchEvent && component.dispatchEvent(new CustomEvent(name, { detail }))
}

All 17 comments

I ran into a similar problem today and found a workaround for now.

createEventDispatcher uses the following for creating a custom event.

https://github.com/sveltejs/svelte/blob/a0e0f0125aa554b3f79b0980922744ee11857069/src/runtime/internal/dom.ts#L254-L257

Now, for custom elements, the custom event by default does not go past the boundaries of the shadowDom. For that to happen, a property named composed has to be set to true. (Refer: Event.composed)

To make it cross the boundaries of shadowDom we have to create a Custom Event as mentioned in the v2 docs for svelte in vanilla JS.

const event = new CustomEvent('message', {
    detail: 'Hello parent!',
    bubbles: true,
    cancelable: true,
    composed: true // makes the event jump shadow DOM boundary
});

this.dispatchEvent(event);

Link: Firing events from Custom Element in Svelte

Note: I am new to Svelte and may be terribly wrong with my analysis 馃槄 Probably @Rich-Harris can clear it out.

Thanks @asifahmedfw for your feedbacks.

Just discovered inside Svelte codebase that an deprecated APi is called by Svelte : https://github.com/sveltejs/svelte/blob/767ce22ed1b8de41573311dd6180c12837443cc9/src/runtime/internal/dom.ts#L255

https://developer.mozilla.org/fr/docs/Web/API/CustomEvent/initCustomEvent

cc @Rich-Harris

See #2101 for why we're using that API.

@Conduitry ok i understand better

See #2101 for why we're using that API.

Is this the reason why Svelte 3 custom events do not reach the custom element itself?

I have encountered the same problem as described by the original poster and am looking for a clean solution. Hopefully without resorting to creating Event objects and manually dispatching them.

I am thinking about using Svelte in a large company, to build features wrapped in WebComponents almost exclusively, because web-components are their strategy for the future. I am proposing Svelte 3 instead of lit-element, their current choice, because lit-element lacks a lot of functionality present in modern front-end frameworks. Also because I think Svelte 3 is the easiest to learn and maintain, powerful front-end framework to date (having used VueJs 2 since 2016 and AngularJs since 2012).

However, there is an issue that I have encountered, which could prevent the adoption of Svelte 3 in this company. The problem is that custom events emitted from within a Svelte 3 feature wrapped as a web-component do not bubble up to the web-component itself as normal DOM events and can not be handled in the usual manner within the template, for example

This does not work

<my-feature onmy-special-event="handler()">

Workaround 1

Instead we have to write special code in JS that looks up the <my-feature> element, after DOM has been mounted, and then manually assign a handler using $on like this:

window.addEventListener('load', () => {
    document.querySelector('my-feature').$on('my-special-event', handler)
})

Although doable, this is a cumbersome and non-standard way to add event handlers to the custom web-component element.

Workaround 2

Same method as mentioned above, which involves creating a native DOM Event and dispatching it manually from within a Svelte event handler. Note that you need composed:true otherwise it won't break the shadowRoot barrier.

Event

let event = new Event('my-special-event', { 
    detail: {abc: 123}, 
    bubbles: true, 
    composed: true 
})

Svelte template event handler

<my-input on:my-click={()=>el.dispatchEvent(event)} />

Request

Can we automatically add the required functionality, when compiling to a web-component target, to auto forward real DOM events to the custom element for each forwarded Svelte event?

I'm happy to help, but don't know where to start in the codebase.

Anyone looking into this.. Exposing custom events from custom elements must be a thing that is needed?
Should also be doable to fix this if the 'custom element' root is exposed somehow.

Then root.dispatchEvent('name', options) would do it.
As of now I cannot see a way to get a reference to the root element of the custom control.. but I might be wrong..

In Svelte 3 I'm working around this with

import { createEventDispatcher } from "svelte"
import { get_current_component } from "svelte/internal"

const component = get_current_component()
const svelteDispatch = createEventDispatcher()
const dispatch = (name, detail) => {
    svelteDispatch(name, detail)
    component.dispatchEvent && component.dispatchEvent(new CustomEvent(name, { detail }))
}

One possible solution might be to use

return new CustomEvent(type, { detail })

when targeting custom elements, and use the current

const e = document.createEvent('CustomEvent')
e.initCustomEvent(type, false, false, detail)
return e

method otherwise.

I believe the main issue here isn't that the event is instantiated without bubbles: true, it's the fact that component.dispatchEvent is never called to emit the event to the DOM.

I face the same issue and it's a dealbreaker :(

I confirm what @TehShrike said and his solution proves that it could work

import { createEventDispatcher } from "svelte"
import { get_current_component } from "svelte/internal"

const component = get_current_component()
const svelteDispatch = createEventDispatcher()
const dispatch = (name, detail) => {
    svelteDispatch(name, detail)
    component.dispatchEvent && component.dispatchEvent(new CustomEvent(name, { detail }))
}

Any update on this issue? we are looking into using custom elements as a way to slowing convert our legacy AngularJS components. ideally i would like to keep our svelte components clean and without work around's required only for legacy code integration.

It also looks like we can't use event forwarding. It would unfortunate to have to not use native svelte features just to support legacy code. We really want to be designing clean base components for a framework that we will be using moving forward both in svelte and our legacy app.

Using the workaround mentioned in #3091 (Specifically the Stackoverflow answer), we are able to emit events as follows (after onMount()):

$: host = element && element.parentNode.host // element is reference to topmost/wrapper DOM element
...
host.dispatchEvent(new CustomEvent('hello', {
  detail: 'world',
  cancelable: true,
  bubbles: true, // bubble up to parent/ancestor element/application
  composed: true // jump shadow DOM boundary
}));

Anyone see any red flags? This seemed straightforward enough.

EDIT:

This failed in Storybook. Had to be a little smarter:

function elementParent(element) {
    if (!element) {
        return undefined;
    }

    // check if shadow root (99.9% of the time)
    if (element.parentNode && element.parentNode.host) {
        return element.parentNode.host;
    }

    // assume storybook (TODO storybook magically avoids shadow DOM)
    return element.parentNode;
}

let componentContainer
$: host = elementParent(componentContainer);
...
host.dispatchEvent(new CustomEvent('hello', {
  detail: 'world',
  cancelable: true,
  bubbles: true, // bubble up to parent/ancestor element/application
  composed: true // jump shadow DOM boundary
}));

TehShrike's solution worked for me.

<!-- Good.svelte -->

<svelte:options tag={null} />

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

const component = get_current_component();
const svelteDispatch = createEventDispatcher();

const dispatch = (name, detail) => {
  svelteDispatch(name, detail);
  component.dispatchEvent && component.dispatchEvent(new CustomEvent(name, { detail }));
  // or use optional chaining (?.)
  // component?.dispatchEvent(new CustomEvent(name, { detail }));
};

function sayGood() {
  dispatch('good', { text: 'Good!' });
}
</script>

<button on:click="{sayGood}">Good</button>
<!-- Test.vue -->
<template>
  <div>
    <cpn-good @good="log"></cpn-good>
  </div>
</template>

<script>
import Good from '~/components/Good';  // import the compiled Good.svelte

customElements.get('cpn-good') || customElements.define('cpn-good', Good);

export default {
  methods: {
    log(evt) {
      console.log(evt.detail.text);  // output: Good
    },
  },
};
</script>
import { createEventDispatcher } from "svelte";
import { get_current_component } from 'svelte/internal';

const component = get_current_component();
const svelteDispatch = createEventDispatcher();

function sayHello() {
    dispatch("message", {
        text: "Hello!",
    });
}
const dispatch = (name, detail) => {
    console.log(`svelte: ${name}`);
    svelteDispatch(name, detail);
    component.dispatchEvent &&
        component.dispatchEvent(new CustomEvent(name, { detail }));
};

This works in angular also.

<svelte-test
    (message)="svelteClick($event.detail)">
</svelte-test>

I did something similar as well...https://github.com/tricinel/svelte-timezone-picker/blob/7003e52887067c945ad1d0070a1505cd76c696f0/src/Picker.svelte#L118. I wanted two separate builds for web and for svelte, that's why I did that __USE__CUSTOM__EVENT__ - I don't quite like it...:(

I ended up removing it :)

I noticed today that Svelte (3.31.0) custom events lack the .target field. This is unfortunate, since it would allow an outer component to know, which of its N identical sub-components emitted the event. Now I need to add the reference in .detail- or forego events and use function references.

This is a tiny detail, and I wish not stir the issue much. Just that when features get done, it would be nice that also this detail be included/considered.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

angelozehr picture angelozehr  路  3Comments

Rich-Harris picture Rich-Harris  路  3Comments

clitetailor picture clitetailor  路  3Comments

AntoninBeaufort picture AntoninBeaufort  路  3Comments

plumpNation picture plumpNation  路  3Comments