Home > Mobile >  How do I create a Vue 3 custom element, including child component styles?
How do I create a Vue 3 custom element, including child component styles?

Time:11-02

I tried Vue's defineCustomElement() to create a custom element, but the child component styles are not included in the shadow root for some reason.

I then tried to manually create my shadow root using the native Element.attachShadow() API instead of using defineCustomElement() (based on a Codesandbox), but then no styles were loaded at all:

Code: main.js:

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

let treeHead = document.querySelector("#app");
let holder = document.createElement("div");
let shadow = treeHead.attachShadow({ mode: "open" });
shadow.appendChild(holder);

createApp(App).use(store).use(router).mount(holder);

Code vue.config.js:

module.exports = {
  chainWebpack: (config) => {
    config.module
      .rule("vue")
      .use("vue-loader")
      .loader("vue-loader")
      .tap((options) => {
        options.shadowMode = true;
        return options;
      });
    config.module
      .rule("css")
      .oneOf("vue-modules")
      .use("vue-style-loader")
      .tap((options) => {
        options.shadowMode = true;
        return options;
      });
    config.module
      .rule("css")
      .oneOf("vue")
      .use("vue-style-loader")
      .tap((options) => {
        options.shadowMode = true;
        return options;
      });
  },
};

Code package.json:

{
  "name": "shadow-root",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build"
  },
  "dependencies": {
    "vue": "^3.2.20",
    "vue-loader": "^16.8.2",
    "vue-router": "^4.0.0-0",
    "vue-style-loader": "^4.1.3",
    "vuex": "^4.0.0-0"
  },
  "devDependencies": {
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "node-sass": "^4.12.0",
    "sass-loader": "^8.0.2"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

How I can create a custom element with all its styles in the shadow root?

CodePudding user response:

That Vue config is not necessary in Vue 3. It was only needed by the dev server in Vue 2 to render the styles in custom elements.

Vue 3.2 added defineCustomElement(), which can be used to define custom elements. However, there's an open issue when using defineCustomElement() (@vuejs/vue-next#4462), where nested component styles are not rendered at all. A workaround is to copy the child component styles into the root component's shadow root, as seen in this utility function (which we'll use later below):

// defineCustomElementWithStyles.js
import { defineCustomElement as VueDefineCustomElement, h } from 'vue'

const getStylesRecursively = (component) => {
  const customElementStyles = []

  if (component.styles) {
    customElementStyles.push(...component.styles)
  }

  const childComponents = component.components
  if (childComponents) {
    Object.keys(childComponents).forEach((name) => {
      const styles = getStylesRecursively(childComponents[name])
      customElementStyles.push(...styles)
    })
  }

  return customElementStyles
}

export const defineCustomElement = (component) =>
  VueDefineCustomElement({
    styles: getStylesRecursively(component),
    render: () => h(component),
  })

Edit public/index.html to replace the <div id="app"> with a custom element (e.g., named "app-root"):

Before:

// public/index.html
<body>
  <div id="app"></div>
</body>

After:

// public/index.html
<body>
  <app-root id="app"></app-root>
</body>

Instead of createApp(), use defineCustomElement() to create a custom element of your app. Also rename .vue files to .ce.vue so that their styles are copied into the shadow root.

Before:

// main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

After:

// main.js
import { defineCustomElement } from './defineCustomElementWithStyles'
import App from './App.ce.vue'
customElements.define('app-root', defineCustomElement(App))

demo

  • Related