From: daiwei Date: Tue, 28 Oct 2025 14:05:51 +0000 (+0800) Subject: test: enhance nested custom element and teleport rendering with shadowRoot: false X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=ccbb1703fa0b10c414e965bb3e5968a03fe1d4f2;p=thirdparty%2Fvuejs%2Fcore.git test: enhance nested custom element and teleport rendering with shadowRoot: false --- diff --git a/packages/runtime-vapor/__tests__/customElement.spec.ts b/packages/runtime-vapor/__tests__/customElement.spec.ts index 3f0f35fa36..f7ef9f8b82 100644 --- a/packages/runtime-vapor/__tests__/customElement.spec.ts +++ b/packages/runtime-vapor/__tests__/customElement.spec.ts @@ -5,11 +5,13 @@ import { type Ref, inject, nextTick, + onMounted, provide, ref, toDisplayString, } from '@vue/runtime-dom' import { + VaporTeleport, child, createComponent, createComponentWithFallback, @@ -1368,189 +1370,209 @@ describe('defineVaporCustomElement', () => { ) }) - // test('render nested customElement w/ shadowRoot false', async () => { - // const calls: string[] = [] + test('render nested customElement w/ shadowRoot false', async () => { + const calls: string[] = [] - // const Child = defineVaporCustomElement( - // { - // setup() { - // calls.push('child rendering') - // onMounted(() => { - // calls.push('child mounted') - // }) - // }, - // render() { - // return renderSlot(this.$slots, 'default') - // }, - // }, - // { shadowRoot: false }, - // ) - // customElements.define('my-child', Child) + const Child = defineVaporCustomElement( + { + setup() { + calls.push('child rendering') + onMounted(() => { + calls.push('child mounted') + }) + return createSlot('default') + }, + }, + { shadowRoot: false } as any, + ) + customElements.define('my-child', Child) - // const Parent = defineVaporCustomElement( - // { - // setup() { - // calls.push('parent rendering') - // onMounted(() => { - // calls.push('parent mounted') - // }) - // }, - // render() { - // return renderSlot(this.$slots, 'default') - // }, - // }, - // { shadowRoot: false }, - // ) - // customElements.define('my-parent', Parent) + const Parent = defineVaporCustomElement( + { + setup() { + calls.push('parent rendering') + onMounted(() => { + calls.push('parent mounted') + }) + return createSlot('default') + }, + }, + { shadowRoot: false } as any, + ) + customElements.define('my-parent', Parent) - // const App = { - // render() { - // return h('my-parent', null, { - // default: () => [ - // h('my-child', null, { - // default: () => [h('span', null, 'default')], - // }), - // ], - // }) - // }, - // } - // const app = createVaporApp(App) - // app.mount(container) - // await nextTick() - // const e = container.childNodes[0] as VaporElement - // expect(e.innerHTML).toBe( - // `default`, - // ) - // expect(calls).toEqual([ - // 'parent rendering', - // 'parent mounted', - // 'child rendering', - // 'child mounted', - // ]) - // app.unmount() - // }) + const App = { + setup() { + return createComponentWithFallback('my-parent', null, { + default: () => + createComponentWithFallback('my-child', null, { + default: () => template('default')(), + }), + }) + }, + } + const app = createVaporApp(App) + app.mount(container) + await nextTick() + const e = container.childNodes[0] as VaporElement + expect(e.innerHTML).toBe( + `default`, + ) + expect(calls).toEqual([ + 'parent rendering', + 'parent mounted', + 'child rendering', + 'child mounted', + ]) + app.unmount() + }) - // test('render nested Teleport w/ shadowRoot false', async () => { - // const target = document.createElement('div') - // const Child = defineVaporCustomElement( - // { - // render() { - // return h( - // Teleport, - // { to: target }, - // { - // default: () => [renderSlot(this.$slots, 'default')], - // }, - // ) - // }, - // }, - // { shadowRoot: false }, - // ) - // customElements.define('my-el-teleport-child', Child) - // const Parent = defineVaporCustomElement( - // { - // render() { - // return renderSlot(this.$slots, 'default') - // }, - // }, - // { shadowRoot: false }, - // ) - // customElements.define('my-el-teleport-parent', Parent) + test('render nested Teleport w/ shadowRoot false', async () => { + const target = document.createElement('div') + const Child = defineVaporCustomElement( + { + setup() { + return createComponent( + VaporTeleport, + { to: () => target }, + { + default: () => createSlot('default'), + }, + ) + }, + }, + { shadowRoot: false } as any, + ) + customElements.define('my-el-teleport-child', Child) + const Parent = defineVaporCustomElement( + { + setup() { + return createSlot('default') + }, + }, + { shadowRoot: false } as any, + ) + customElements.define('my-el-teleport-parent', Parent) - // const App = { - // render() { - // return h('my-el-teleport-parent', null, { - // default: () => [ - // h('my-el-teleport-child', null, { - // default: () => [h('span', null, 'default')], - // }), - // ], - // }) - // }, - // } - // const app = createVaporApp(App) - // app.mount(container) - // await nextTick() - // expect(target.innerHTML).toBe(`default`) - // app.unmount() - // }) + const App = { + setup() { + return createComponentWithFallback('my-el-teleport-parent', null, { + default: () => + createComponentWithFallback('my-el-teleport-child', null, { + default: () => template('default')(), + }), + }) + }, + } + const app = createVaporApp(App) + app.mount(container) + await nextTick() + expect(target.innerHTML).toBe(`default`) + app.unmount() + }) - // test('render two Teleports w/ shadowRoot false', async () => { - // const target1 = document.createElement('div') - // const target2 = document.createElement('span') - // const Child = defineVaporCustomElement( - // { - // render() { - // return [ - // h(Teleport, { to: target1 }, [renderSlot(this.$slots, 'header')]), - // h(Teleport, { to: target2 }, [renderSlot(this.$slots, 'body')]), - // ] - // }, - // }, - // { shadowRoot: false }, - // ) - // customElements.define('my-el-two-teleport-child', Child) + test('render two Teleports w/ shadowRoot false', async () => { + const target1 = document.createElement('div') + const target2 = document.createElement('span') + const Child = defineVaporCustomElement( + { + setup() { + return [ + createComponent( + VaporTeleport, + { to: () => target1 }, + { + default: () => createSlot('header'), + }, + ), + createComponent( + VaporTeleport, + { to: () => target2 }, + { + default: () => createSlot('body'), + }, + ), + ] + }, + }, + { shadowRoot: false } as any, + ) + customElements.define('my-el-two-teleport-child', Child) - // const App = { - // render() { - // return h('my-el-two-teleport-child', null, { - // default: () => [ - // h('div', { slot: 'header' }, 'header'), - // h('span', { slot: 'body' }, 'body'), - // ], - // }) - // }, - // } - // const app = createVaporApp(App) - // app.mount(container) - // await nextTick() - // expect(target1.outerHTML).toBe( - // `
header
`, - // ) - // expect(target2.outerHTML).toBe( - // `body`, - // ) - // app.unmount() - // }) + const App = { + setup() { + return createComponentWithFallback('my-el-two-teleport-child', null, { + default: () => [ + template('
header
')(), + template('body')(), + ], + }) + }, + } + const app = createVaporApp(App) + app.mount(container) + await nextTick() + expect(target1.outerHTML).toBe( + `
header
`, + ) + expect(target2.outerHTML).toBe( + `body`, + ) + app.unmount() + }) - // test('render two Teleports w/ shadowRoot false (with disabled)', async () => { - // const target1 = document.createElement('div') - // const target2 = document.createElement('span') - // const Child = defineVaporCustomElement( - // { - // render() { - // return [ - // // with disabled: true - // h(Teleport, { to: target1, disabled: true }, [ - // renderSlot(this.$slots, 'header'), - // ]), - // h(Teleport, { to: target2 }, [renderSlot(this.$slots, 'body')]), - // ] - // }, - // }, - // { shadowRoot: false }, - // ) - // customElements.define('my-el-two-teleport-child-0', Child) + test('render two Teleports w/ shadowRoot false (with disabled)', async () => { + const target1 = document.createElement('div') + const target2 = document.createElement('span') + const Child = defineVaporCustomElement( + { + setup() { + return [ + createComponent( + VaporTeleport, + // with disabled: true + { to: () => target1, disabled: () => true }, + { + default: () => createSlot('header'), + }, + ), + createComponent( + VaporTeleport, + { to: () => target2 }, + { + default: () => createSlot('body'), + }, + ), + ] + }, + }, + { shadowRoot: false } as any, + ) + customElements.define('my-el-two-teleport-child-0', Child) - // const App = { - // render() { - // return h('my-el-two-teleport-child-0', null, { - // default: () => [ - // h('div', { slot: 'header' }, 'header'), - // h('span', { slot: 'body' }, 'body'), - // ], - // }) - // }, - // } - // const app = createVaporApp(App) - // app.mount(container) - // await nextTick() - // expect(target1.outerHTML).toBe(`
`) - // expect(target2.outerHTML).toBe( - // `body`, - // ) - // app.unmount() - // }) + const App = { + setup() { + return createComponentWithFallback( + 'my-el-two-teleport-child-0', + null, + { + default: () => [ + template('
header
')(), + template('body')(), + ], + }, + ) + }, + } + const app = createVaporApp(App) + app.mount(container) + await nextTick() + expect(target1.outerHTML).toBe(`
`) + expect(target2.outerHTML).toBe( + `body`, + ) + app.unmount() + }) // test('toggle nested custom element with shadowRoot: false', async () => { // customElements.define( diff --git a/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts b/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts index 4f01a50ed4..a3158cc3ab 100644 --- a/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts +++ b/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts @@ -95,6 +95,12 @@ export class VaporElement extends VueElementBase< this._createComponent() this._app!.mount(this._root) + + // Render slots immediately after mount for shadowRoot: false + // This ensures correct lifecycle order for nested custom elements + if (!this.shadowRoot) { + this._renderSlots() + } } protected _update(): void { @@ -176,8 +182,11 @@ export class VaporElement extends VueElementBase< private _createComponent() { this._def.ce = instance => { this._app!._ceComponent = this._instance = instance + // For shadowRoot: false, _renderSlots is called synchronously after mount + // in _mount() to ensure correct lifecycle order if (!this.shadowRoot) { - this._instance!.m = this._instance!.u = [this._renderSlots.bind(this)] + // Still set updated hooks for subsequent updates + this._instance!.u = [this._renderSlots.bind(this)] } this._processInstance() } diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index ef3d4598c9..7236786743 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -1,7 +1,9 @@ import { + type GenericComponentInstance, MismatchTypes, type TeleportProps, type TeleportTargetElement, + currentInstance, isMismatchAllowed, isTeleportDeferred, isTeleportDisabled, @@ -53,11 +55,13 @@ export class TeleportFragment extends VaporFragment { placeholder?: Node mountContainer?: ParentNode | null mountAnchor?: Node | null + parentComponent: GenericComponentInstance constructor(props: LooseRawProps, slots: LooseRawSlots) { super([]) this.rawProps = props this.rawSlots = slots + this.parentComponent = currentInstance as GenericComponentInstance this.anchor = isHydrating ? undefined : __DEV__ @@ -145,6 +149,14 @@ export class TeleportFragment extends VaporFragment { insert((this.targetAnchor = createTextNode('')), target) } + // track CE teleport targets + if (this.parentComponent && this.parentComponent.isCE) { + ;( + this.parentComponent.ce!._teleportTargets || + (this.parentComponent.ce!._teleportTargets = new Set()) + ).add(target) + } + mount(target, this.targetAnchor!) } else if (__DEV__) { warn(