bitovi / react-to-web-component

Convert react components to native Web Components. Works with Preact too!
https://www.bitovi.com/open-source/react-to-web-component
MIT License
673 stars 41 forks source link

Embedding styles in shadow DOM #172

Open seesharper opened 6 months ago

seesharper commented 6 months ago

When we pass shadow : 'open' or shadow : 'closed' we would probably expect the styles to be included in the shadow root. Is there any way to accomplish this?

.welcome {
    color: red;
}

React component

import './Welcome.css';

export function Welcome(props: { name: string }) {
    return <div>
        <h1 className="welcome">Welcome, {props.name}!</h1>
        <slot></slot>
    </div>
}

export default Welcome;

To create the web component

const WelcomeWC = r2wc(Welcome, {
  props: {
    name: "string"
  },
  shadow: "open"
});

customElements.define("welcome-wc", WelcomeWC);

Usage

<welcome-wc>
  </welcome-wc>
se-andbjo commented 6 months ago

I'm wondering the same, no styles seems to be included in the shadow dom

danielegarciav commented 4 months ago

First of all, this would entirely depend on how your bundler/build tool deals with CSS imports, and the constraints you have in your project. With that said, you can make it work, but at the moment it looks like you must roll your own solution.

In my situation, I am using esbuild directly, and one of my constraints is that I must end up with a single JS file as output from the bundler. I found an excellent plugin for the Bun bundler called bun-lightningcss. I use esbuild, but it was dead simple to modify it so that it works with esbuild, since Bun's API is almost identical.

Without modifications, the bun-lightningcss plugin does a couple things:

After modifying the plugin so that it worked with esbuild, all I had to do was to extend the custom element class returned by r2wc so that it cloned the <style> tag from the host document and appended it into the shadow DOM whenever the custom element was constructed. If you see the source code for r2wc, you can see you can get it with this.container:

class MyCustomElement extends r2wc(MyComponent) {
  constructor() {
    super();
    this.container.append(document.getElementById('bun_lightningcss').cloneNode(true));
  }
}

It works like a charm and fits my use case perfectly. But you might have to find a way to do this with the tools you use if the solution doesn't exist yet.

shogoroy commented 1 month ago

Almost same as https://github.com/bitovi/react-to-web-component/issues/172#issuecomment-2075776780, In case of mine, below example is worked fine.

My usecase:

Example

  1. Add id to injected style tag
    
    import { defineConfig } from "vite";
    import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";

export default defineConfig({ plugins: [ react(), cssInjectedByJsPlugin({ styleId: "" }), // 1. add id to style tag ],

// other config here });


2. Wrap r2wc

export const convertReact2WebComponent = ( Component: Parameters[0], options?: Parameters[1], ) => { const WebComponent = r2wc(Component, options);

class WebComponentWithStyle extends WebComponent { connectedCallback() { // 2. Use connectedCallback instead of constructor (this can be changed by your usecase.) const styleTag = document.getElementById(""); if (styleTag) { this.shadowRoot?.append(styleTag.cloneNode(true)); } } }

return WebComponentWithStyle; };


3. Define Web component using wrapped r2wc

const webComponent = convertReact2WebComponent(Component, { props, shadow: "open", }); customElements.define(name, webComponent);



I hope this helps.
wataruoguchi commented 1 week ago

Similar to ^but scoped example:

https://github.com/wataruoguchi/poc-spa-gh-pages/blob/main/packages/react-tailwind-fragment/src/index.tsx

import r2wc from "@r2wc/react-to-web-component";
import App from "./App";

class StyledHelloWC extends r2wc(App, {
  props: { name: "string" },
  shadow: "open",
}) {
  connectedCallback() {
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    super.connectedCallback();
    // window.__styles is injected by vite-plugin-css-injected-by-js
    if (window.__styles) {
      const template = document.createElement("template");
      template.innerHTML = `<style id="vite-plugin-css-injected-by-js">${window.__styles}</style>`;
      this.shadowRoot?.appendChild(template.content.cloneNode(true));
    }
  }
}

customElements.define("hello-tailwind-wc", StyledHelloWC);

And in [vite.config.ts](https://github.com/wataruoguchi/poc-spa-gh-pages/blob/main/packages/react-tailwind-fragment/vite.config.ts) I have

plugins: [
        ...defaultConfig.plugins,
        cssInjectedByJsPlugin({
          injectCode: (cssCode: string) => {
            return `window.__styles = ${cssCode}`;
          },
        }),
      ],