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

When importing component inside custom element, style is discarded #4662

Closed
gnuletik opened this issue Sep 23, 2021 · 87 comments
Closed

When importing component inside custom element, style is discarded #4662

gnuletik opened this issue Sep 23, 2021 · 87 comments
Labels
❗ p4-important Priority 4: this fixes bugs that violate documented behavior, or significantly improves perf. scope: custom elements

Comments

@gnuletik
Copy link

Version

3.2.14

Reproduction link

github.com

Steps to reproduce

git clone [email protected]:gnuletik/vue-ce-import-comp-style.git
cd vue-ce-import-comp-style
yarn run dev

Open browser

What is expected?

CSS of OtherComponent.vue should be applied.
The text "It should be blue" should be blue.

What is actually happening?

Style is not applied.


I tried renaming OtherComponent to OtherComponent.ce.vue, but the result is the same.

This is useful when writing a set of custom elements to have shared components between custom elements.

@tony19
Copy link
Contributor

tony19 commented Sep 27, 2021

Also encountered this issue. Here's a couple more repros:

@raffobaffo
Copy link

I also encountered it and just worked on a fix, will create a PR soon (my first one here 😨 )

@gnuletik
Copy link
Author

I just realized there is already an open PR about this : #4309

@raffobaffo
Copy link

I see 👁️ . My changes are very similar, just its more recursive, meaning also components at any level are interested.

raffobaffo added a commit to raffobaffo/vue-next that referenced this issue Sep 27, 2021
…ted components. Missing feature, store available styles to avoid dupes. close vuejs#4662
raffobaffo added a commit to raffobaffo/vue-next that referenced this issue Sep 28, 2021
…ted components. Missing feature, store available styles to avoid dupes. close vuejs#4662
@raffobaffo
Copy link

raffobaffo commented Sep 28, 2021

Ok, this should be safer now. This addition will add any child component style to the main parent, no matter how deeply nested it is

raffobaffo added a commit to raffobaffo/vue-next that referenced this issue Sep 28, 2021
…ted components. Missing feature, store available styles to avoid dupes. close vuejs#4662
@pawel-marciniak
Copy link

Anyone know if it will be merged? It blocks me from using Vue 3 :/ @raffobaffo What about third party nested components (e.g. imported from some package)? Will your solution also work for them?

@LinusBorg
Copy link
Member

We will solve this, it might take a few more days or weeks though.

@pawel-marciniak
Copy link

We will solve this, it might take a few more days or weeks though.

@LinusBorg Ok, thanks for quick reply, it's good to know that it will be solved at some point in the future :)

@raffobaffo
Copy link

@pawel-marciniak Yes, it is.
@LinusBorg Great to hear. Atm to overcome this problem, we are using an own extended version of the defineCustomElement that provides that hack I inserted in the pr.

@lucasantunes-ciandt
Copy link

@raffobaffo would you mind sharing how did you extend it? I tried it here by basically cloning /runtime-dom/src/apiCustomElement.ts with your changes but got a lot of syntax error from TS.

@raffobaffo
Copy link

@lucasantunes-ciandt sorry but not TS for me on this project. This is pretty much the hack I built:

import { defineCustomElement as rootDefineCustomElement } from 'vue'
[...]
export const getChildrenComponentsStyles = (component) => {
  let componentStyles = [];
  if (component.components) {
    componentStyles = Object.values(component.components).reduce(
      (
        aggregatedStyles,
        nestedComponent,
      ) => {
        if (nestedComponent?.components) {
          aggregatedStyles = [
            ...aggregatedStyles,
            ...getChildrenComponentsStyles(nestedComponent),
          ];
        }
        return nestedComponent.styles
          ? [...aggregatedStyles, ...nestedComponent.styles]
          : aggregatedStyles;
      }, [],
    );
  }
if (component.styles) {
    componentStyles.push(...component.styles);
  }

  return [...new Set(componentStyles)];
};

export const defineCustomElement = (component) => {

  // Attach children styles to main element
  // Should be removed once https://github.com/vuejs/vue-next/pull/4695
  // gets merged
  component.styles = getChildrenComponentsStyles(component);
  const cElement = rootDefineCustomElement(component);

  // Programmatically generate name for component tag
  const componentName = kebabize(component.name.replace(/\.[^/.]+$/, "") );
  // eslint-disable-next-line tree-shaking/no-side-effects-in-initialization
  customElements.define(componentName, cElement);

  // Here we are attaching a <ling ref="style" href="/style.css" ...
  // This is to have the css outside of the shadow dom
  // also available inside the shadow root without inlining them
  // The browser will automatically use the available declaration and won't
  // make multiple calls
  const componentShadowDom = document.querySelector(componentName)?.shadowRoot;
  if(componentShadowDom){
    const styleLink = document.createElement('link');
    styleLink.setAttribute('rel', 'stylesheet');
    styleLink.setAttribute('href', 'style.css');
    componentShadowDom.appendChild(styleLink);
  }
}

