Skip to content

Commit f2d8019

Browse files
authored
fix(custom-element): handle nested customElement mount w/ shadowRoot false (#11861)
close #11851 close #11871
1 parent 1d99d61 commit f2d8019

File tree

5 files changed

+172
-4
lines changed

5 files changed

+172
-4
lines changed

Diff for: packages/runtime-core/src/component.ts

+5
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ import type { BaseTransitionProps } from './components/BaseTransition'
9494
import type { DefineComponent } from './apiDefineComponent'
9595
import { markAsyncBoundary } from './helpers/useId'
9696
import { isAsyncWrapper } from './apiAsyncComponent'
97+
import type { RendererElement } from './renderer'
9798

9899
export type Data = Record<string, unknown>
99100

@@ -1263,4 +1264,8 @@ export interface ComponentCustomElementInterface {
12631264
shouldReflect?: boolean,
12641265
shouldUpdate?: boolean,
12651266
): void
1267+
/**
1268+
* @internal attached by the nested Teleport when shadowRoot is false.
1269+
*/
1270+
_teleportTarget?: RendererElement
12661271
}

Diff for: packages/runtime-core/src/components/Teleport.ts

+3
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ export const TeleportImpl = {
119119
// Teleport *always* has Array children. This is enforced in both the
120120
// compiler and vnode children normalization.
121121
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
122+
if (parentComponent && parentComponent.isCE) {
123+
parentComponent.ce!._teleportTarget = container
124+
}
122125
mountChildren(
123126
children as VNodeArrayChildren,
124127
container,

Diff for: packages/runtime-dom/__tests__/customElement.spec.ts

+109
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { MockedFunction } from 'vitest'
22
import {
33
type HMRRuntime,
44
type Ref,
5+
Teleport,
56
type VueElement,
67
createApp,
78
defineAsyncComponent,
@@ -10,6 +11,7 @@ import {
1011
h,
1112
inject,
1213
nextTick,
14+
onMounted,
1315
provide,
1416
ref,
1517
render,
@@ -975,6 +977,113 @@ describe('defineCustomElement', () => {
975977
`<span>default</span>text` + `<!---->` + `<div>fallback</div>`,
976978
)
977979
})
980+
981+
test('render nested customElement w/ shadowRoot false', async () => {
982+
const calls: string[] = []
983+
984+
const Child = defineCustomElement(
985+
{
986+
setup() {
987+
calls.push('child rendering')
988+
onMounted(() => {
989+
calls.push('child mounted')
990+
})
991+
},
992+
render() {
993+
return renderSlot(this.$slots, 'default')
994+
},
995+
},
996+
{ shadowRoot: false },
997+
)
998+
customElements.define('my-child', Child)
999+
1000+
const Parent = defineCustomElement(
1001+
{
1002+
setup() {
1003+
calls.push('parent rendering')
1004+
onMounted(() => {
1005+
calls.push('parent mounted')
1006+
})
1007+
},
1008+
render() {
1009+
return renderSlot(this.$slots, 'default')
1010+
},
1011+
},
1012+
{ shadowRoot: false },
1013+
)
1014+
customElements.define('my-parent', Parent)
1015+
1016+
const App = {
1017+
render() {
1018+
return h('my-parent', null, {
1019+
default: () => [
1020+
h('my-child', null, {
1021+
default: () => [h('span', null, 'default')],
1022+
}),
1023+
],
1024+
})
1025+
},
1026+
}
1027+
const app = createApp(App)
1028+
app.mount(container)
1029+
await nextTick()
1030+
const e = container.childNodes[0] as VueElement
1031+
expect(e.innerHTML).toBe(
1032+
`<my-child data-v-app=""><span>default</span></my-child>`,
1033+
)
1034+
expect(calls).toEqual([
1035+
'parent rendering',
1036+
'parent mounted',
1037+
'child rendering',
1038+
'child mounted',
1039+
])
1040+
app.unmount()
1041+
})
1042+
1043+
test('render nested Teleport w/ shadowRoot false', async () => {
1044+
const target = document.createElement('div')
1045+
const Child = defineCustomElement(
1046+
{
1047+
render() {
1048+
return h(
1049+
Teleport,
1050+
{ to: target },
1051+
{
1052+
default: () => [renderSlot(this.$slots, 'default')],
1053+
},
1054+
)
1055+
},
1056+
},
1057+
{ shadowRoot: false },
1058+
)
1059+
customElements.define('my-el-teleport-child', Child)
1060+
const Parent = defineCustomElement(
1061+
{
1062+
render() {
1063+
return renderSlot(this.$slots, 'default')
1064+
},
1065+
},
1066+
{ shadowRoot: false },
1067+
)
1068+
customElements.define('my-el-teleport-parent', Parent)
1069+
1070+
const App = {
1071+
render() {
1072+
return h('my-el-teleport-parent', null, {
1073+
default: () => [
1074+
h('my-el-teleport-child', null, {
1075+
default: () => [h('span', null, 'default')],
1076+
}),
1077+
],
1078+
})
1079+
},
1080+
}
1081+
const app = createApp(App)
1082+
app.mount(container)
1083+
await nextTick()
1084+
expect(target.innerHTML).toBe(`<span>default</span>`)
1085+
app.unmount()
1086+
})
9781087
})
9791088

9801089
describe('helpers', () => {

Diff for: packages/runtime-dom/src/apiCustomElement.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ export class VueElement
221221
*/
222222
_nonce: string | undefined = this._def.nonce
223223

224+
/**
225+
* @internal
226+
*/
227+
_teleportTarget?: HTMLElement
228+
224229
private _connected = false
225230
private _resolved = false
226231
private _numberProps: Record<string, true> | null = null
@@ -272,6 +277,9 @@ export class VueElement
272277
}
273278

274279
connectedCallback(): void {
280+
// avoid resolving component if it's not connected
281+
if (!this.isConnected) return
282+
275283
if (!this.shadowRoot) {
276284
this._parseSlots()
277285
}
@@ -322,7 +330,7 @@ export class VueElement
322330
}
323331
// unmount
324332
this._app && this._app.unmount()
325-
this._instance!.ce = undefined
333+
if (this._instance) this._instance.ce = undefined
326334
this._app = this._instance = null
327335
}
328336
})
@@ -601,7 +609,7 @@ export class VueElement
601609
}
602610

