Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(custom-element): handle nested customElement mount w/ shadowRoot false #11861

Merged
merged 18 commits into from
Sep 13, 2024
Merged
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
/**
* Only effective 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) {
edison1105 marked this conversation as resolved.
Show resolved Hide resolved
parentComponent.ce!._teleportTarget = container
}
mountChildren(
children as VNodeArrayChildren,
container,
Expand Down
86 changes: 86 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,91 @@ describe('defineCustomElement', () => {
`<span>default</span>text` + `<!---->` + `<div>fallback</div>`,
)
})
test('should render slots with nested customElement', async () => {
const Son = defineCustomElement(
{
render() {
return renderSlot(this.$slots, 'default')
},
},
{ shadowRoot: false },
)
customElements.define('my-son', Son)
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-son', null, {
default: () => [h('span', null, 'default')],
}),
],
})
},
}
const app = createApp(App)
app.mount(container)
await new Promise(r => setTimeout(r))
const e = container.childNodes[0] as VueElement
expect(e.innerHTML).toBe(
`<my-son data-v-app=""><span>default</span></my-son>`,
)
app.unmount()
})

test('should work with Teleport', async () => {
const target = document.createElement('div')
const Y = defineCustomElement(
{
render() {
return h(
Teleport,
{ to: target },
{
default: () => [renderSlot(this.$slots, 'default')],
},
)
},
},
{ shadowRoot: false },
)
customElements.define('my-y', Y)
const P = defineCustomElement(
{
render() {
return renderSlot(this.$slots, 'default')
},
},
{ shadowRoot: false },
)
customElements.define('my-p', P)

const App = {
render() {
return h('my-p', null, {
default: () => [
h('my-y', null, {
default: () => [h('span', null, 'default')],
}),
],
})
},
}
const app = createApp(App)
app.mount(container)
await new Promise(r => setTimeout(r))
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 @@ -238,6 +238,11 @@ export class VueElement
private _ob?: MutationObserver | null = null
private _slots?: Record<string, Node[]>

/**
* Only effective when shadowRoot is false.
*/
_teleportTarget?: HTMLElement

constructor(
/**
* Component def - note this may be an AsyncWrapper, and this._def will
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