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

Guide to generating an array entry for every line of a text file in node.js

Struggling with converting each line of a text file into an array entry in node.js The array I am working with is named "temp." The code below successfully prints out each line: var temp = []; const readline = require('readline'); const fs = re ...

ng-repeat not functioning properly when using track by, filter, and orderBy

I have come across this code snippet. http://jsfiddle.net/0tgL7u6e/ Scripting in JavaScript var myApp = angular.module('myApp',[]); function MyCtrl($scope) { $scope.nameFilter = ''; $scope.contacts = [ {name: 'G ...

Receiving error messages about missing images in my React project

I am new to programming and I have encountered an issue while running my React project. When I use the command npm start, I noticed that some image resources are not being packaged properly, resulting in certain images disappearing when the website is run ...

Identifying browsers with Zend Framework versus JavaScript

Currently, I am working on developing an application that demands the capability to upload large files. After much consideration, I have opted to utilize the FormData object as it allows me to provide progress updates to the user. Sadly, Internet Explorer ...

What is the best way to consistently position a particular row at the bottom of the table in AG-grid?

I have successfully implemented a table using AG-grid. Here is the code snippet: .HTML <ag-grid-angular #agGrid style="width: 100%; height: 100%; font-size: 12px;" class="ag-theme-alpine" [rowData]=&quo ...

Install the npm package if there have been modifications to the package.json file

In short: Can we make npm install run automatically before executing any npm script if the package.json file has been modified? Situation Summary Imagine you switch to a branch that has updated the package.json file. You try running npm run my-script, bu ...

What is the best way to retrieve the results of an indexedDb request beyond the limitations of its callback function?

I am working on a form that has an input box which I want to auto-complete with values from an IndexedDb objectStore. Currently, it is functioning with two overlapping input boxes, using a simple array. However, I am looking to make it work with the values ...

Tips for effectively organizing a collapsible list

Here is a list that I have: <ul> <li><span class="Collapsable">item 1</span> <ul> <li><span class="Collapsable">item 1.1</span></li> </ul> </ul> I am looking to create ...

By default, the HTML table will highlight the specific column based on the current month using either AngularJS or JavaScript upon loading

I am working with a table of 10 products and their monthly sales data. Using Angular JS, I am looking to highlight the entire column based on the current month and year. Additionally, we will also be incorporating future expected sales data into the table. ...

Testing form submission in React with React Testing Library and checking if prop is called

Feel free to check out the issue in action using this codesandbox link: https://codesandbox.io/s/l5835jo1rm To illustrate, I've created a component that fetches a random image every time a button is clicked within the form: import { Button } from " ...

Conflicting Angular components: Sorting tables and dragging/dropping table rows

In the current project I'm working on, I've integrated both angular table-sort and angular drag-drop. However, I ran into an issue where dragging a row and attempting to drop it onto another row causes the table sort to forcefully rearrange the r ...

modifying a record in MongoDB with the help of Mongoose

I need help with updating a collection in MongoDB using Mongoose. function check (db) { var hours = 60 * 60 * 1000 var threeHours = 3 * hours; var lastUpdated = null; db.collection("profile").find({userName: "Rick"}).toArray(function(er ...

Tips for efficiently proxying URL calls in Vue.js during production

During development, I have my local Vue.js project and a development server. To ensure proper routing for API calls made with Axios to the dev server instead of the Vue URL, I followed this helpful guide: When deploying to production, my Vue build package ...

Generating a component and rendering it according to the dynamic route parameters using mapStateToProps and reselect techniques

I have set up a global app container to store data for different rooms, with a sub-container called roomDetails that utilizes a reselect selector to pick a room from the global state based on ownProps.params.slug. This process is accomplished through mapSt ...

Leverage ESlint for optimal code quality in your expressjs

Is there a way to use ESlint with Express while maintaining the no-unused-vars rule? After enabling ESlint, I am encountering the following issue: https://i.stack.imgur.com/7841z.png I am interested in disabling the no-unused-vars rule exclusively for e ...

What is the best way to load the nested array attributes in an HTML table dynamically with AngularJS?

I attempted the following code, but I am only able to access values for cardno and cardtype. Why can't I access the others? Any suggestions? <tr ng-repeat="data in myData17.layouts"> <td ng-show="$index==1">{{data.name}}</td> &l ...

Refresh Twitter Bootstrap Tooltip after deactivating/removing

I have a quick question. I am currently dealing with data that is constantly changing and displayed from a selected item in a table. To monitor for overflow, I have implemented the following code: if (event.target.offsetWidth < event.target.scrollW ...

Challenges arise when using Bootstrap 4 for country selection

There have been reports that bootstrap 4 country select is not functioning properly. In an effort to troubleshoot, I delved into the documentation to find a solution. To make bootstrap-select compatible with bootstrap 4, refer to: Bootstrap 4 beta-2 ...

Navigating through the Express.js routes incorrectly

I currently have 3 different express.js routes set up: app.get('/packages/:name', (req, res) => {...}); app.get('/packages/search/', (req, res) => {...}); app.get('/packages/search/:name', (req, res) => {...}); At t ...

What is the best way to craft an if/else statement for a situation when a twig variable is undefined?

When utilizing twig to declare a variable called user: <script type="text/javascript> {% if user is defined %} var user = { example: {{ userjson | raw }} }; {% endif %} </script> If a user is not logged in, an error message a ...