Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inject CSS styles to shadow root via Webpack

I am attempting to modify the styles of web component that created with shadow root.

I see that the styles are added to a head tag but it has no effect on the shadow root as it's encapsulated.

What i need is to load the styles of all components and make them appear inisde the shadow root.

This is a part of creating the web component:

index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
import './tmp/mockComponent.css'; // This is the styling i wish to inject


let container: HTMLElement;

class AssetsWebComponent extends HTMLElement {

    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        const { shadowRoot } = this;
        container = document.createElement('div');
        shadowRoot.appendChild(container);
        ReactDOM.render(<App />, container);

    }
}

window.customElements.define('assets-component', AssetsWebComponent);

App.ts // Regular react component

import React from 'react';
import './App.css';
import { MockComponent } from './tmp/mockComponent'

export const App: React.FunctionComponent = (props) => {
    return (
        <MockComponent />
    );
};

webpack.config.ts

// @ts-ignore
const path = require('path');
const common = require('./webpack.common.ts');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = merge(common, {
    mode: 'development',
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },

    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    {
                        loader: 'style-loader',
                        options: {
                            insert: function (element) {
                                const parent = document.querySelector('assets-component');

                                ***This what i want, to inject inside the shadowRoot but it 
                                never steps inside since the shadowRoot not created yet***

                                if (parent.shadowRoot) {
                                    parent.shadowRoot.appendChild(element);
                                }
                                parent.appendChild(element);
                            },
                        },
                    },
                    'css-loader',
                ],
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'sass-loader',
                ],
            },
        ],
    },

    plugins: [
        new HtmlWebpackPlugin({
            template: './src/template.html',
        }),
    ],

    devtool: 'inline-source-map',
});

Since the MockComponent can have more components inside, I rely on Webpack to inject all styles to the shadow root. I am using style-loader to inject the styles but it's not working well.

What I am doing wrong or is there any alternative for this solution.

like image 522
albertR Avatar asked Mar 12 '20 17:03

albertR


3 Answers

It turns out all you need is 'css-loader', so you should delete 'style-loader' completely along with its options. So in webpack.config.ts :

 module: {
            rules: [
                {test:/\.css$/, use:'css-loader'}
            ]
        }

Then you want to import a style string we got from css-loader and use it in a style element that you append to the shadow root before your container. So in index.tsx:

......
import style from './tmp/mockComponent.css'; 
......
constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        const { shadowRoot } = this;
        container = document.createElement('div');

        styleTag = document.createElement('style');
        styleTag.innerHTML = style;
        shadowRoot.appendChild(styleTag);            

        shadowRoot.appendChild(container);
        ReactDOM.render(<App />, container);

    }
......

The reason 'style-loader' is giving you problems is that it's made to find where to inject the css style tag on its own in the final html file, but you already know where to put it in your shadowDOM via JS so you only need 'css-loader'.

like image 146
InvalidReferenceException Avatar answered Oct 23 '22 16:10

InvalidReferenceException


There is a way to use style-loader for that

It all comes down to order of execution. Your index.tsx gets exectued after style-loader::insert. But the shadow root has to exist before.

The easiest way to do this is to modify index.html.

Here is a full example:

./src/index.html

...
<body>
    <div id="root"></div>
    <script>
        <!-- This gets executed before any other script. -->
        document.querySelector("#root").attachShadow({ mode: 'open' })
    </script>
</body>
...

./webpack.config.js

var HtmlWebpackPlugin = require('html-webpack-plugin');

const cssRegex = /\.css$/i

const customStyleLoader = {
  loader: 'style-loader',
  options: {
    insert: function (linkTag) {
      const parent = document.querySelector('#root').shadowRoot
      parent.appendChild(linkTag)
    },
  },
}

module.exports = {
  module: {
    rules: [
      {
        test: cssRegex,
        use: [
          customStyleLoader,
          'css-loader'
        ],
      },
    ],
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
};

./src/index.js

import "./style.css";

const root = document.getElementById('root').shadowRoot
// Create a new app root inside the shadow DOM to avoid overwriting stuff (applies to React, too)
const appRoot = document.createElement("div")
appRoot.id = "app-root"
root.appendChild(appRoot)

appRoot.textContent = "This is my app!"

./src/style.css

#app-root {
    background: lightgreen;
}

After building and serving your app, you should see "This is my app" with green background, everything inside your shadow root.

like image 5
Josef Wittmann Avatar answered Oct 23 '22 15:10

Josef Wittmann


I'd like to add my solution to this problem. It's based on @josef-wittmann's idea, but allows to add the styles to any new instance of a custom element, instead of just one root element.

index.html

...
<body>
    <script src="main.js"></script>
    <my-custom-element></my-custom-element>
</body>
...

./webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          {
            loader: "style-loader",
            options: {
              insert: require.resolve("./src/util/style-loader.ts"),
            },
          },
          "css-loader",
        ],
      },
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        loader: "ts-loader",
        options: {
          configFile: "tsconfig.app.json",
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),
  ],
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
  },
};

./src/util/style-loader.ts

const styleTags: HTMLLinkElement[] = [];
export const getStyleTags = () => styleTags.map(node => node.cloneNode(true));

export default function (linkTag) {
    styleTags.push(linkTag);
}

./src/index.ts

import "./style.css";
import { styleTags } from "./util/style-loader";

customElements.define(
  "my-custom-element",
  class extends HTMLElement {
    async connectedCallback() {
      this.attachShadow({ mode: "open" });

      const myElement = document.createElement("div");
      myElement.innerHTML = "Hello";

      this.shadowRoot?.append(...styleTags, myElement );
    }
  }
);

./src/style.css

div {
    background: lightgreen;
}
like image 3
Florian Bachmann Avatar answered Oct 23 '22 16:10

Florian Bachmann