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

Ability to append style/script elements inside of a shadow-root #11855

Open
4 tasks done
seahindeniz opened this issue Jan 30, 2023 · 29 comments
Open
4 tasks done

Ability to append style/script elements inside of a shadow-root #11855

seahindeniz opened this issue Jan 30, 2023 · 29 comments
Labels
feat: css p2-to-be-discussed Enhancement under consideration (priority)

Comments

@seahindeniz
Copy link

seahindeniz commented Jan 30, 2023

Description

Vue started to support mounting app inside of a shadow-root element within this PR.
With the following code, I can mount my Vue app under a shadow-root node

import App from './App.vue';

const shadowHost = document.querySelector('#my-host');
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });

createApp(App).mount(shadowRoot);

However, Vite loads style/script elements under the document head element because of the following function and since style elements are getting appended outside of the shadow-root scope, style definitions are not getting applied to HTML elements under the shadow-root.

function preload(
baseModule: () => Promise<{}>,
deps?: string[],
importerUrl?: string,
) {
// @ts-expect-error __VITE_IS_MODERN__ will be replaced with boolean later
if (!__VITE_IS_MODERN__ || !deps || deps.length === 0) {
return baseModule()
}
const links = document.getElementsByTagName('link')
return Promise.all(
deps.map((dep) => {
// @ts-expect-error assetsURL is declared before preload.toString()
dep = assetsURL(dep, importerUrl)
if (dep in seen) return
seen[dep] = true
const isCss = dep.endsWith('.css')
const cssSelector = isCss ? '[rel="stylesheet"]' : ''
const isBaseRelative = !!importerUrl
// check if the file is already preloaded by SSR markup
if (isBaseRelative) {
// When isBaseRelative is true then we have `importerUrl` and `dep` is
// already converted to an absolute URL by the `assetsURL` function
for (let i = links.length - 1; i >= 0; i--) {
const link = links[i]
// The `links[i].href` is an absolute URL thanks to browser doing the work
// for us. See https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:idl-domstring-5
if (link.href === dep && (!isCss || link.rel === 'stylesheet')) {
return
}
}
} else if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) {
return
}
const link = document.createElement('link')
link.rel = isCss ? 'stylesheet' : scriptRel
if (!isCss) {
link.as = 'script'
link.crossOrigin = ''
}
link.href = dep
document.head.appendChild(link)
if (isCss) {
return new Promise((res, rej) => {
link.addEventListener('load', res)
link.addEventListener('error', () =>
rej(new Error(`Unable to preload CSS for ${dep}`)),
)
})
}
}),
).then(() => baseModule())
}

As a use case, I need to mount a multiple apps to DOM under a shadow-root to avoid scope related collisions.

Suggested solution

Mentioned function can support different parent element to append style/script elements

Alternative

No response

Additional context

No response

Validations

@IamFive
Copy link

IamFive commented Apr 19, 2023

is this feature in plan?

@github-project-automation github-project-automation bot moved this to Discussing in Team Board Apr 19, 2023
@patak-dev
Copy link
Member

There is an open PR that should address this issue #12206. I added it with higher priority to discuss the addition of a new option. Let's use this issue to track the feature.

@patak-dev patak-dev removed this from Team Board Apr 19, 2023
@bluwy bluwy added the has pr label Apr 26, 2023
@bluwy bluwy added the feat: css label Jun 6, 2023
@bluwy bluwy added this to Team Board Jun 6, 2023
@github-project-automation github-project-automation bot moved this to Discussing in Team Board Jun 6, 2023
@bluwy bluwy moved this from Discussing to P2 - 3 in Team Board Jun 6, 2023
@RogierdeRuijter
Copy link

RogierdeRuijter commented Sep 4, 2023

This is the workaround I used.

Run this code in the connectedCallback:

const styleTag = document.createElement("style");
styleTag.innerHTML = <link-to-css>;
this.shadowRoot.appendChild(styleTag);

@Oumar96
Copy link

Oumar96 commented Sep 7, 2023

This is the workaround I used.

@RogierdeRuijter Can you expand on what you mean by the connectedCallback? Is the connectedCallback part of Vue? In my use case I'm not using web components but rather, I am mounting my app inside a shadow DOM to use that as a micro front end.

@Oumar96
Copy link

Oumar96 commented Sep 7, 2023

