What is the process for creating a custom Vue 3 element with incorporating styles for child components?

After experimenting with Vue's defineCustomElement() to develop a custom element, I encountered an issue where the child component styles were not being included in the shadow root for some unknown reason.

To address this problem, I took a different approach by manually creating my shadow root using the native Element.attachShadow() API instead of relying on defineCustomElement(). However, this alternative method led to none of the styles being loaded at all:

Snippet: 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);

Snippet 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;
      });
  },
};

Snippet 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"
  ]
}

Any suggestions on how to properly create a custom element with all its styles within the shadow root?

Answer №1

In Vue 3, the Vue config mentioned is no longer required. It was specifically used by the development server in Vue 2 to render styles within custom elements.

The recommended approach for registering custom elements now is to use defineCustomElement(). However, there is an ongoing issue when using this method where styles of nested components are not rendered at all (@vuejs/vue-next#4462).

A workaround involves importing all components as custom elements so that styles are attached to the component definition instead of being appended to the <head>. These styles can then be inserted into the DOM during mounting:

  • To enable vue-loader's customElement mode in your vue.config.js:

    // vue.config.js
    module.exports = {
      chainWebpack: config => {
        config.module
          .rule('vue')
          .use('vue-loader')
          .tap(options => {
            options.customElement = true
            return options
          })
      }
    }
    

    Alternatively, you can rename all component file extensions from .vue to .ce.vue.

  • Create a utility function that wraps Vue's defineCustomElement() and performs the following steps within a setup():

    1. Set up a temporary application instance with a mixin for the mounted and unmounted lifecycle hooks.
    2. In the mounted hook, insert the component's own styles from this.$.type.styles into the DOM using a <style> tag. Repeat this process for component definitions from this.$options.components map.
    3. In the unmounted hook, remove the <style> tag added during the mounted phase.
    4. Copy the temporary application instance's _context to the current application context obtained through getCurrentInstance().
    5. Return a render function for the component.
    // defineCustomElementWithStyles.js
    import { defineCustomElement as VueDefineCustomElement, h, createApp, getCurrentInstance } from 'vue'
    
    const nearestElement = (el) => {
      while (el?.nodeType !== 1 /* ELEMENT */) el = el.parentElement
      return el
    }
    
    export const defineCustomElement = (component) =>
      VueDefineCustomElement({
        setup() {
          const app = createApp()
          // Code block here
          return () => h(component)
        },
      })
    
  • Edit the public/index.html file to replace the <div id="app"> with a custom element named "my-custom-element":

    Before:

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

    After:

    // public/index.html
    <body>
      <my-custom-element></my-custom-element>
    </body>
    
  • Instead of using createApp(), utilize the defineCustomElement() function mentioned earlier to create a custom element for your app:

    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.vue'
    customElements.define('my-custom-element', defineCustomElement(App))
    

Demo link

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Removing item from Angular service

Within my Angular 2 application, I have created a service called events.service.ts: const EVENTS = { 1512205360: { event: 'foo' }, 1511208360: { event: 'bar' } } @Injectable() export class EventsService { getEvents() ...

What are the steps to switch the dropdown values?

I am facing an issue with swapping values in two dropdowns. The scenario is that I have a dropdown with minimal values and another one with maximum values. If a value selected in the first dropdown is greater than the value in the second dropdown, they nee ...

Vue encountered an invalid value for the dynamic directive argument, which was expected to be a string or null, but

Hello, I am trying to use a dynamic argument for a directive in my HTML code: <div id="app5"> <p>{{message}}</p> <button v-on:[eventName]="reverseMessage">Reverse Message</button> </div> Here is my Vue instance ...

Instructions on utilizing type interfaces for prop drilling in my React Typescript counter

I am currently developing a basic counter app to monitor my progress in a digital card game that I enjoy playing. While attempting to pass props from the parent component to the child component, I encountered an issue where the props were not being success ...

Efficiently transferring a style property to a child component as a computed property in Vue.js

Currently, I am facing an issue that involves too much logic in my inline style, which I would like to move inside a computed property. While I understand that this is the correct approach, I am unsure of how to implement it. To provide a clearer understa ...

Selecting radio button does not update corresponding label

I am having an issue with setting a radio button as checked. In the example snippet, it works perfectly fine but on my localhost, it is not working. Even though the input gets checked, the label does not change. Surprisingly, if I set another radio button ...

Prevent users from selecting the same item for multiple dropdowns

I am currently working on creating an OTC trading interface as well as a request for quote interface using Vue.js. My main objective is to prevent users from selecting the same item for two different select inputs. First select input: <v-select v-mod ...

Checking for duplicate entries in an array created with the Angular form builder

I am currently utilizing angular6 reactive form with form builder and form array. The issue I am encountering is duplicate subject entries from the drop down in the form array. How can I implement validation to prevent duplicate entries in the form array? ...

Move the cursor over the text to reveal an image

Hello, I'm trying to replicate the same animation effect as seen on these websites: and . Specifically, when hovering over the "selected works" section, an image is displayed. I suspect it's using a JavaScript library, but I can't seem to i ...

Validating properties of a class using Typescript's Class-Validator

I tried using the class-validator decorator library for validation processes on my sample project. However, it doesn't seem to be working as expected. The sample project aims to create projects based on user inputs, and I'm attempting to validate ...

Embracing the Unknown: Exploring Wildcard Values

I have a code snippet below that has a wildcard * in it. I'm looking for suggestions on how to make the * accept any number. Any thoughts on this? $('body').on('click', '.custom_295_*-row', function(){ var href = "htt ...

Utilizing Local Storage in Vuex Store with Vue.js

I have been working with localStorage for storing and retrieving items in my JavaScript code housed within a .vue file. However, I am now looking to find a way to transfer this stored data into my Vuex store, specifically within the mutations section locat ...

What is the best way to retrieve the value of a property within a JavaScript object?

I am facing an issue with retrieving the value of the status property from an object in my code. Below is a snippet of what I have tried: console.log("Resource.query()"); console.log(Resource.query()); console.log("Resource.query().status"); console.log(R ...

Simple HTML and CSS exercise for beginners to grasp the concept

I'm looking to modify the layout of a specific webpage at based on the following instructions: First, insert this link within the <head> <link rel="stylesheet" href="https://trafficbalance.io/static/css/sdk/sdk.css"> -next, add th ...

Leveraging AJAX within a RESTful API in a Node.js environment to retrieve a JSON file and dynamically parse its contents according to the specific button selected on the front-end interface

Can anyone help me with understanding the communication process between server.js (Node.js) and the front-end JavaScript file? I am trying to implement AJAX as a RESTful API in the server to retrieve a JSON file, parse it based on specific button clicks in ...

Numerous entities in motion, each adorned with its own accompanying text

I have been experimenting with the interactive cubes and recently implemented a click function that directs to specific links. Now, I am interested in assigning distinct text to each cube as a material when rendered. Currently, I am using a single materi ...

React-Bootstrap Popup encounters overlay failure

While using the Tooltip without an OverlayTrigger, I encountered the following error: webpack-internal:///133:33 Warning: Failed prop type: The prop overlay is marked as required in Tooltip, but its value is undefined. The code snippet causing the issu ...

When properties remain unchanged, they do not hold the same value in a Firestore-triggered Cloud Function

Within my Firestore database, there is a collection named events consisting of documents with attributes such as begin, end, and title. The function in question is triggered when any changes occur within a document. The begin and end fields are both categ ...

The compatibility of Datatables responsive feature with ajax calls appears to be limited

I recently started using the datatables plugin and have encountered an issue with responsive tables. While I successfully implemented a responsive table and an AJAX call on a previous page, I am facing difficulties with it on a new page for unknown reasons ...

Manipulating DropDownList Attributes in ASP.NET using JavaScript

I am facing an issue with populating a Dropdownlist control on my ASCX page. <asp:DropDownList ID="demoddl" runat="server" onchange="apply(this.options[this.selectedIndex].value,event)" onclick="borderColorChange(this.id, 'Click')" onblur="bo ...