Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Embedding styles in shadow DOM #172

Open
seesharper opened this issue Feb 9, 2024 · 4 comments
Open

Embedding styles in shadow DOM #172

seesharper opened this issue Feb 9, 2024 · 4 comments

Comments

@seesharper
Copy link

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
Copy link

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

@danielegarciav
Copy link

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:

  • It creates a virtual module called inject-styles. It exports a function injectStyles that adds all CSS code passed onto it into a <style> tag appended to the document's head. It also adds an ID to this tag.
  • When a CSS file is imported, the target CSS file is transpiled using lightningcss, and a JS file is generated which calls injectStyles with the transpiled CSS at runtime. It also does some other stuff like set up the exported object for CSS modules.

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
Copy link

shogoroy commented Jul 30, 2024

Almost same as #172 (comment),
In case of mine, below example is worked fine.

My usecase:

  • Using vite
  • Using vite-plugin-css-injected-by-js to inject css in bundle file.
  • Use connectedCallback instead of cunstructor (in my case, constructor not working)

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: "<custom_id>" }), // 1. add id to style tag
  ],

  // other config here
});
  1. Wrap r2wc
export const convertReact2WebComponent = (
  Component: Parameters<typeof r2wc>[0],
  options?: Parameters<typeof r2wc>[1],
) => {
  const WebComponent = r2wc(Component, options);

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

  return WebComponentWithStyle;
};
  1. Define Web component using wrapped r2wc
const webComponent = convertReact2WebComponent(Component, {
  props,
  shadow: "open",
});
customElements.define(name, webComponent);

I hope this helps.

@wataruoguchi
Copy link

wataruoguchi commented Aug 20, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants