21

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.

albertR
  • 321
  • 1
  • 3
  • 6

5 Answers5

10

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'.

  • 1
    If you do need the 'style-loader' for other parts of your application, you could specify loaders in an import statement, prefixing it with "!!" in order to disable all configured loaders. More details in https://webpack.js.org/concepts/loaders/ – tseshevsky Jan 21 '21 at 16:49
5

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.

Josef Wittmann
  • 1,259
  • 9
  • 16
5

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;
}
Florian Bachmann
  • 532
  • 6
  • 14
  • This solution is so simple and yet works exactly as I need it to. Thanks for this answer! – Dan Jan 05 '23 at 14:32
1

Other solution is insert the styles directly:

import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
import styles from  './tmp/mockComponent.css'; //--> import the styles


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(<>
           <style>{styles}</style> {/* add styles*/}
           <App />
        </>, container);

    }
}
lissettdm
  • 12,267
  • 1
  • 18
  • 39
  • Got this error when applying your solution - "Objects are not valid as a React child (found: object with keys {}). If you meant to render a collection of children, use an array instead." – ParfectShot Jun 22 '21 at 11:39
  • @ParfectShot That is because you are not receiving styles as string, check your module bundle configuration...this import styles from './tmp/mockComponent.css should return a string... – lissettdm Jun 22 '21 at 13:11
  • Thanks for the clarification. I ended up creating a element(appended in shadow) and put styles inside that. That worked for me. – ParfectShot Jun 24 '21 at 16:15
1

I had to create a web component that could be placed inside a regular dom element or inside the shadow dom of an element. I was also using a lot of sass files which I had to transpile to CSS first.

I tried using @florian-bachmann solution but somehow require.resolve("./src/util/style-loader.ts") line was causing a problem as style-loader.ts file was not getting resolved, so I decided to use window object for storing style tags and appended it to document body or inside shadow dom depending on where my custom element is placed.

index.html

...
<body>
    <script src="main.js"></script>
    <div>
       #shadow-root(open)
       <my-custom-element></my-custom-element>
    </div>
</body>
...

webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/,
            },
            {
                test: /\.scss$/,
                use: [
                    {
                        loader: 'style-loader',
                        options: {
                            injectType: 'singletonStyleTag',
                            insert: function addToWindowObject(element) {
                                const _window = typeof window !== 'undefined' ? window : {};
                                if (!_window.myCustomElementStyles) {
                                    _window.myCustomElementStyles = [];
                                }
                                element.classList.add('my-custom-element-styles');
                                _window.myCustomElementStyles.push(element);
                            },
                        },
                    },
                    'css-loader',
                    {
                        loader: 'sass-loader',
                        options: {
                            sassOptions: {
                                outputStyle: 'compressed',
                            },
                        },
                    },
                ],
                exclude: /node_modules/,
            },
        ],
    },
    output: {
       filename: "main.js",
       path: path.resolve(__dirname, "dist"),
    },
    plugins: [...],
    ...
};

my-custom-element.ts

import './styles.scss';

export class MyCustomElement extends HTMLElement {
    constructor() {
       super();
    }

    connectedCallback() {
       const styletags = window['myCustomElementStyles'] as HTMLStyleElement[];
       const rootNode = this.getRootNode();
       if (rootNode instanceof ShadowRoot) {
          rootNode.append(...styletags.map((tag) => tag.cloneNode(true)));
       } else if (rootNode instanceof Document) {
          !document.getElementById('my-custom-element-styles') && document.head.append(...styletags);
       }
       ...
    }

    ...
    
}

customElements.define("my-custom-element", MyCustomElement);

style.scss

div {
    background: lightgreen;
}
abhishek khandait
  • 1,990
  • 2
  • 15
  • 18