core
core copied to clipboard
feat(runtime-dom): Apply nested component styles when using defineCustomElements
Motivation
defineCustomElements
is supporting SFC having style tag, but nested SFC's styles are not applied.
Even if SFC tools import nested SFCs in custom element mode and each SFCs have styles as property,_applyStyles
is received root SFC's style only and nested SFCs' styles are ignored.
I think it's commonly that a SFC has some smaller SFCs.
Changes
apiCustomElement.ts
- add private method
_getStylesRecursively
- When a SFC has nested components, a SFC has
components
property. - When a SFC has styles, a SFC imported by SFC tools of custom element mode has
styles
property. -_getStylesRecursively
read component recursively using eachcomponents
properties and readstyles
property if a component has. - All read styles are pushed to an array and
_getStylesRecursively
returns the array.
- When a SFC has nested components, a SFC has
- Provide all styles including nested component's styles to
_applyStyles
inconnectedCallback
(_resolveDef
).
customElement.spec.ts
- add test case (
test('should attach styles recursively to shadow dom')
)
If the child components are compiled under custom elements mode, it's probably better to register them and use them as custom elements.
That said, this could be a use case where the library author don't want to pre-determine the tags the elements are registered as. Unfortunately this PR wouldn't cover the case if child components are not registered using the components
option (e.g. <script setup>
)
If the child components are compiled under custom elements mode, it's probably better to register them and use them as custom elements.
Generally I'd agree, but for tightly coupled components you wouldn't be able to use provide/Inject any longer, which is quite common in these scenarios.
If the child components are compiled under custom elements mode, it's probably better to register them and use them as custom elements.
Indeed. Would it work and be an acceptable workaround to require component authors to register them in a second script block?
<script setup>
import Child from './Child.vue'
</script>
<script>
import Child from './Child.vue'
export default {
Child
}
</script>
Not exactly pretty but ...
sorry, I accidentally close this PR.......
If it default-exports an object with a component property, this PR can recursively read SFCs with the current _getStylesRecursively
. (@LinusBorg, thank you for your idea!)
<script>
import Child from './Child.vue'
export default {
components: { Child }
}
</script>
@yyx990803 thank you for checking this PR!
it's probably better to register them and use them as custom elements.
I understood that ​component authors have two options to creating web-components with nested vue components.
- register child SFCs as custom elements and use them in parent SFCs.
- export child SFCs in parent SFCs.
I think "export child SFCs" has smaller impact on a SFC implementation as vue components. In the case of "register child SFCs as custom elements", an implementation to output web-components affects a SFC implementation as vue components
The document announces usage alongside normal <script>
for declaring additional options.
https://v3.vuejs.org/api/sfc-script-setup.html#usage-alongside-normal-script
After reading this, I feel natural to declare components option in script
.
Hi, for me exporting child component in second
@pawel-marciniak I can check the cause of error if there is a reproduction code(repository).
Is there any plan to fix this? Because for now I will need to get back to Vue 2.x. Regarding my above error - I'm using TypeScript and I have the same error when I'm using one
Hey, I'm facing the same problem now. Is there any plan to fix this?
I have an idea to add a feature for mini-css-extract-plugin to resolve this.
If we need to inject all CSS (including child components or load by other dependencies) into the root shadow-dom.
Can provide a runtime container for storing all the extract-plugin extracted CSS and import that to inject into the root.
This is inspirated by the runtimeGenerator
option of svg-sprite-loader .
Hey, I'm facing the same problem now. Is there any plan to fix this? I have an idea to add a feature for mini-css-extract-plugin to resolve this. If we need to inject all CSS (including child components or load by other dependencies) into the root shadow-dom. Can provide a runtime container for storing all the extract-plugin extracted CSS and import that to inject into the root. This is inspirated by the
runtimeGenerator
option of svg-sprite-loader .
I've added the shadow-DOM
type for the injectType
option of the style-loader
But seems they don't want to add the feature for the plugin. Can see the PR: https://github.com/webpack-contrib/style-loader/pull/536
So I create a new loader to temporarily fix this issue. https://github.com/Justinidlerz/style-shadow-dom-loader#shadowdom
Also, would this 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, imported from an external package, that was not created with defineCustomElement
.
You could do a workaround. It's situational and doesn't cover everything, but it might suit your usecase. In your components you simple call useStyles(). This uses the fact that the styles are exposed on the style property. You need to name your components *.ce.vue or change the compiler options.
import { ref, onMounted, getCurrentInstance } from 'vue'
export const useStyles = (removeStyleTag: boolean = true) => {
const shadowRoot = ref<ShadowRoot>()
onMounted(() => {
const instance = getCurrentInstance()
if (!instance) return
shadowRoot.value = instance.vnode?.el?.getRootNode()
if (!shadowRoot.value) return
// @ts-expect-error
const styles = instance.type.styles.join('')
// Use __hmrId as UUID for replacing styles, instead of adding new ones every time.
// @ts-expect-error
const __hmrId = instance.type.__hmrId
if (instance.isCE) {
// Don't remove styletag in dev, because runtime would try to remove non existing dom element on update.
if (import.meta.env.DEV || !removeStyleTag) return
// Not the best to let the runtime first add a styletag and than remove it, but we don't want to touch the vue runtime code.
shadowRoot.value
.querySelectorAll('style')
.forEach((style) => style.remove())
}
adoptStyles(shadowRoot.value, styles, __hmrId)
})
}
/**
* Whether the current browser supports `adoptedStyleSheets`.
*/
export const supportsAdoptingStyleSheets =
window.ShadowRoot &&
'adoptedStyleSheets' in Document.prototype &&
'replaceSync' in CSSStyleSheet.prototype
/**
* Add constructed Stylesheet or style tag to Shadowroot of VueCE.
* @param renderRoot The shadowroot of the vueCE..
* @param styles The styles of the Element.
* @param __hmrId hmr id of vite used as an UUID.
*/
export const adoptStyles = (
renderRoot: ShadowRoot,
styles: string,
__hmrId: string
) => {
if (supportsAdoptingStyleSheets) {
const sheets = renderRoot.adoptedStyleSheets
const oldSheet = sheets.find((sheet) => sheet.__hmrId === __hmrId)
// Check if this StyleSheet exists already. Replace content if it does. Otherwise construct a new CSSStyleSheet.
if (oldSheet) {
oldSheet.replaceSync(styles)
} else {
const styleSheet: CSSStyleSheet = new CSSStyleSheet()
styleSheet.__hmrId = __hmrId
styleSheet.replaceSync(styles)
renderRoot.adoptedStyleSheets = [
...renderRoot.adoptedStyleSheets,
styleSheet,
]
}
} else {
const existingStyleElements = renderRoot.querySelectorAll('style')
const oldStyleElement = Array.from(existingStyleElements).find(
(sheet) => sheet.title === __hmrId
)
// Check if this Style Element exists already. Replace content if it does. Otherwise construct a new HTMLStyleElement.
if (oldStyleElement) {
oldStyleElement.innerHTML = styles
} else {
const styleElement = document.createElement('style')
styleElement.title = __hmrId
styleElement.innerHTML = styles
renderRoot.appendChild(styleElement)
}
}
}
<template>
<div class="abc">ABC</div>
</template>
<script setup lang="ts">
import { useStyles } from '../composables/useStyles'
useStyles()
</script>
<style>
.abc {
background: red;
}
</style>
<style>
.abc {
color: purple;
}
</style>
<template>
<Abc />
</template>
<script setup lang="ts">
import Abc from './Abc.ce.vue'
</script>
If the child components are compiled under custom elements mode, it's probably better to register them and use them as custom elements.
Using children this way means then you have to deal with DOM style props (slightly different syntax, arrays as string trap) and grabbing values from an ugly attribute array inside of custom events. This is just as unsatisfying as defining all styles in the root element. I may be wrong, but the developer experience for custom elements is currently not nearly as good as for regular Vue applications (although still a great project).
Is there any update on this? Not including the styles of child components makes it impossible to use component-based UI libraries in a custom element. Is that a bad practice to begin with, am I missing something?
Are there a workaround for this? Some indication of where this is going would be nice. A simple "not in the near future", "or not possible in v3" is fine. A long running open issue like this is a bit difficult when taking decitions.
Hello! Any news on this PR?
Bump
I think the whole custom element api should get a look at. There are a lot of painpoints. E.g.:
- nested styles
- access to the constructor
- access to the host element
- access to the shadowroot
- prefer constructed stylesheets over style tags
- allow attachElementInternals
- better support for defineExpose/defineEmits