Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vue3 Web Components events not working with custom wrapper

The WebComponents do work fine with this custom script but I have problems to. I want to be able to add event listener from outside to listen to the custom Vue events fired from within the component (in this example on click on the WC). Thus I created an event proxy but for some reason the listener is never triggered. Obviously I made a mistake somewhere. So I hope that someone can point me to the right solution.

I made a CodeSandbox for you to take a look and play around with the code.

(as CodeSandbox has some issues you'll need to click on the reload button on the right side of the layout in order to make it work)

//main.js
import { createApp } from "vue";
import App from "./App.vue";
import WebComponentService from "./services/wc";
WebComponentService.init();

createApp(App).mount("#app");

Vue Component used as source file for Web Component

//src/wcs/MyComp.vue
<template>
  <div class="m" @click="click">
    Hello My Comp {{ name }}
    <div v-for="(i, index) in values" :key="index">ID: {{ i.title }}</div>
  </div>
</template>

<script>
export default {
  __useShadowDom: false,
  emits: ["my-click"],
  // emits: ["my-click", "myclick"],
  props: {
    name: {
      default: "Test",
    },
    values: {
      type: Object,
      default: () => {
        return [{ title: "A" }, { title: "B" }];
      },
    },
  },
  methods: {
    click() {
      // emit the event
      this.$emit("my-click");
      console.log("EMIT");
    },
  },
};
</script>

<style lang="less" scoped>
.m {
  border: 5px solid red;
  margin: 10px;
  padding: 10px;
  border-radius: 5px;
}
</style>

Index.html where I test the web component and added an event listener

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
    <h3>WC Test here</h3>

    <div class="component-canvas">
      <!-- web component testing -->
      <xvue-my-comp
        id="vtest"
        name="MyComp HTML Rendering"
        :values='[{"title": "Element 1"}, {"title": "Element"}]'
        >Inner HTML Caption</xvue-my-comp
      >
    </div>

    <script>
      const mySelector = document.querySelector("xvue-my-comp");
      //listen to the event
      mySelector.addEventListener("my-click", function (ev) {
        console.log("@LISTEN");
        alert(ev);
      });
    </script>
  </body>
</html>

Custom WebComponents wrapper

import HTMLParsedElement from "html-parsed-element";
import { createApp, h, toHandlerKey } from "vue";
import { snakeCase, camelCase } from "lodash";

let registeredComponents = {};

const tagPrefix = "xvue-"; // Prefix for Custom Elements (HTML5)
const vueTagPrefix = "xvue-init-"; // Prefix for VUE Components - Must not be tagPrefix to avoid loops

// We use only one file as web component for simplicity
// and because codesandbox doesen't work with dynamic requires
const fileName = "MyComp.vue";
const webComponent = require(`../wcs/MyComp.vue`);

const componentName = snakeCase(
  camelCase(
    // Gets the file name regardless of folder depth
    fileName
      .split("/")
      .pop()
      .replace(/.ce/, "")
      .replace(/\.\w+$/, "")
  )
).replace(/_/, "-");
// store our component
registeredComponents[componentName] = {
  component: webComponent.default
};

export default {
  init() {
    // HTMLParsedElement is a Polyfil Library (NPM Package) to give a clean parsedCallback as the browser
    // default implementation has no defined callback when all props and innerHTML is available
    class VueCustomElement extends HTMLParsedElement {
      // eslint-disable-next-line
      constructor() {
        super();
      }

      parsedCallback() {
        console.log("Legacy Component Init", this);

        if (!this.getAttribute("_innerHTML")) {
          this.setAttribute("data-initialized", this.innerHTML);
        }

        let rootNode = this;
        let vueTagName = this.tagName
          .toLowerCase()
          .replace(tagPrefix, vueTagPrefix);
        let compBaseName = this.tagName.toLowerCase().replace(tagPrefix, "");
        let compConfig = registeredComponents[compBaseName];

        // Optional: Shadow DOM Mode. Can be used by settings __useShadowDom in component vue file
        if (compConfig.component.__useShadowDom) {
          rootNode = this.attachShadow({ mode: "open" });
          document
            .querySelectorAll('head link[rel=stylesheet][href*="core."]')
            .forEach((el) => {
              rootNode.appendChild(el.cloneNode(true));
            });
        }

        if (vueTagName) {
          // If we have no type we do nothing
          let appNode = document.createElement("div");
          // let appNode = rootNode;
          appNode.innerHTML +=
            "<" +
            vueTagName +
            ">" +
            this.getAttribute("data-initialized") +
            "</" +
            vueTagName +
            ">"; // @TODO: Some issues with multiple objects being created via innerHTML and slots
          rootNode.appendChild(appNode);

          // eslint-disable-next-line @typescript-eslint/no-this-alias
          const self = this;

          function createCustomEvent(name, args) {
            return new CustomEvent(name, {
              bubbles: false,
              cancelable: false,
              detail: args.length === 1 ? args[0] : args
            });
          }

          const createEventProxies = (eventNames) => {
            const eventProxies = {};
            if (eventNames) {
              console.log("eventNames", eventNames);
              // const handlerName = toHandlerKey(camelCase(name));
              eventNames.forEach((name) => {
                const handlerName = name;
                eventProxies[handlerName] = (...args) => {
                  this.dispatchEvent(createCustomEvent(name, args));
                };
              });
            }
            return eventProxies;
          };
          const eventProxies = createEventProxies(compConfig.component.emits);
          this._props = {};

          const app = createApp({
            render() {
              let props = Object.assign({}, self._props, eventProxies);
              // save our attributes as props
              [...self.attributes].forEach((attr) => {
                let newAttr = {};
                newAttr[attr.nodeName] = attr.nodeValue;
                props = Object.assign({}, props, newAttr);
              });
              console.log("props", props);
              delete props.dataVApp;
              return h(compConfig.component, props);
            },

            beforeCreate: function () {}
          });

          // Append only relevant VUE components for this tag
          app.component(vueTagPrefix + compBaseName, compConfig.component);

          this.vueObject = app.mount(appNode);
          console.log("appNode", app.config);
        }
      }

      disconnectedCallback() {
        if (this.vueObject) {
          this.vueObject.$destroy(); // Remove VUE Object
        }
      }

      adoptedCallback() {
        //console.log('Custom square element moved to new page.');
      }
    }

    // Register for all available component tags ---------------------------

    // Helper to Copy Classes as customElement.define requires separate Constructors
    function cloneClass(parent) {
      return class extends parent {};
    }

    for (const [name, component] of Object.entries(registeredComponents)) {
      customElements.define(tagPrefix + name, cloneClass(VueCustomElement));
    }
  }
};
like image 775
Hexodus Avatar asked Oct 26 '25 10:10

Hexodus


1 Answers

If I whack this into your render method, it all works fine.

Thus your problem is not Web Components or Events related

              document.addEventListener("foo", (evt) => {
                console.log("FOOd", evt.detail, evt.composedPath());
              });
              self.dispatchEvent(
                new CustomEvent("foo", {
                  bubbles: true,
                  composed: true,
                  cancelable: false,
                  detail: self
                })
              );

addendum after comment:

  • Your code has bubbles:false - that will never work
  • This code properly emits an Event
  • You need to look into how you trigger your proxy code
          const createCustomEvent = (name, args = []) => {
            return new CustomEvent(name, {
              bubbles: true,
              composed: true,
              cancelable: false,
              detail: !args.length ? self : args.length === 1 ? args[0] : args
            });
          };

          const createEventProxies = (eventNames) => {
            const eventProxies = {};
            if (eventNames) {
              eventNames.forEach((name) => {
                const handlerName =
                  "on" + name[0].toUpperCase() + name.substr(1).toLowerCase();
                eventProxies[handlerName] = (...args) => {
                  appNode.dispatchEvent(createCustomEvent(name));
                };
              });
              document.addEventListener("foo",evt=>console.warn("foo!",evt));
              appNode.dispatchEvent(createCustomEvent("foo"));
              console.log(
                "@eventProxies",
                eventProxies,
                self,
                "appnode:",
                appNode,
                "rootNode",
                rootNode
              );
            }
            return eventProxies;
          };
like image 176
Danny '365CSI' Engelman Avatar answered Oct 28 '25 22:10

Danny '365CSI' Engelman



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!