@seahindeniz Do you have any updates on this issue? I feel that this is a crucial part of building Microfrontends with Vue. ShadowDOM is probably the best and least hacky way to encapsulate styles within micro-frontends.
Perhaps you know a workaround that is not too hacky? I feel that manually moving styles from the document head into the shadow DOM is brittle.

@seahindeniz
Copy link
Author

Hi @Oumar96, I totally agree with you and unfortunately no solution so far

@hood

This comment was marked as spam.

@gs-rpal

This comment was marked as spam.

@bradlocking

This comment was marked as spam.

@gs-rpal
Copy link

gs-rpal commented Oct 11, 2023

Screenshot 2023-10-11 at 10 55 27 PM try this

@Oumar96
Copy link

Oumar96 commented Oct 11, 2023

@gs-rpal Could you share the vite config file instead of the screenshot?

@arqex
Copy link

arqex commented Oct 13, 2023

+1, it would be great to have this option as some kind of vite's configuration or plugin.

@gs-rpal please share that config 🙏 ❤️

@trusktr
Copy link

trusktr commented Oct 13, 2023

The only workaround for this for now is to query the document for <link> or <style> elements, then add them to your ShadowRoot.

StylesSheets can be added to this.shadowRoot.adoptedStyleSheets in all modern browsers.

const styleElements = document.documentElement.querySelectorAll('..... select all the <link>s and <style>s you want ...')
const sheets = Array.from(styleElements).map(el => el.sheet)

// Apply the desired style sheets to your custom element (via the shadow root):
this.shadowRoot.adoptedStyleSheets = [...sheets]

If you cannot support adoptedStyleSheets because you support older browsers, then you can clone elements and place them in the root:

const styleElements = document.documentElement.querySelectorAll('..... select all the links and styles you want ...')
const styleClones = Array.from(styleElements).map(el => el.cloneNode(true))

// Append clones of the desired style elements to your custom element (via the shadow root):
this.shadowRoot.prepend(...styleClones)

Note

prepend() instead of append() so that they will be above your shadow roots existing styles, if any, so that your own root's styles override the shared ones that you bring in.

@pascalvos
Copy link

pascalvos commented Oct 19, 2023

i would also like the element that are created to be not document.createElement but shadowroot.createElement... current way is copy whole internals to add a custom renderer in place... this would help with custom element registery where there is already a proposal for and a polyfill

vue is just not made for webcomponents.. could be oke for mfe stuff if it fixes lot of these issues there many more on like not syncing the attributes if you use a custom element as a root its not ready for most of the newer browser api's
and its not made for smaller webcomponents/custom elements.. there only thing is lit or fast something like this... this hacking with out real good support just makes is very brittle because you have to copy and rely on non exposed internals... that dont follow semver :) cause there internals... and hard to follow...

sorry for the rant .. vue just needs to give webcomponents some serious love to be atlease useable for mfe stuff there currently only properly suited for spa things

for singlar webcomponents since vue creates a vue app even if you make a button its about 800% slower then something like lit and 10 times more heavy on memory usage.

@hood
Copy link

hood commented Oct 23, 2023

vue just needs to give webcomponents some serious love

This is the vite repo tho.

@pascalvos
Copy link

yea true

@hood
Copy link

hood commented Nov 4, 2023

FYI I’ve created a plugin that does just what I was looking for. Please try it out and let’s see whether it fits your use cases too. I’m pretty sure it’s very specific to my specific usage scenarios, so it’ll not fit anyone’s needs, but nothing stops us from using it as a starting point for a more adaptable plugin.

Link here:
https://www.npmjs.com/package/vite-plugin-shadow-style

@Oumar96
Copy link

Oumar96 commented Nov 28, 2023

Screenshot 2023-10-11 at 10 55 27 PM try this

@gs-rpal Any updates? Could you share the config file?

@patak-dev patak-dev added p2-to-be-discussed Enhancement under consideration (priority) and removed enhancement: pending triage labels Feb 21, 2024
@patak-dev patak-dev moved this from P2 - 3 to Low Priority in Team Board Feb 21, 2024
@shtief
Copy link

shtief commented Apr 25, 2024

Hello, this feature would be really great to support injection into Shadow roots! Is there already a nice workaround for this?

@hood
Copy link

hood commented Apr 25, 2024

