mirror of https://github.com/vuejs/core.git
feat(custom-element): inject child components styles to custom element shadow root (#11517)
close #4662 close #7941 close #7942
This commit is contained in:
parent
b74687c0bb
commit
56c76a8b05
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -263,6 +263,7 @@ export type {
|
|||
GlobalComponents,
|
||||
GlobalDirectives,
|
||||
ComponentInstance,
|
||||
ComponentCustomElementInterface,
|
||||
} from './component'
|
||||
export type {
|
||||
DefineComponent,
|
||||
|
|
|
@ -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`)
|
||||
}
|
||||
|
|
|
@ -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 = `<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; }`])
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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<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
|
||||
|
@ -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 => {
|
||||
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 = css
|
||||
root.appendChild(s)
|
||||
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.`)
|
||||
|
|
Loading…
Reference in New Issue