core icon indicating copy to clipboard operation
core copied to clipboard

When importing component inside custom element, style is discarded

Open gnuletik opened this issue 4 years ago • 51 comments
trafficstars

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.

gnuletik avatar Sep 23 '21 12:09 gnuletik

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

tony19 avatar Sep 27 '21 06:09 tony19

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

raffobaffo avatar Sep 27 '21 13:09 raffobaffo

I just realized there is already an open PR about this : https://github.com/vuejs/vue-next/pull/4309

gnuletik avatar Sep 27 '21 13:09 gnuletik

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

raffobaffo avatar Sep 27 '21 15:09 raffobaffo

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 avatar Sep 28 '21 10:09 raffobaffo

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?

pawel-marciniak avatar Oct 14 '21 12:10 pawel-marciniak

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

LinusBorg avatar Oct 14 '21 13:10 LinusBorg

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 :)

pawel-marciniak avatar Oct 14 '21 13:10 pawel-marciniak

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

raffobaffo avatar Oct 16 '21 15:10 raffobaffo

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

lucasantunes-ciandt avatar Nov 09 '21 23:11 lucasantunes-ciandt

@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);
  }
}

raffobaffo avatar Nov 10 '21 21:11 raffobaffo

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

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 avatar Nov 11 '21 21:11 lucasantunes-ciandt

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 avatar Nov 11 '21 22:11 lucasantunes-ciandt

@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);
});

lucasantunes-ciandt avatar Nov 11 '21 23:11 lucasantunes-ciandt

@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 🤔 .

raffobaffo avatar Nov 12 '21 17:11 raffobaffo

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

lucasantunes-ciandt avatar Nov 13 '21 03:11 lucasantunes-ciandt

@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?

adamdehaven avatar Nov 24 '21 17:11 adamdehaven

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

raffobaffo avatar Nov 27 '21 14:11 raffobaffo

@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!

adamdehaven avatar Nov 27 '21 14:11 adamdehaven

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>

dezmound avatar Nov 29 '21 13:11 dezmound

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.

Makoehle avatar Jan 19 '22 21:01 Makoehle

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 avatar Feb 16 '22 09:02 claudiomedina

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.

dezmound avatar Feb 16 '22 10:02 dezmound

Is there any solution for this in sight ? This is a big show stopper in https://github.com/vitejs/vite/issues/5731

lroal avatar Mar 15 '22 14:03 lroal

Is there any solution for this in sight ? This is a big show stopper in vitejs/vite#5731

If you don't need to use shadow DOM, you can use your own defineCustomElement function from this PR as described here: https://github.com/vuejs/core/issues/4314#issuecomment-1021393430. That fixes the issue.

gnuletik avatar Mar 15 '22 15:03 gnuletik

Thank's, but I need to use shadow DOM. I guess another workaround would be to use global styles from the top component - instead of scoped styles in each sub component ?

lroal avatar Mar 16 '22 07:03 lroal

@lroal

I use global styles with importing stylesheets in root Component (root.ce.vue) to avoid this problem.

<style>
// v-bind things... like
--configurable-size: v-bind("~~~")

</style>
<style src="./styles/sheet1"></style> // maybe, use css variables declared in root component style
<style src="./styles/sheet2"></style>
<style src="./styles/sheet3"></style>
<style src="./styles/sheet4"></style>

naramdash avatar Mar 16 '22 08:03 naramdash

import { createApp, defineCustomElement } from 'vue'
import App from './App.vue'

const styles = ['button { background: red; }']
customElements.define('yuty-lens', defineCustomElement({ ...App, styles }))

const app = createApp(App)
app.mount('#app')

I added inline CSS for shadow root but styles aren't loading at all, Does anyone has with the same issue?

engineerbelawalumer avatar Apr 27 '22 21:04 engineerbelawalumer

Hi, is there a solution coming for this?

I currently use a workaround where I let vue-cli's webpack simply generate external link-tags inside a template tag. In the mounted hook I clone those tags into the shadow root. This solution also works with @-paths for static assets in the css/scss.
I've added a minimal demo here, maybe it's helpful for someone:

https://gitlab.com/cgz/vue-web-comp-css-injection-demo.git

But I would really prefer a out of the box solution. Hopefully it will be fixed in one of the next versions.

Chrisx0385 avatar May 03 '22 11:05 Chrisx0385

There's another workaround:

  1. rename all components to *.ce.vue or enable { customElement: true } in your config so all components styles are exported like described
  2. in your main.ts import all components you are using in your CE and concatenate their styles with the main components styles e.g.:
import HelloWorld from './components/HelloWorld.vue';
import App from './App.vue';

App.styles = [...App.styles, ...HelloWorld.styles];

customElements.define('my-component', defineCustomElement(App));

I haven't exhaustively tested this yet but it works for me with the starter app.

soultice avatar May 04 '22 21:05 soultice