603611
/**
604-
* Only called when shaddowRoot is false
612+
* Only called when shadowRoot is false
605613
*/
606614
private _parseSlots() {
607615
const slots: VueElement['_slots'] = (this._slots = {})
@@ -615,10 +623,10 @@ export class VueElement
615623
}
616624

617625
/**
618-
* Only called when shaddowRoot is false
626+
* Only called when shadowRoot is false
619627
*/
620628
private _renderSlots() {
621-
const outlets = this.querySelectorAll('slot')
629+
const outlets = (this._teleportTarget || this).querySelectorAll('slot')
622630
const scopeId = this._instance!.type.__scopeId
623631
for (let i = 0; i < outlets.length; i++) {
624632
const o = outlets[i] as HTMLSlotElement

Diff for: packages/vue/__tests__/e2e/ssr-custom-element.spec.ts

+43
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,49 @@ test('ssr custom element hydration', async () => {
7878
await assertInteraction('my-element-async')
7979
})
8080

81+
test('work with Teleport (shadowRoot: false)', async () => {
82+
await setContent(
83+
`<div id='test'></div><my-p><my-y><span>default</span></my-y></my-p>`,
84+
)
85+
86+
await page().evaluate(() => {
87+
const { h, defineSSRCustomElement, Teleport, renderSlot } = (window as any)
88+
.Vue
89+
const Y = defineSSRCustomElement(
90+
{
91+
render() {
92+
return h(
93+
Teleport,
94+
{ to: '#test' },
95+
{
96+
default: () => [renderSlot(this.$slots, 'default')],
97+
},
98+
)
99+
},
100+
},
101+
{ shadowRoot: false },
102+
)
103+
customElements.define('my-y', Y)
104+
const P = defineSSRCustomElement(
105+
{
106+
render() {
107+
return renderSlot(this.$slots, 'default')
108+
},
109+
},
110+
{ shadowRoot: false },
111+
)
112+
customElements.define('my-p', P)
113+
})
114+
115+
function getInnerHTML() {
116+
return page().evaluate(() => {
117+
return (document.querySelector('#test') as any).innerHTML
118+
})
119+
}
120+
121+
expect(await getInnerHTML()).toBe('<span>default</span>')
122+
})
123+
81124
// #11641
82125
test('pass key to custom element', async () => {
83126
const messages: string[] = []

0 commit comments

Comments
 (0)