Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import type { BaseTransitionProps } from './components/BaseTransition'
import type { DefineComponent } from './apiDefineComponent'
import { markAsyncBoundary } from './helpers/useId'
import { isAsyncWrapper } from './apiAsyncComponent'
import type { RendererElement } from './renderer'

export type Data = Record<string, unknown>

Expand Down Expand Up @@ -1263,4 +1264,8 @@ export interface ComponentCustomElementInterface {
shouldReflect?: boolean,
shouldUpdate?: boolean,
): void
/**
* @internal attached by the nested Teleport when shadowRoot is false.
*/
_teleportTarget?: RendererElement
}
3 changes: 3 additions & 0 deletions packages/runtime-core/src/components/Teleport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ export const TeleportImpl = {
// Teleport *always* has Array children. This is enforced in both the
// compiler and vnode children normalization.
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (parentComponent && parentComponent.isCE) {
parentComponent.ce!._teleportTarget = container
}
mountChildren(
children as VNodeArrayChildren,
container,
Expand Down
88 changes: 88 additions & 0 deletions packages/runtime-dom/__tests__/customElement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { MockedFunction } from 'vitest'
import {
type HMRRuntime,
type Ref,
Teleport,
type VueElement,
createApp,
defineAsyncComponent,
Expand Down Expand Up @@ -975,6 +976,93 @@ describe('defineCustomElement', () => {
`<span>default</span>text` + `<!---->` + `<div>fallback</div>`,
)
})

test('render nested customElement w/ shadowRoot false', async () => {
const Child = defineCustomElement(
{
render() {
return renderSlot(this.$slots, 'default')
},
},
{ shadowRoot: false },
)
customElements.define('my-child', Child)

const Parent = defineCustomElement(
{
render() {
return renderSlot(this.$slots, 'default')
},
},
{ shadowRoot: false },
)
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 = createApp(App)
app.mount(container)
await nextTick()
const e = container.childNodes[0] as VueElement
expect(e.innerHTML).toBe(
`<my-child data-v-app=""><span>default</span></my-child>`,
)
app.unmount()
})

test('render nested Teleport w/ shadowRoot false', async () => {
const target = document.createElement('div')
const Child = defineCustomElement(
{
render() {
return h(
Teleport,
{ to: target },
{
default: () => [renderSlot(this.$slots, 'default')],
},
)
},
},
{ shadowRoot: false },
)
customElements.define('my-el-teleport-child', Child)
const Parent = defineCustomElement(
{
render() {
return renderSlot(this.$slots, 'default')
},
},
{ shadowRoot: false },
)
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 = createApp(App)
app.mount(container)
await nextTick()
expect(target.innerHTML).toBe(`<span>default</span>`)
app.unmount()
})
})

describe('helpers', () => {
Expand Down
9 changes: 7 additions & 2 deletions packages/runtime-dom/src/apiCustomElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ export class VueElement
*/
_nonce: string | undefined = this._def.nonce

/**
* @internal
*/
_teleportTarget?: HTMLElement

private _connected = false
private _resolved = false
private _numberProps: Record<string, true> | null = null
Expand Down Expand Up @@ -272,7 +277,7 @@ export class VueElement
}

connectedCallback(): void {
if (!this.shadowRoot) {
if (!this.shadowRoot && !this._slots) {
this._parseSlots()
}
this._connected = true
Expand Down Expand Up @@ -618,7 +623,7 @@ export class VueElement
* Only called when shaddowRoot is false
*/
private _renderSlots() {
const outlets = this.querySelectorAll('slot')
const outlets = (this._teleportTarget || this).querySelectorAll('slot')
const scopeId = this._instance!.type.__scopeId
for (let i = 0; i < outlets.length; i++) {
const o = outlets[i] as HTMLSlotElement
Expand Down
43 changes: 43 additions & 0 deletions packages/vue/__tests__/e2e/ssr-custom-element.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,49 @@ test('ssr custom element hydration', async () => {
await assertInteraction('my-element-async')
})

test('work with Teleport (shadowRoot: false)', async () => {
await setContent(
`<div id='test'></div><my-p><my-y><span>default</span></my-y></my-p>`,
)

await page().evaluate(() => {
const { h, defineSSRCustomElement, Teleport, renderSlot } = (window as any)
.Vue
const Y = defineSSRCustomElement(
{
render() {
return h(
Teleport,
{ to: '#test' },
{
default: () => [renderSlot(this.$slots, 'default')],
},
)
},
},
{ shadowRoot: false },
)
customElements.define('my-y', Y)
const P = defineSSRCustomElement(
{
render() {
return renderSlot(this.$slots, 'default')
},
},
{ shadowRoot: false },
)
customElements.define('my-p', P)
})

function getInnerHTML() {
return page().evaluate(() => {
return (document.querySelector('#test') as any).innerHTML
})
}

expect(await getInnerHTML()).toBe('<span>default</span>')
})

// #11641
test('pass key to custom element', async () => {
const messages: string[] = []
Expand Down