diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 713c8148790..4863c24a8cc 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -417,7 +417,7 @@ export interface ComponentInternalInstance {
* is custom element?
* @internal
*/
- ce?: Element
+ ce?: ComponentCustomElementInterface
/**
* custom element specific HMR method
* @internal
@@ -1237,3 +1237,8 @@ export function formatComponentName(
export function isClassComponent(value: unknown): value is ClassComponent {
return isFunction(value) && '__vccOpts' in value
}
+
+export interface ComponentCustomElementInterface {
+ injectChildStyle(type: ConcreteComponent): void
+ removeChildStlye(type: ConcreteComponent): void
+}
diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts
index e5c16b7077b..6eb0c372c3f 100644
--- a/packages/runtime-core/src/hmr.ts
+++ b/packages/runtime-core/src/hmr.ts
@@ -159,6 +159,11 @@ function reload(id: string, newComp: HMRComponent) {
'[HMR] Root or manually mounted instance modified. Full reload required.',
)
}
+
+ // update custom element child style
+ if (instance.root.ce && instance !== instance.root) {
+ instance.root.ce.removeChildStlye(oldComp)
+ }
}
// 5. make sure to cleanup dirty hmr components after update
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index 27372cfc303..34d62762a5c 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -263,6 +263,7 @@ export type {
GlobalComponents,
GlobalDirectives,
ComponentInstance,
+ ComponentCustomElementInterface,
} from './component'
export type {
DefineComponent,
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index 466a21a7e51..588a58e34ca 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -1276,8 +1276,8 @@ function baseCreateRenderer(
const componentUpdateFn = () => {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
- const { el, props, type } = initialVNode
- const { bm, m, parent } = instance
+ const { el, props } = initialVNode
+ const { bm, m, parent, root, type } = instance
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
toggleRecurse(instance, false)
@@ -1335,6 +1335,11 @@ function baseCreateRenderer(
hydrateSubTree()
}
} else {
+ // custom element style injection
+ if (root.ce) {
+ root.ce.injectChildStyle(type)
+ }
+
if (__DEV__) {
startMeasure(instance, `render`)
}
diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts
index 91596b67f1c..58de1810548 100644
--- a/packages/runtime-dom/__tests__/customElement.spec.ts
+++ b/packages/runtime-dom/__tests__/customElement.spec.ts
@@ -1,5 +1,6 @@
import type { MockedFunction } from 'vitest'
import {
+ type HMRRuntime,
type Ref,
type VueElement,
createApp,
@@ -15,6 +16,8 @@ import {
useShadowRoot,
} from '../src'
+declare var __VUE_HMR_RUNTIME__: HMRRuntime
+
describe('defineCustomElement', () => {
const container = document.createElement('div')
document.body.appendChild(container)
@@ -636,18 +639,84 @@ describe('defineCustomElement', () => {
})
describe('styles', () => {
- test('should attach styles to shadow dom', () => {
- const Foo = defineCustomElement({
+ function assertStyles(el: VueElement, css: string[]) {
+ const styles = el.shadowRoot?.querySelectorAll('style')!
+ expect(styles.length).toBe(css.length) // should not duplicate multiple copies from Bar
+ for (let i = 0; i < css.length; i++) {
+ expect(styles[i].textContent).toBe(css[i])
+ }
+ }
+
+ test('should attach styles to shadow dom', async () => {
+ const def = defineComponent({
+ __hmrId: 'foo',
styles: [`div { color: red; }`],
render() {
return h('div', 'hello')
},
})
+ const Foo = defineCustomElement(def)
customElements.define('my-el-with-styles', Foo)
container.innerHTML = ``
const el = container.childNodes[0] as VueElement
const style = el.shadowRoot?.querySelector('style')!
expect(style.textContent).toBe(`div { color: red; }`)
+
+ // hmr
+ __VUE_HMR_RUNTIME__.reload('foo', {
+ ...def,
+ styles: [`div { color: blue; }`, `div { color: yellow; }`],
+ } as any)
+
+ await nextTick()
+ assertStyles(el, [`div { color: blue; }`, `div { color: yellow; }`])
+ })
+
+ test("child components should inject styles to root element's shadow root", async () => {
+ const Baz = () => h(Bar)
+ const Bar = defineComponent({
+ __hmrId: 'bar',
+ styles: [`div { color: green; }`, `div { color: blue; }`],
+ render() {
+ return 'bar'
+ },
+ })
+ const Foo = defineCustomElement({
+ styles: [`div { color: red; }`],
+ render() {
+ return [h(Baz), h(Baz)]
+ },
+ })
+ customElements.define('my-el-with-child-styles', Foo)
+ container.innerHTML = ``
+ const el = container.childNodes[0] as VueElement
+
+ // inject order should be child -> parent
+ assertStyles(el, [
+ `div { color: green; }`,
+ `div { color: blue; }`,
+ `div { color: red; }`,
+ ])
+
+ // hmr
+ __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
+ ...Bar,
+ styles: [`div { color: red; }`, `div { color: yellow; }`],
+ } as any)
+
+ await nextTick()
+ assertStyles(el, [
+ `div { color: red; }`,
+ `div { color: yellow; }`,
+ `div { color: red; }`,
+ ])
+
+ __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
+ ...Bar,
+ styles: [`div { color: blue; }`],
+ } as any)
+ await nextTick()
+ assertStyles(el, [`div { color: blue; }`, `div { color: red; }`])
})
})
diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts
index 846774fa66e..4bc0b292421 100644
--- a/packages/runtime-dom/src/apiCustomElement.ts
+++ b/packages/runtime-dom/src/apiCustomElement.ts
@@ -1,5 +1,6 @@
import {
type Component,
+ type ComponentCustomElementInterface,
type ComponentInjectOptions,
type ComponentInternalInstance,
type ComponentObjectPropsOptions,
@@ -189,7 +190,10 @@ const BaseClass = (
type InnerComponentDef = ConcreteComponent & CustomElementOptions
-export class VueElement extends BaseClass {
+export class VueElement
+ extends BaseClass
+ implements ComponentCustomElementInterface
+{
/**
* @internal
*/
@@ -198,7 +202,15 @@ export class VueElement extends BaseClass {
private _connected = false
private _resolved = false
private _numberProps: Record | null = null
+ private _styleChildren = new WeakSet()
+ /**
+ * dev only
+ */
private _styles?: HTMLStyleElement[]
+ /**
+ * dev only
+ */
+ private _childStyles?: Map
private _ob?: MutationObserver | null = null
/**
* @internal
@@ -312,13 +324,14 @@ export class VueElement extends BaseClass {
}
// apply CSS
- if (__DEV__ && styles && def.shadowRoot === false) {
+ if (this.shadowRoot) {
+ this._applyStyles(styles)
+ } else if (__DEV__ && styles) {
warn(
'Custom element style injection is not supported when using ' +
'shadowRoot: false',
)
}
- this._applyStyles(styles)
// initial render
this._update()
@@ -329,7 +342,7 @@ export class VueElement extends BaseClass {
const asyncDef = (this._def as ComponentOptions).__asyncLoader
if (asyncDef) {
- asyncDef().then(def => resolve(def, true))
+ asyncDef().then(def => resolve((this._def = def), true))
} else {
resolve(this._def)
}
@@ -486,19 +499,36 @@ export class VueElement extends BaseClass {
return vnode
}
- private _applyStyles(styles: string[] | undefined) {
- const root = this.shadowRoot
- if (!root) return
- if (styles) {
- styles.forEach(css => {
- const s = document.createElement('style')
- s.textContent = css
- root.appendChild(s)
- // record for HMR
- if (__DEV__) {
+ private _applyStyles(
+ styles: string[] | undefined,
+ owner?: ConcreteComponent,
+ ) {
+ if (!styles) return
+ if (owner) {
+ if (owner === this._def || this._styleChildren.has(owner)) {
+ return
+ }
+ this._styleChildren.add(owner)
+ }
+ for (let i = styles.length - 1; i >= 0; i--) {
+ const s = document.createElement('style')
+ s.textContent = styles[i]
+ this.shadowRoot!.prepend(s)
+ // record for HMR
+ if (__DEV__) {
+ if (owner) {
+ if (owner.__hmrId) {
+ if (!this._childStyles) this._childStyles = new Map()
+ let entry = this._childStyles.get(owner.__hmrId)
+ if (!entry) {
+ this._childStyles.set(owner.__hmrId, (entry = []))
+ }
+ entry.push(s)
+ }
+ } else {
;(this._styles || (this._styles = [])).push(s)
}
- })
+ }
}
}
@@ -547,6 +577,24 @@ export class VueElement extends BaseClass {
parent.removeChild(o)
}
}
+
+ injectChildStyle(comp: ConcreteComponent & CustomElementOptions) {
+ this._applyStyles(comp.styles, comp)
+ }
+
+ removeChildStlye(comp: ConcreteComponent): void {
+ if (__DEV__) {
+ this._styleChildren.delete(comp)
+ if (this._childStyles && comp.__hmrId) {
+ // clear old styles
+ const oldStyles = this._childStyles.get(comp.__hmrId)
+ if (oldStyles) {
+ oldStyles.forEach(s => this._root.removeChild(s))
+ oldStyles.length = 0
+ }
+ }
+ }
+ }
}
/**
@@ -557,7 +605,7 @@ export function useShadowRoot(): ShadowRoot | null {
const instance = getCurrentInstance()
const el = instance && instance.ce
if (el) {
- return el.shadowRoot
+ return (el as VueElement).shadowRoot
} else if (__DEV__) {
if (!instance) {
warn(`useCustomElementRoot called without an active component instance.`)