svelte
svelte copied to clipboard
Events are not emitted from components compiled to a custom element
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');
});
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 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>
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.
try it
import App from './App.svelte';
App.prototype.addEventListener = function(...arg){
this.$on(arg[0],arg[1])
document.addEventListener.call(this,...arg)
}
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.
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.
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?
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)
Closed via #8457, to be released in Svelte 4