* is custom element?
* @internal
*/
- ce?: Element
+ ce?: ComponentCustomElementInterface
/**
* custom element specific HMR method
* @internal
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
+}
'[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
GlobalComponents,
GlobalDirectives,
ComponentInstance,
+ ComponentCustomElementInterface,
} from './component'
export type {
DefineComponent,
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)
hydrateSubTree()
}
} else {
+ // custom element style injection
+ if (root.ce) {
+ root.ce.injectChildStyle(type)
+ }
+
if (__DEV__) {
startMeasure(instance, `render`)
}
import type { MockedFunction } from 'vitest'
import {
+ type HMRRuntime,
type Ref,
type VueElement,
createApp,
useShadowRoot,
} from '../src'
+declare var __VUE_HMR_RUNTIME__: HMRRuntime
+
describe('defineCustomElement', () => {
const container = document.createElement('div')
document.body.appendChild(container)
})
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; }`])
})
})
import {
type Component,
+ type ComponentCustomElementInterface,
type ComponentInjectOptions,
type ComponentInternalInstance,
type ComponentObjectPropsOptions,
type InnerComponentDef = ConcreteComponent & CustomElementOptions
-export class VueElement extends BaseClass {
+export class VueElement
+ extends BaseClass
+ implements ComponentCustomElementInterface
+{
/**
* @internal
*/
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
}
// 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()
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)
}
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)
}
- })
+ }
}
}
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
+ }
+ }
+ }
+ }
}
/**
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.`)