diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index 62ba166b030..41d651e074f 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -1,4 +1,5 @@ import { + type Ref, type Ref, type VueElement, createApp, @@ -8,6 +9,7 @@ import { h, inject, nextTick, + provide, ref, render, renderSlot, @@ -771,5 +773,83 @@ describe('defineCustomElement', () => { `
fallback
`, ) }) + + test('async & nested custom elements', async () => { + let fooVal: string | undefined = '' + const E = defineCustomElement( + defineAsyncComponent(() => { + return Promise.resolve({ + setup(props) { + provide('foo', 'foo') + }, + render(this: any) { + return h('div', null, [renderSlot(this.$slots, 'default')]) + }, + }) + }), + ) + + const EChild = defineCustomElement({ + setup(props) { + fooVal = inject('foo') + }, + render(this: any) { + return h('div', null, 'child') + }, + }) + customElements.define('my-el-async-nested-ce', E) + customElements.define('slotted-child', EChild) + container.innerHTML = `
` + + await new Promise(r => setTimeout(r)) + const e = container.childNodes[0] as VueElement + expect(e.shadowRoot!.innerHTML).toBe(`
`) + expect(fooVal).toBe('foo') + }) + test('async & multiple levels of nested custom elements', async () => { + let fooVal: string | undefined = '' + let barVal: string | undefined = '' + const E = defineCustomElement( + defineAsyncComponent(() => { + return Promise.resolve({ + setup(props) { + provide('foo', 'foo') + }, + render(this: any) { + return h('div', null, [renderSlot(this.$slots, 'default')]) + }, + }) + }), + ) + + const EChild = defineCustomElement({ + setup(props) { + provide('bar', 'bar') + }, + render(this: any) { + return h('div', null, [renderSlot(this.$slots, 'default')]) + }, + }) + + const EChild2 = defineCustomElement({ + setup(props) { + fooVal = inject('foo') + barVal = inject('bar') + }, + render(this: any) { + return h('div', null, 'child') + }, + }) + customElements.define('my-el-async-nested-m-ce', E) + customElements.define('slotted-child-m', EChild) + customElements.define('slotted-child2-m', EChild2) + container.innerHTML = `
` + + await new Promise(r => setTimeout(r)) + const e = container.childNodes[0] as VueElement + expect(e.shadowRoot!.innerHTML).toBe(`
`) + expect(fooVal).toBe('foo') + expect(barVal).toBe('bar') + }) }) }) diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 2a96cafa0ea..42cd6c86319 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -193,6 +193,8 @@ export class VueElement extends BaseClass { private _numberProps: Record | null = null private _styles?: HTMLStyleElement[] private _ob?: MutationObserver | null = null + private _ce_parent?: VueElement | null = null + private _ce_children?: VueElement[] | null = null constructor( private _def: InnerComponentDef, private _props: Record = {}, @@ -222,7 +224,35 @@ export class VueElement extends BaseClass { if (this._resolved) { this._update() } else { - this._resolveDef() + let parent: Node | null = this + let isParentResolved = true + let isAncestors = false + while ( + (parent = + parent && (parent.parentNode || (parent as ShadowRoot).host)) + ) { + if (parent instanceof VueElement) { + // Find the first custom element in the ancestor and set it to `_ce_parent` + !isAncestors && (this._ce_parent = parent as VueElement) + if ( + !parent._resolved && + (parent._def as ComponentOptions).__asyncLoader + ) { + ;( + this._ce_parent!._ce_children || + (this._ce_parent!._ce_children = []) + ).push(this) + isParentResolved = false + } else { + isAncestors = true + continue + } + break + } + } + if (isParentResolved) { + this._resolveDef() + } } } } @@ -245,8 +275,6 @@ export class VueElement extends BaseClass { * resolve inner component definition (handle possible async component) */ private _resolveDef() { - this._resolved = true - // set initial attrs for (let i = 0; i < this.attributes.length; i++) { this._setAttr(this.attributes[i].name) @@ -262,6 +290,7 @@ export class VueElement extends BaseClass { this._ob.observe(this, { attributes: true }) const resolve = (def: InnerComponentDef, isAsync = false) => { + this._resolved = true const { props, styles } = def // cast Number-type props set before resolve @@ -292,6 +321,12 @@ export class VueElement extends BaseClass { // initial render this._update() + + // The asynchronous custom element needs to call + // the resolveDef function of the descendant custom element at the end. + if (this._ce_children) { + this._ce_children.forEach(child => child._resolveDef()) + } } const asyncDef = (this._def as ComponentOptions).__asyncLoader @@ -411,17 +446,9 @@ export class VueElement extends BaseClass { } } - // locate nearest Vue custom element parent for provide/inject - let parent: Node | null = this - while ( - (parent = - parent && (parent.parentNode || (parent as ShadowRoot).host)) - ) { - if (parent instanceof VueElement) { - instance.parent = parent._instance - instance.provides = parent._instance!.provides - break - } + if (this._ce_parent) { + instance.parent = this._ce_parent._instance + instance.provides = this._ce_parent._instance!.provides } } }