Hello, this feature would be really great to support injection into Shadow roots! Is there already a nice workaround for this?

Hi, probably my solution I linked above can help you with style injection into shadow roots: https://www.npmjs.com/package/vite-plugin-shadow-style

@shtief
Copy link

shtief commented Apr 30, 2024

Hello @hood thanks for your comment, but somehow for the dev mode it deactivates the shadow root and for build mode it gives me the warning:

rendering chunks (141)...[vite-plugin-shadow-style] Multiple CSS files found in the output bundle. This plugin currently only supports handling a single css output.

A question to the "vite" team: is it possible to add a feature where it is possible to configure where to inject the styles? Right now only "head" is possible...
The code for it is here:

link.href = dep
if (cspNonce) {
link.setAttribute('nonce', cspNonce)
}
document.head.appendChild(link)
if (isCss) {
return new Promise((res, rej) => {
link.addEventListener('load', res)
link.addEventListener('error', () =>
rej(new Error(`Unable to preload CSS for ${dep}`)),
)
})
}

@shtief
Copy link

shtief commented Apr 30, 2024

I also tried the solution from @gs-rpal but it only seems to work for dev mode.

@YKalashnikov
Copy link

Hello guys, is there any progress for this feature? Thank you 🙏
cc: @bluwy

@66696e656c696665
Copy link

66696e656c696665 commented May 20, 2024

Screenshot 2023-10-11 at 10 55 27 PM try this

worked for me! But need create Shadow Dom in Vue and after this code put in ShadowRoot your css code
create shadowdom main.js:

import vue from 'vue';
import MyComponent from 'src/MyComponent';

let treeHead = document.querySelector(".container");
let holder   = document.createElement("div");
let shadow   = treeHead.attachShadow({mode: 'open'});
shadow.appendChild(holder);

let app = new Vue({
  el: holder,
  render: h => h(MyComponent, {})
});

@jayswo
Copy link

jayswo commented Jul 19, 2024

I would need this feature so bad for my current project!
A custom target, where the Styles should be inserted would be so great - with it we would be totally flexible!
Is there any progress on this feature? 🙏🙏🙏🙏

@scottsmith-1
Copy link

Something else to consider is SSR Declarative Shadow DOM. Link tags will be hoisted to the head ignoring the template and waiting for client code to append the tags results in FOUC.

@Ahn1
Copy link

Ahn1 commented Oct 10, 2024

Is there any updates on this? I ended up importing the styles using 'style.css?inline' and adding them manually inside the shadow dom. But it feels quite hacked.

@dbalabka
Copy link

dbalabka commented Oct 13, 2024

FYI it is impossible to add fonts and icons to shadow dom via <style> tag because @font-face isn't supported there yet:
https://issues.chromium.org/issues/41085401

The workaround is to use <link> instead:
mdn/interactive-examples#887 (comment)

@kuus
Copy link

kuus commented Oct 19, 2024

my currently working solution (both with serve and build commands) is:

in vite.config.ts

import { defineConfig } from "vite";
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";

export function defineConfig(() => {
  const globalStylesIdentifier = "__myStylesIdentifier";

  return {
    plugins: [
      cssInjectedByJsPlugin({
        injectCode: (cssCode, _options) => {
          return (
            `try{` +
            `var s = document.createElement('style');` +
            `s.appendChild(document.createTextNode(${cssCode}));` +
            `window.${globalStylesIdentifier} = window.${globalStylesIdentifier} || []; window.${globalStylesIdentifier}.push(s);` +
            `}catch(e){` +
            `console.error('${globalStylesIdentifier}', e);` +
            `}`
          );
        },
        dev: {
          enableDev: true,
          removeStyleCodeFunction: function removeStyleCode(_id) {
            // The 'id' corresponds to the value of the 'data-vite-dev-id' attribute found on the style element. This attribute is visible even when the development mode of this plugin is not activated.
          },
        },
      })
    ]
  };
};

in app.ts or where in my application code I want to inject the styles

const myShadowDomRoot = myDiv.shadowRoot;

if (myShadowDomRoot) {
  const styles = window.__myStylesIdentifier || [];
  while (styles.length) {
    myShadowDomRoot.prepend(styles.pop() || "");
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat: css p2-to-be-discussed Enhancement under consideration (priority)
Projects
Status: Low Priority
Development

No branches or pull requests