@lucasantunes-ciandt
Copy link

lucasantunes-ciandt commented Nov 11, 2021

@raffobaffo Thank you so much! I thought you were literally extending the whole file 😅

I tried this code here, but unfortunately it doesn't work. I suppose it expects the components are all custom elements, e.g. Component.ce.vue and ChildComponent.ce.vue.

When my components are named Component.vue they don't come with any styles prop, only when they're Component.ce.vue.

I reckon we'll need to wait for your PR to be merged.

@lucasantunes-ciandt
Copy link

lucasantunes-ciandt commented Nov 11, 2021

Oopsie, actually this works! Thank you @raffobaffo!!

I was a bit confused about the meaning of .ce.vue, now I realize the extension is only a Custom Element MODE and not actually declaring those components as custom elements. This means I really should have them as .ce.vue in order for this to work, even after this PR is merged.

So my two cents to the Issue Owner: OtherComponent.vue should be OtherComponent.ce.vue indeed.

@lucasantunes-ciandt
Copy link

lucasantunes-ciandt commented Nov 11, 2021

@raffobaffo I had only to change the injection a little bit, since I may have multiple instances of the same Custom Element:

document.querySelectorAll(customElementName).forEach((element) => {
  if (!element.shadowRoot) return;

  const styleLink = document.createElement('link');
  styleLink.setAttribute('rel', 'stylesheet');
  styleLink.setAttribute('href', 'style.css');

  element.shadowRoot.appendChild(styleLink);
});

@raffobaffo
Copy link

@lucasantunes-ciandt Nice find 👍 ! Still this block is not part of the PR, because this actually make sense to have it in a separate module that is loaded just when custom elements are needed. In my case I have a components library and I want to have 2 exports, one to be consumed by Vue applications and one for browsers/html.
This part would maybe make more sense in some Vite or VueCli plugin 🤔 .

@lucasantunes-ciandt
Copy link

@raffobaffo Exactly! In our case we're using Vue inside a CMS, so we'll be only exporting custom elements to use within it instead of creating a Vue app, so we'll definitely keep that part because we need some external styles and even scripts from the CMS to be injected into the elements.

@adamdehaven
Copy link

adamdehaven commented Nov 24, 2021

@raffobaffo would your solution also inject styles into the shadow DOM from child components imported within the defineCustomElement component that originate from external packages?

As an example, if I define and export a component with defineCustomElement, but within that element is a button component that is being imported from an external package (and is not created with defineCustomElement).

Also @lucasantunes-ciandt do you have a working example using the reproduction repo?

@raffobaffo
Copy link

raffobaffo commented Nov 27, 2021

@adamlewkowicz hard to answer. Depends from how the imported element (third party) is structured. Does it comes with is own inline styles? If yes, it should work, if not, it cant.

@adamdehaven
Copy link

adamdehaven commented Nov 27, 2021

@raffobaffo would you be willing to update your reproduction repo linked above with a new branch that utilizes your fix? I tried what you have but wasn't able to get it working for some reason. Would be much appreciated!

@dezmound
Copy link

dezmound commented Nov 29, 2021

As a workaround I've created component, which tracks Mutation in document.head and inline style tags (must includes comment /* VueCustomElementChildren */) into shadow dom in dev mode, for production it inlines link for stylesheets:

CustomElements.vue

<script setup lang="ts">
/***
 * @see https://github.com/vuejs/vue-next/issues/4662
 **/

import { computed } from "vue";
import { onMounted, onUnmounted, ref } from "@vue/runtime-core";

const props = defineProps<{
  cssHref?: string;
}>();

const inlineStyles = ref<HTMLElement[]>([]);
const html = computed(() =>
  // @ts-ignore
  import.meta.env.PROD && props.cssHref
    ? `<link href="${props.cssHref}" rel="stylesheet"/>`
    : `<style>${inlineStyles.value
        .map((style) => style.innerHTML)
        .join("")}</style>`
);

const findAndInsertStyle = () => {
  inlineStyles.value = Array.from(
    document.getElementsByTagName("style")
  ).filter((node) => {
    return node.innerHTML.includes("VueCustomElementChildren");
  });
};

