Skip to content

Commit 56c76a8

Browse files
authoredAug 5, 2024··
feat(custom-element): inject child components styles to custom element shadow root (#11517)
close #4662 close #7941 close #7942
1 parent b74687c commit 56c76a8

File tree

6 files changed

+154
-21
lines changed

6 files changed

+154
-21
lines changed
 

‎packages/runtime-core/src/component.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ export interface ComponentInternalInstance {
417417
* is custom element?
418418
* @internal
419419
*/
420-
ce?: Element
420+
ce?: ComponentCustomElementInterface
421421
/**
422422
* custom element specific HMR method
423423
* @internal
@@ -1237,3 +1237,8 @@ export function formatComponentName(
12371237
export function isClassComponent(value: unknown): value is ClassComponent {
12381238
return isFunction(value) && '__vccOpts' in value
12391239
}
1240+
1241+
export interface ComponentCustomElementInterface {
1242+
injectChildStyle(type: ConcreteComponent): void
1243+
removeChildStlye(type: ConcreteComponent): void
1244+
}

‎packages/runtime-core/src/hmr.ts

+5
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ function reload(id: string, newComp: HMRComponent) {
159159
'[HMR] Root or manually mounted instance modified. Full reload required.',
160160
)
161161
}
162+
163+
// update custom element child style
164+
if (instance.root.ce && instance !== instance.root) {
165+
instance.root.ce.removeChildStlye(oldComp)
166+
}
162167
}
163168

164169
// 5. make sure to cleanup dirty hmr components after update

‎packages/runtime-core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ export type {
263263
GlobalComponents,
264264
GlobalDirectives,
265265
ComponentInstance,
266+
ComponentCustomElementInterface,
266267
} from './component'
267268
export type {
268269
DefineComponent,

‎packages/runtime-core/src/renderer.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1276,8 +1276,8 @@ function baseCreateRenderer(
12761276
const componentUpdateFn = () => {
12771277
if (!instance.isMounted) {
12781278
let vnodeHook: VNodeHook | null | undefined
1279-
const { el, props, type } = initialVNode
1280-
const { bm, m, parent } = instance
1279+
const { el, props } = initialVNode
1280+
const { bm, m, parent, root, type } = instance
12811281
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
12821282

12831283
toggleRecurse(instance, false)
@@ -1335,6 +1335,11 @@ function baseCreateRenderer(
13351335
hydrateSubTree()
13361336
}
13371337
} else {
1338+
// custom element style injection
1339+
if (root.ce) {
1340+
root.ce.injectChildStyle(type)
1341+
}
1342+
13381343
if (__DEV__) {
13391344
startMeasure(instance, `render`)
13401345
}

‎packages/runtime-dom/__tests__/customElement.spec.ts

+71-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { MockedFunction } from 'vitest'
22
import {
3+
type HMRRuntime,
34
type Ref,
45
type VueElement,
56
createApp,
@@ -15,6 +16,8 @@ import {
1516
useShadowRoot,
1617
} from '../src'
1718

19+
declare var __VUE_HMR_RUNTIME__: HMRRuntime
20+
1821
describe('defineCustomElement', () => {
1922
const container = document.createElement('div')
2023
document.body.appendChild(container)
@@ -636,18 +639,84 @@ describe('defineCustomElement', () => {
636639
})
637640

638641
describe('styles', () => {
639-
test('should attach styles to shadow dom', () => {
640-
const Foo = defineCustomElement({
642+
function assertStyles(el: VueElement, css: string[]) {
643+
const styles = el.shadowRoot?.querySelectorAll('style')!
644+
expect(styles.length).toBe(css.length) // should not duplicate multiple copies from Bar
645+
for (let i = 0; i < css.length; i++) {
646+
expect(styles[i].textContent).toBe(css[i])
647+
}
648+
}
649+
650+
test('should attach styles to shadow dom', async () => {
651+
const def = defineComponent({
652+
__hmrId: 'foo',
641653
styles: [`div { color: red; }`],
642654
render() {
643655
return h('div', 'hello')
644656
},
645657
})
658+
const Foo = defineCustomElement(def)
646659
customElements.define('my-el-with-styles', Foo)
647660
container.innerHTML = `<my-el-with-styles></my-el-with-styles>`
648661
const el = container.childNodes[0] as VueElement
649662
const style = el.shadowRoot?.querySelector('style')!
650663
expect(style.textContent).toBe(`div { color: red; }`)
664+
665+
// hmr
666+
__VUE_HMR_RUNTIME__.reload('foo', {
667+
...def,
668+
styles: [`div { color: blue; }`, `div { color: yellow; }`],
669+
} as any)
670+
671+
await nextTick()
672+
assertStyles(el, [`div { color: blue; }`, `div { color: yellow; }`])
673+
})
674+
675+
test("child components should inject styles to root element's shadow root", async () => {
676+
const Baz = () => h(Bar)
677+
const Bar = defineComponent({
678+
__hmrId: 'bar',
679+
styles: [`div { color: green; }`, `div { color: blue; }`],
680+
render() {
681+
return 'bar'
682+
},
683+
})
684+
const Foo = defineCustomElement({
685+
styles: [`div { color: red; }`],
686+
render() {
687+
return [h(Baz), h(Baz)]
688+
},
689+
})
690+
customElements.define('my-el-with-child-styles', Foo)
691+
container.innerHTML = `<my-el-with-child-styles></my-el-with-child-styles>`
692+
const el = container.childNodes[0] as VueElement
693+
694+
// inject order should be child -> parent
695+
assertStyles(el, [
696+
`div { color: green; }`,
697+
`div { color: blue; }`,
698+
`div { color: red; }`,
699+
])
700+
701+
// hmr
702+
__VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
703+
...Bar,
704+
styles: [`div { color: red; }`, `div { color: yellow; }`],
705+
} as any)
706+
707+
await nextTick()
708+
assertStyles(el, [
709+
`div { color: red; }`,
710+
`div { color: yellow; }`,
711+
`div { color: red; }`,
712+
])
713+
714+
__VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
715+
...Bar,
716+
styles: [`div { color: blue; }`],
717+
} as any)
718+
await nextTick()
719+
assertStyles(el, [`div { color: blue; }`, `div { color: red; }`])
651720
})
652721
})
653722

‎packages/runtime-dom/src/apiCustomElement.ts

+64-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
type Component,
3+
type ComponentCustomElementInterface,
34
type ComponentInjectOptions,
45
type ComponentInternalInstance,
56
type ComponentObjectPropsOptions,
@@ -189,7 +190,10 @@ const BaseClass = (
189190

190191
type InnerComponentDef = ConcreteComponent & CustomElementOptions
191192

192-
export class VueElement extends BaseClass {
193+
export class VueElement
194+
extends BaseClass
195+
implements ComponentCustomElementInterface
196+
{
193197
/**
194198
* @internal
195199
*/
@@ -198,7 +202,15 @@ export class VueElement extends BaseClass {
198202
private _connected = false
199203
private _resolved = false
200204
private _numberProps: Record<string, true> | null = null
205+
private _styleChildren = new WeakSet()
206+
/**
207+
* dev only
208+
*/
201209
private _styles?: HTMLStyleElement[]
210+
/**
211+
* dev only
212+
*/
213+
private _childStyles?: Map<string, HTMLStyleElement[]>
202214
private _ob?: MutationObserver | null = null
203215
/**
204216
* @internal
@@ -312,13 +324,14 @@ export class VueElement extends BaseClass {
312324
}
313325

314326
// apply CSS
315-
if (__DEV__ && styles && def.shadowRoot === false) {
327+
if (this.shadowRoot) {
328+
this._applyStyles(styles)
329+
} else if (__DEV__ && styles) {
316330
warn(
317331
'Custom element style injection is not supported when using ' +
318332
'shadowRoot: false',
319333
)
320334
}
321-
this._applyStyles(styles)
322335

323336
// initial render
324337
this._update()
@@ -329,7 +342,7 @@ export class VueElement extends BaseClass {
329342

330343
const asyncDef = (this._def as ComponentOptions).__asyncLoader
331344
if (asyncDef) {
332-
asyncDef().then(def => resolve(def, true))
345+
asyncDef().then(def => resolve((this._def = def), true))
333346
} else {
334347
resolve(this._def)
335348
}
@@ -486,19 +499,36 @@ export class VueElement extends BaseClass {
486499
return vnode
487500
}
488501

489-
private _applyStyles(styles: string[] | undefined) {
490-
const root = this.shadowRoot
491-
if (!root) return
492-
if (styles) {
493-
styles.forEach(css => {
494-
const s = document.createElement('style')
495-
s.textContent = css
496-
root.appendChild(s)
497-
// record for HMR
498-
if (__DEV__) {
502+
private _applyStyles(
503+
styles: string[] | undefined,
504+
owner?: ConcreteComponent,
505+
) {
506+
if (!styles) return
507+
if (owner) {
508+
if (owner === this._def || this._styleChildren.has(owner)) {
509+
return
510+
}
511+
this._styleChildren.add(owner)
512+
}
513+
for (let i = styles.length - 1; i >= 0; i--) {
514+
const s = document.createElement('style')
515+
s.textContent = styles[i]
516+
this.shadowRoot!.prepend(s)
517+
// record for HMR
518+
if (__DEV__) {
519+
if (owner) {
520+
if (owner.__hmrId) {
521+
if (!this._childStyles) this._childStyles = new Map()
522+
let entry = this._childStyles.get(owner.__hmrId)
523+
if (!entry) {
524+
this._childStyles.set(owner.__hmrId, (entry = []))
525+
}
526+
entry.push(s)
527+
}
528+
} else {
499529
;(this._styles || (this._styles = [])).push(s)
500530
}
501-
})
531+
}
502532
}
503533
}
504534

@@ -547,6 +577,24 @@ export class VueElement extends BaseClass {
547577
parent.removeChild(o)
548578
}
549579
}
580+
581+
injectChildStyle(comp: ConcreteComponent & CustomElementOptions) {
582+
this._applyStyles(comp.styles, comp)
583+
}
584+
585+
removeChildStlye(comp: ConcreteComponent): void {
586+
if (__DEV__) {
587+
this._styleChildren.delete(comp)
588+
if (this._childStyles && comp.__hmrId) {
589+
// clear old styles
590+
const oldStyles = this._childStyles.get(comp.__hmrId)
591+
if (oldStyles) {
592+
oldStyles.forEach(s => this._root.removeChild(s))
593+
oldStyles.length = 0
594+
}
595+
}
596+
}
597+
}
550598
}
551599

552600
/**
@@ -557,7 +605,7 @@ export function useShadowRoot(): ShadowRoot | null {
557605
const instance = getCurrentInstance()
558606
const el = instance && instance.ce
559607
if (el) {
560-
return el.shadowRoot
608+
return (el as VueElement).shadowRoot
561609
} else if (__DEV__) {
562610
if (!instance) {
563611
warn(`useCustomElementRoot called without an active component instance.`)

0 commit comments

Comments
 (0)
Please sign in to comment.