Lakeeast
Lakeeast

Reputation: 11

Unable to emit events when wrapping a Vue 3 component into a custom element

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

Answers (2)

Maksym
Maksym

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

Herobrine
Herobrine

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

Related Questions