Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(custom-element): inject child components styles to custom element shadow root #11517

Merged
merged 1 commit into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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