svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Events are not emitted from components compiled to a custom element

Open vogloblinsky opened this issue 5 years ago • 21 comments

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');
                });

vogloblinsky avatar Jun 26 '19 13:06 vogloblinsky

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.

asif-ahmed-1990 avatar Jun 26 '19 14:06 asif-ahmed-1990

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

vogloblinsky avatar Jun 26 '19 15:06 vogloblinsky

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

Conduitry avatar Jun 26 '19 16:06 Conduitry

@Conduitry ok i understand better

vogloblinsky avatar Jun 27 '19 14:06 vogloblinsky

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.

pbastowski avatar Aug 10 '19 20:08 pbastowski

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.

pbastowski avatar Aug 11 '19 14:08 pbastowski

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..

petterek avatar Sep 18 '19 08:09 petterek

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 }))
}

TehShrike avatar Dec 17 '19 15:12 TehShrike

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.

TehShrike avatar Dec 17 '19 15:12 TehShrike

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.

TehShrike avatar Jan 16 '20 23:01 TehShrike

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 }))
}

Grafikart avatar Feb 20 '20 01:02 Grafikart

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.

rburnham52 avatar Apr 28 '20 01:04 rburnham52

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
}));

raven-connected avatar May 26 '20 15:05 raven-connected

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 svelteDispatch = createEventDispatcher();
const component = get_current_component();

const dispatch = (name, detail) => {
  svelteDispatch(name, detail);
  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>

Shyam-Chen avatar Jul 15 '20 09:07 Shyam-Chen

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>

rhideg avatar Oct 09 '20 14:10 rhideg

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 :)

tricinel avatar Oct 12 '20 07:10 tricinel

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.

akauppi avatar Dec 10 '20 19:12 akauppi

try it

import App from './App.svelte';
App.prototype.addEventListener = function(...arg){
	this.$on(arg[0],arg[1])
    document.addEventListener.call(this,...arg)
}

cereschen avatar Dec 25 '20 14:12 cereschen

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Jun 26 '21 22:06 stale[bot]

If anyone is having trouble with dispatchEvent being null, be sure to access this in your topmost component. In my case, because it was a web component, any nested Svelte component doesn't have the dispatchEvent. Thus, to resolve this, either dispatch from the web component, or keep the component inside a store. Later you can get the component and call the component.dispatchEvent, which will work. Do not try to store the function itself because it will trigger an invalid call exception. Hope it helps.

gorjan-mishevski avatar Jun 29 '21 13:06 gorjan-mishevski

Any update regarding this issue? It's still a problem, but IE11 nowadays shouldn't hold back new feature. Would it be acceptable to review https://github.com/sveltejs/svelte/pull/2101 solution and use in only when legacy mode is enabled, and use CustomEvent for other targets?

sinedied avatar Dec 08 '22 21:12 sinedied

Here's a short utility for defining typed dispatch function that works in custom elements

import { get_current_component } from 'svelte/internal'

export const defineDispatch = <T extends DispatchOpts>() => {

  const component = get_current_component()

  return <N extends keyof T, D extends T[N]>(name: N, detail?: D) => {

    component.dispatchEvent(
      new CustomEvent(name as string, {
        detail,
        bubbles: true,
        composed: true
      })
    )

  }

}

type DispatchOpts = Record<string, any>

Usage

const dispatch = defineDispatch<{
  myBooleanEmit: boolean
  myStringEmit: string
}>()

dispatch('myBooleanEmit', true)
dispatch('myNumberEmit', 22)

gVguy avatar Feb 16 '23 10:02 gVguy

Closed via #8457, to be released in Svelte 4

dummdidumm avatar May 02 '23 10:05 dummdidumm