// @ts-ignore
if (!import.meta.env.PROD) {
  const observer = new MutationObserver(findAndInsertStyle);
  observer.observe(document.head, {
    childList: true,
    subtree: true,
    characterData: true,
  });

  onMounted(findAndInsertStyle);
  onUnmounted(() => {
    observer.disconnect();
  });
}
</script>

<template>
  <div v-html="html"></div>
</template>

<style>
/* VueCustomElementChildren */
</style>

SomeOtherComponent.vue

<script setup lang="ts">
</script>

<template>
  <div class="some-other">test</div>
</template>

<style>
/* VueCustomElementChildren */
.some-other {
  color: red;
}
</style>

App.ce.vue

<script setup lang="ts">
import CustomElements from "@/components/CustomElements.vue";
import SomeOtherComponent from "@/components/SomeOtherComponent.vue";
</script>

<template>
  <CustomElements css-href="/style/stylesheet.css" />
  <SomeOtherComponent />
</template>

<style>
</style>

@Makoehle
Copy link

I've found a blog by @ElMassimo describing a workaround which works for me. To summarize:

  1. Name all SFC file names *.ce.vue
  2. In your main.ts use defineCustomElement.
  3. Define your custom elements.
  4. Use tag names instead of importing components. E.G <HelloWorld msg="hi"/> becomes \<hello-world msg="hi" />.

Don't know the limitations yet e.G vuex etc.

@claudiomedina
Copy link

As a workaround I've created component, which tracks Mutation in document.head and inline style tags (must includes comment /* VueCustomElementChildren */) into shadow dom in dev mode, for production it inlines link for stylesheets:

@dezmound, wont that make other Vue custom elements' styles get injected into the first one?

For example, if you have 3 custom elements in the same page:

  1. element-a is inserted in page adds style to document.head
  2. observer from element-a copies the style from element-a to its shadow DOM
  3. element-b is inserted in page and adds style to document.head
  4. observer from element-a copies the style from element-b to element-a shadow DOM
  5. observer from element-b copies the style from element-a and element-b to element-b shadow DOM
  6. element-c is inserted in page and adds style to document.head
  7. observer from element-a copies the style from element-c to element-a shadow DOM
  8. observer from element-b copies the style from element-c to element-b shadow DOM
  9. observer from element-b copies the style from element-a, element-b and element-c to element-c shadow DOM

As so you end up with unwanted styles from other components into the components. It only works well if there is only one Vue based custom element/application in the page.

@dezmound
Copy link

dezmound commented Feb 16, 2022

As a workaround I've created component, which tracks Mutation in document.head and inline style tags (must includes comment /* VueCustomElementChildren */) into shadow dom in dev mode, for production it inlines link for stylesheets:

@dezmound, wont that make other Vue custom elements' styles get injected into the first one?

For example, if you have 3 custom elements in the same page:

  1. element-a is inserted in page adds style to document.head
  2. observer from element-a copies the style from element-a to its shadow DOM
  3. element-b is inserted in page and adds style to document.head
  4. observer from element-a copies the style from element-b to element-a shadow DOM
  5. observer from element-b copies the style from element-a and element-b to element-b shadow DOM
  6. element-c is inserted in page and adds style to document.head
  7. observer from element-a copies the style from element-c to element-a shadow DOM
  8. observer from element-b copies the style from element-c to element-b shadow DOM
  9. observer from element-b copies the style from element-a, element-b and element-c to element-c shadow DOM

As so you end up with unwanted styles from other components into the components. It only works well if there is only one Vue based custom element/application in the page.

@claudiomedina Thank you for the comment 🙏 That's right, but it can be solved for example with adding scope prop for CustomElements.vue:

CustomElements.vue

<script setup lang="ts">
/***
 * @see https://github.com/vuejs/vue-next/issues/4662
 **/

import { computed } from "vue";
import { onMounted, onUnmounted, ref } from "@vue/runtime-core";

const props = defineProps<{
  cssHref?: string;
  scope?: string;
}>();

const inlineStyles = ref<HTMLElement[]>([]);
const html = computed(() =>
  // @ts-ignore
  import.meta.env.PROD && props.cssHref
    ? `<link href="${props.cssHref}" rel="stylesheet"/>`
    : `<style>${inlineStyles.value
        .map((style) => style.innerHTML)
        .join("")}</style>`
);

const findAndInsertStyle = () => {
  inlineStyles.value = Array.from(
    document.getElementsByTagName("style")
  ).filter((node) => {
    return node.innerHTML.includes(`${props.scope}: VueCustomElementChildren`);
  });
};

