Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Events are not emitted from components compiled to a custom element #3119

Closed
vogloblinsky opened this issue Jun 26, 2019 · 23 comments · Fixed by #8457
Closed

Events are not emitted from components compiled to a custom element #3119

vogloblinsky opened this issue Jun 26, 2019 · 23 comments · Fixed by #8457

Comments

@vogloblinsky
Copy link

vogloblinsky commented Jun 26, 2019

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 ?

$on(type, callback) {
// TODO should this delegate to addEventListener?
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
if (index !== -1) callbacks.splice(index, 1);
};
}

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');
                });
@asif-ahmed-1990
Copy link

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

createEventDispatcher uses the following for creating a custom event.

export function custom_event<T=any>(type: string, detail?: T) {
const e: CustomEvent<T> = document.createEvent('CustomEvent');
e.initCustomEvent(type, false, false, detail);
return e;

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.

@vogloblinsky
Copy link
Author

Thanks @asifahmedfw for your feedbacks.

Just discovered inside Svelte codebase that an deprecated APi is called by Svelte :

e.initCustomEvent(type, false, false, detail);

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

cc @Rich-Harris

@Conduitry
Copy link
Member

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

@vogloblinsky
Copy link
Author

@Conduitry ok i understand better

@pbastowski
Copy link

pbastowski commented Aug 10, 2019

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
Copy link

pbastowski commented Aug 11, 2019

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.

@petterek
Copy link

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

@TehShrike
Copy link
Member

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 TehShrike added the bug label Dec 17, 2019
@TehShrike
Copy link
Member

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 TehShrike changed the title [BUG] Custom elements and events dispatching Events are not emitted from components compiled to a custom element Dec 18, 2019
@TehShrike
Copy link
Member

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.

@Grafikart
Copy link

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

@rburnham52
Copy link

rburnham52 commented Apr 28, 2020

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.

@raven-connected
Copy link

raven-connected commented May 26, 2020

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

@Shyam-Chen
Copy link

Shyam-Chen commented Jul 15, 2020

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>

@rhideg
Copy link

rhideg commented Oct 9, 2020

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>

@tricinel
Copy link

tricinel commented Oct 12, 2020

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

@akauppi
Copy link

akauppi commented Dec 10, 2020

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.

@cereschen
Copy link

try it

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

@stale
Copy link

stale bot commented Jun 26, 2021

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 stale bot added the stale-bot label Jun 26, 2021
@stale stale bot removed the stale-bot label Jun 26, 2021
@stale stale bot removed the stale-bot label Jun 27, 2021
@gorjan-mishevski
Copy link

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.

@sinedied
Copy link

sinedied commented Dec 8, 2022

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 #2101 solution and use in only when legacy mode is enabled, and use CustomEvent for other targets?

@gVguy
Copy link

gVguy commented Feb 16, 2023

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)

@baseballyama baseballyama added this to the 4.x milestone Feb 26, 2023
dummdidumm added a commit that referenced this issue May 2, 2023
This is an overhaul of custom elements in Svelte. Instead of compiling to a custom element class, the Svelte component class is mostly preserved as-is. Instead a wrapper is introduced which wraps a Svelte component constructor and returns a HTML element constructor. This has a couple of advantages:

- component can be used both as a custom element as well as a regular component. This allows creating one wrapper custom element and using regular Svelte components inside. Fixes #3594, fixes #3128, fixes #4274, fixes #5486, fixes #3422, fixes #2969, helps with sveltejs/kit#4502
- all components are compiled with injected styles (inlined through Javascript), fixes #4274
- the wrapper instantiates the component in `connectedCallback` and disconnects it in `disconnectedCallback` (but only after one tick, because this could be a element move). Mount/destroy works as expected inside, fixes #5989, fixes #8191
- the wrapper forwards `addEventListener` calls to `component.$on`, which allows to listen to custom events, fixes #3119, closes #4142 
- some things are hard to auto-configure, like attribute hyphen preferences or whether or not setting a property should reflect back to the attribute. This is why `<svelte:options customElement={..}>` can also take an object to modify such aspects. This option allows to specify whether setting a prop should be reflected back to the attribute (default `false`), what to use when converting the property to the attribute value and vice versa (through `type`, default `String`, or when `export let prop = false` then `Boolean`), and what the corresponding attribute for the property is (`attribute`, default lowercased prop name). These options are heavily inspired by lit: https://lit.dev/docs/components/properties. Closes #7638, fixes #5705
- adds a `shadowdom` option to control whether or not encapsulate the custom element. Closes #4330, closes #1748 

Breaking changes:
- Wrapped Svelte component now stays as a regular Svelte component (invokeing it like before with `new Component({ target: ..})` won't create a custom element). Its custom element constructor is now a static property named `element` on the class (`Component.element`) and should be regularly invoked through setting it in the html.
- The timing of mount/destroy/update is different. Mount/destroy/updating a prop all happen after a tick, so `shadowRoot.innerHTML` won't immediately reflect the change (Lit does this too). If you rely on it, you need to await a promise
@dummdidumm
Copy link
Member

Closed via #8457, to be released in Svelte 4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.