Reputation: 11
When using the following code to wrap a Vue 3 component into a custom element, I noticed that Vue events were not received by the caller.
import { createApp, defineCustomElement, getCurrentInstance, h } from "vue"
export const defineVueCustomElement = (component: any, { plugins = [] } = {}) =>
defineCustomElement({
styles: component.styles,
props: component.props,
emits: component.emits,
setup(props, { emit }) {
const app = createApp();
plugins.forEach((plugin) => {
app.use(plugin);
});
const inst = getCurrentInstance();
Object.assign(inst.appContext, app._context);
Object.assign(inst.provides, app._context.provides);
return () =>
h(component, {
...props,
});
},
})
But when I wrote a simpler code, Vue events can be received by the client correctly. The drawback of the code is that it doesn't support Vue plugins:
import { defineCustomElement } from "vue"
export const defineVueCustomElement = (component: any) => {
defineCustomElement(component)
}
I am wondering why the first piece of code was not working correctly? How should I correct it? Thanks!
Upvotes: 1
Views: 958
Reputation: 3428
Inspired by @herobrine I managed to get it to work with components defined as async (since we don't want to "load" all web components at once, instead load them on demand). So given
window.customElements.define(
`org-custom-element`,
defineCustomElement({
// lazy loaded component (chunk)
rootComponent: defineAsyncComponent(() => import('./MyComponent.ce.vue')),
globalStyles: [globalStyles],
plugins: {
install(app: App) {
app.use(store);
app.use(i18n);
},
},
})
);
we defined defineCustomElement
as
const extractEventNamesFromEmits = (
emits
) => {
if (Array.isArray(emits)) {
return emits;
}
if (typeof emits === 'object') {
return Object.keys(emits);
}
return [];
};
export const defineCustomElement = ({
rootComponent,
plugins,
globalStyles = []
}) =>
vueDefineCustomElement({
props: rootComponent.props,
styles: globalStyles,
setup(props, ctx) {
const app = createApp()
app.component("org-cpm-root", rootComponent)
app.use(plugins)
const inst = getCurrentInstance()
Object.assign(inst.appContext, app._context)
Object.assign(inst.provides, app._context.provides)
rootComponent.setup?.(props, ctx)
// important
const isAsyncComponent = rootComponent.name === "AsyncComponentWrapper"
const onVnodeMounted = isAsyncComponent
? vnode => {
asyncVnode.value = vnode
}
: undefined
const asyncVnode = ref(null)
const events = computed(() => {
const asyncComponentEmits =
isAsyncComponent &&
typeof asyncVnode.value?.type === "object" &&
"emits" in asyncVnode.value.type
? asyncVnode.value.type.emits
: null
return Object.fromEntries(
(
extractEventNamesFromEmits(
asyncComponentEmits ?? rootComponent.emits
) ?? []
).map(eventName => {
return [
`on${eventName[0].toUpperCase()}${eventName.slice(1)}`,
payload => ctx.emit(eventName, payload)
]
})
)
})
return () =>
h(rootComponent, {
...props,
...events.value,
onVnodeMounted
})
}
})
Basically if we detect that component name is AsyncComponentWrapper
then we will listen on underlying VNode being mounted. When it has mounted we will re-render component with events extracted from this VNode emits
option. It may not work in all cases, but it works in our case and allows the event to be properly "catched" and dispatched as Custom DOM events.
Upvotes: 0
Reputation: 3183
After digging HOURS into this, I managed to get it to work. Here's your code updated:
export const defineVueCustomElement = (component: any, { plugins = [] } = {}) =>
defineCustomElement({
styles: component.styles,
props: component.props,
emits: component.emits,
setup(props, { emit }) {
const app = createApp();
plugins.forEach((plugin) => {
app.use(plugin);
});
const inst = getCurrentInstance();
Object.assign(inst.appContext, app._context);
Object.assign(inst.provides, app._context.provides);
const events = Object.fromEntries(
(component.emits || []).map((event: string) => {
return [
`on${ event[0].toUpperCase() }${ event.slice(1) }`,
(payload: unknown) => emit(event, payload)
];
})
);
return () =>
h(component, {
...props,
...events
});
},
})
Upvotes: 2