// @ts-ignore
if (!import.meta.env.PROD) {
  const observer = new MutationObserver(findAndInsertStyle);
  observer.observe(document.head, {
    childList: true,
    subtree: true,
    characterData: true,
  });

  onMounted(findAndInsertStyle);
  onUnmounted(() => {
    observer.disconnect();
  });
}
</script>

<template>
  <div v-html="html"></div>
</template>

<style>
/* VueCustomElementChildren */
</style>

Element-a.ce.vue

<script setup lang="ts">
import CustomElements from "@/components/CustomElements.vue";
import SomeOtherComponent from "@/components/SomeOtherComponent.vue";
</script>

<template>
  <CustomElements css-href="/style/stylesheet.css" scope="Element A" />
  <SomeOtherComponentForA />
</template>

<style>
</style>

SomeOtherComponentForA.vue

<script setup lang="ts">
</script>

<template>
  <div class="some-other">test</div>
</template>

<style>
/* Element A: VueCustomElementChildren */
.some-other {
  color: red;
}
</style>

Element-b.ce.vue

<script setup lang="ts">
import CustomElements from "@/components/CustomElements.vue";
import SomeOtherComponent from "@/components/SomeOtherComponent.vue";
</script>

<template>
  <CustomElements css-href="/style/stylesheet.css" scope="Element B" />
  <SomeOtherComponentForB />
</template>

<style>
</style>

SomeOtherComponentForB.vue

<script setup lang="ts">
</script>

<template>
  <div class="some-other">test</div>
</template>

<style>
/* Element B: VueCustomElementChildren */
.some-other {
  color: red;
}
</style>

My example's just a solution that allows to write and use components for CustomElement without casting it into native custom elements, so it's still compactable with TS. The child components may be shared with any entry points such as Vue App or different Custom Element. It doesn't contain complex logic because it does not need to. In some other cases as in your example, this workaround may be easily improved.

@soultice
Copy link

Any update, or is it time to ditch Vue and switch to Svelte?

Oddly enough, Svelte has the exact same issue: sveltejs/svelte#4274 where there is talk about switching to Vue.

@haoqunjiang
Copy link
Member

haoqunjiang commented May 17, 2023

Should we by any chance expect a fix anytime soon

This issue is labeled as "p4-important".

I can't promise, but in an ideal world, the priority queue could be like this:

  1. Fixing v3.3 regressions
  2. p4 issues in core / important issues in other core repos / other planned 3.4 features
  3. p3 issues

@baiwusanyu-c
Copy link
Member

Since this problem has caused me some troubles, and I don’t know when the official vue will fix it, as a temporary solution, I made my PR into a vite plugin and injected the code through compilation to temporarily solve this problem problem, but for more complex scenarios still need testing, if you happen to need it too, you can use this plugin
https://github.com/baiwusanyu-c/unplugin-vue-ce/blob/master/packages/sub-style/README.md

@mrcego
Copy link

mrcego commented May 18, 2023

Since this problem has caused me some troubles, and I don’t know when the official vue will fix it, as a temporary solution, I made my PR into a vite plugin and injected the code through compilation to temporarily solve this problem problem, but for more complex scenarios still need testing, if you happen to need it too, you can use this plugin
https://github.com/baiwusanyu-c/unplugin-vue-ce/blob/master/packages/sub-style/README.md

Could I using regular Vue components with scoped styles tag inside custom elements?

@baiwusanyu-c
Copy link
Member

Since this problem has caused me some troubles, and I don’t know when the official vue will fix it, as a temporary solution, I made my PR into a vite plugin and injected the code through compilation to temporarily solve this problem problem, but for more complex scenarios still need testing, if you happen to need it too, you can use this plugin
https://github.com/baiwusanyu-c/unplugin-vue-ce/blob/master/packages/sub-style/README.md

Could I using regular Vue components with scoped styles tag inside custom elements?

cc 👀: https://stackblitz.com/edit/vitejs-vite-4wwdax?file=package.json

@yoyo837
Copy link

yoyo837 commented May 19, 2023

Since this problem has caused me some troubles, and I don’t know when the official vue will fix it, as a temporary solution, I made my PR into a vite plugin and injected the code through compilation to temporarily solve this problem problem, but for more complex scenarios still need testing, if you happen to need it too, you can use this plugin
https://github.com/baiwusanyu-c/unplugin-vue-ce/blob/master/packages/sub-style/README.md

Could I using regular Vue components with scoped styles tag inside custom elements?

cc 👀: https://stackblitz.com/edit/vitejs-vite-4wwdax?file=package.json

