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', () => {
`
`,
)
})
+
+ 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
}
}
}