From 37ccb9b9a0e4381f9465e0fc6459609003030da4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 7 Aug 2024 02:18:54 +0800 Subject: [PATCH] fix(custom-element): delay mounting of custom elements with async parent close #8127 close #9341 close #9351 the fix is based on #9351 with reused tests --- .../__tests__/customElement.spec.ts | 85 +++++++++++++++++++ packages/runtime-dom/src/apiCustomElement.ts | 55 ++++++++---- 2 files changed, 125 insertions(+), 15 deletions(-) diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index 70af88ed466..52e677ea625 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -10,6 +10,7 @@ import { h, inject, nextTick, + provide, ref, render, renderSlot, @@ -1032,4 +1033,88 @@ describe('defineCustomElement', () => { ).toHaveBeenWarned() }) }) + + 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 4c3be8d4494..2684e97ea51 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -207,6 +207,8 @@ export class VueElement private _resolved = false private _numberProps: Record | null = null private _styleChildren = new WeakSet() + private _pendingResolve: Promise | undefined + private _parent: VueElement | undefined /** * dev only */ @@ -257,15 +259,42 @@ export class VueElement this._parseSlots() } this._connected = true + + // 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) { + this._parent = parent + break + } + } + if (!this._instance) { if (this._resolved) { + this._setParent() this._update() } else { - this._resolveDef() + if (parent && parent._pendingResolve) { + this._pendingResolve = parent._pendingResolve.then(() => { + this._pendingResolve = undefined + this._resolveDef() + }) + } else { + this._resolveDef() + } } } } + private _setParent(parent = this._parent) { + if (parent) { + this._instance!.parent = parent._instance + this._instance!.provides = parent._instance!.provides + } + } + disconnectedCallback() { this._connected = false nextTick(() => { @@ -285,7 +314,9 @@ export class VueElement * resolve inner component definition (handle possible async component) */ private _resolveDef() { - this._resolved = true + if (this._pendingResolve) { + return + } // set initial attrs for (let i = 0; i < this.attributes.length; i++) { @@ -302,6 +333,9 @@ export class VueElement this._ob.observe(this, { attributes: true }) const resolve = (def: InnerComponentDef, isAsync = false) => { + this._resolved = true + this._pendingResolve = undefined + const { props, styles } = def // cast Number-type props set before resolve @@ -346,7 +380,9 @@ export class VueElement const asyncDef = (this._def as ComponentOptions).__asyncLoader if (asyncDef) { - asyncDef().then(def => resolve((this._def = def), true)) + this._pendingResolve = asyncDef().then(def => + resolve((this._def = def), true), + ) } else { resolve(this._def) } @@ -486,18 +522,7 @@ export class VueElement } } - // 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 - } - } + this._setParent() } } return vnode