Duplicate css

image

@baiwusanyu-c
Copy link
Member

Thanks, I'll check it out later, maybe there's something wrong with the plug-in injection code @yoyo837

@baiwusanyu-c
Copy link
Member

Thanks, I'll check it out later, maybe there's something wrong with the plug-in injection code @yoyo837

Sorry, this is not a code problem. Before this test project, I manually pasted and copied 1000 style tags for testing.

@lucasantunes-ciandt
Copy link

@raffobaffo would your solution also inject styles into the shadow DOM from child components imported within the defineCustomElement component that originate from external packages?

As an example, if I define and export a component with defineCustomElement, but within that element is a button component that is being imported from an external package (and is not created with defineCustomElement).

Also @lucasantunes-ciandt do you have a working example using the reproduction repo?

Sorry, I haven't worked in this project in over a year now so I no longer have access to it. Hope you have fixed your issue or been able to use baiwusanyu-c's plugin!

@lucasantunes-ciandt
Copy link

lucasantunes-ciandt commented Jun 1, 2023

Since this problem has caused me some troubles, and I don’t know when the official vue will fix it, as a temporary solution, I made my PR into a vite plugin and injected the code through compilation to temporarily solve this problem problem, but for more complex scenarios still need testing, if you happen to need it too, you can use this plugin
baiwusanyu-c/unplugin-vue-ce@master/packages/sub-style/README.md

Could I using regular Vue components with scoped styles tag inside custom elements?

cc 👀: stackblitz.com/edit/vitejs-vite-4wwdax?file=package.json

Duplicate css

image

I remember having this issue at the time, but the culprit was another one. In my case, I vaguely remember I was trying to @import .less (could be SCSS or other preprocessor in your case) mixins and variables in our main.ts file for them to be available in all components, but it would duplicate the CSS for every component we had, so instead I had to inject it by creating a global "theme" for our app. IIRC we used style-resources-loader for this, so Webpack would handle it during build time. Hope it helps 😅

@SavkaTaras
Copy link

Hey all,

I wanted to give a BIG THANK YOU to @baiwusanyu-c for creating unplugin-vue-ce and resolving #4662 issue on a plugin level.

Thank you for all your hard work @baiwusanyu-c !

https://github.com/baiwusanyu-c/unplugin-vue-ce/tree/master

Best regards,
Taras

@padcom
Copy link

padcom commented Dec 14, 2023

We will solve this, it might take a few more days or weeks though.

@LinusBorg looks like it takes quite a bit more than a few days or weeks. Any idea when this will be fixed?

@Grimille
Copy link

Bump, we really need this...

@seyfer
Copy link

seyfer commented Jan 18, 2024

Bump, we really need this...

it will reduce the amount of hacks we have to implement, for sure. now everybody is solving this problem in different ways.

@matthiasPOE
Copy link

anyone managed to solve this cleanly yet?

@dsmelon
Copy link

dsmelon commented May 16, 2024

Three years and still no resolution. This problem is not so simple to solve. The context of the webcomponent needs to be maintained, and the inserted style tags need to be marked to prevent repeated insertion when the component is used multiple times. Currently, I deprecated scoped, and all styles are imported into .ce.vue using @import. Expect an official fix soon

@EranGrin
Copy link

Just another plugin that resolves the issue as SFC or a full app
https://www.npmjs.com/package/vue-web-component-wrapper

@dsmelon
Copy link

dsmelon commented Jul 31, 2024

Just another plugin that resolves the issue as SFC or a full app https://www.npmjs.com/package/vue-web-component-wrapper

有大bug,上面别人修复出现的问题基本全部出现了,一个组件多次使用,样式重复,element-plus的例子,组件样式上升到head标签里去了,而且大量重复的样式,仅仅解决了组件样式不生效的基本问题,不可靠

@SavkaTaras
Copy link

I used the following plugin to resolve all issues:
https://github.com/baiwusanyu-c/unplugin-vue-ce/tree/master

Works like magic. I understand this is not an official fix, but this is good.

@mrcego
Copy link

mrcego commented Jul 31, 2024

I used the following plugin to resolve all issues:
https://github.com/baiwusanyu-c/unplugin-vue-ce/tree/master

Works like magic. I understand this is not an official fix, but this is good.

Life saver, I'm using from its foundation. Kudos to @baiwusanyu-c for this masterpiece.

@yyx990803
Copy link
Member

closed via #11517

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
❗ p4-important Priority 4: this fixes bugs that violate documented behavior, or significantly improves perf. scope: custom elements
Projects
None yet