Skip to content

Commit

Permalink
feat(custom-element): inject child components styles to custom elemen…
Browse files Browse the repository at this point in the history
…t shadow root (#11517)

close #4662
close #7941
close #7942
  • Loading branch information
yyx990803 committed Aug 5, 2024
1 parent b74687c commit 56c76a8
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 21 deletions.
7 changes: 6 additions & 1 deletion packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ export interface ComponentInternalInstance {
* is custom element?
* @internal
*/
ce?: Element
ce?: ComponentCustomElementInterface
/**
* custom element specific HMR method
* @internal
Expand Down Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions packages/runtime-core/src/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ export type {
GlobalComponents,
GlobalDirectives,
ComponentInstance,
ComponentCustomElementInterface,
} from './component'
export type {
DefineComponent,
Expand Down
9 changes: 7 additions & 2 deletions packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1335,6 +1335,11 @@ function baseCreateRenderer(
hydrateSubTree()
}
} else {
// custom element style injection
if (root.ce) {
root.ce.injectChildStyle(type)
}

if (__DEV__) {
startMeasure(instance, `render`)
}
Expand Down
73 changes: 71 additions & 2 deletions packages/runtime-dom/__tests__/customElement.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MockedFunction } from 'vitest'
import {
type HMRRuntime,
type Ref,
type VueElement,
createApp,
Expand All @@ -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)
Expand Down Expand Up @@ -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 = `<my-el-with-styles></my-el-with-styles>`
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 = `<my-el-with-child-styles></my-el-with-child-styles>`
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; }`])
})
})

Expand Down
80 changes: 64 additions & 16 deletions packages/runtime-dom/src/apiCustomElement.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
type Component,
type ComponentCustomElementInterface,
type ComponentInjectOptions,
type ComponentInternalInstance,
type ComponentObjectPropsOptions,
Expand Down Expand Up @@ -189,7 +190,10 @@ const BaseClass = (

type InnerComponentDef = ConcreteComponent & CustomElementOptions

export class VueElement extends BaseClass {
export class VueElement
extends BaseClass
implements ComponentCustomElementInterface
{
/**
* @internal
*/
Expand All @@ -198,7 +202,15 @@ export class VueElement extends BaseClass {
private _connected = false
private _resolved = false
private _numberProps: Record<string, true> | null = null
private _styleChildren = new WeakSet()
/**
* dev only
*/
private _styles?: HTMLStyleElement[]
/**
* dev only
*/
private _childStyles?: Map<string, HTMLStyleElement[]>
private _ob?: MutationObserver | null = null
/**
* @internal
Expand Down Expand Up @@ -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()
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
})
}
}
}

Expand Down Expand Up @@ -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
}
}
}
}
}

/**
Expand All @@ -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.`)
Expand Down

0 comments on commit 56c76a8

Please sign in to comment.