Skip to content

Commit

Permalink
fix(custom-element): handle nested customElement mount w/ shadowRoot …
Browse files Browse the repository at this point in the history
…false (#11861)

close #11851
close #11871
  • Loading branch information
linzhe141 committed Sep 13, 2024
1 parent 1d99d61 commit f2d8019
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 4 deletions.
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
109 changes: 109 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 All @@ -10,6 +11,7 @@ import {
h,
inject,
nextTick,
onMounted,
provide,
ref,
render,
Expand Down Expand Up @@ -975,6 +977,113 @@ describe('defineCustomElement', () => {
`<span>default</span>text` + `<!---->` + `<div>fallback</div>`,
)
})

test('render nested customElement w/ shadowRoot false', async () => {
const calls: string[] = []

const Child = defineCustomElement(
{
setup() {
calls.push('child rendering')
onMounted(() => {
calls.push('child mounted')
})
},
render() {
return renderSlot(this.$slots, 'default')
},
},
{ shadowRoot: false },
)
customElements.define('my-child', Child)

const Parent = defineCustomElement(
{
setup() {
calls.push('parent rendering')
onMounted(() => {
calls.push('parent mounted')
})
},
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>`,
)
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 = 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
16 changes: 12 additions & 4 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,6 +277,9 @@ export class VueElement
}

connectedCallback(): void {
// avoid resolving component if it's not connected
if (!this.isConnected) return

if (!this.shadowRoot) {
this._parseSlots()
}
Expand Down Expand Up @@ -322,7 +330,7 @@ export class VueElement
}
// unmount
this._app && this._app.unmount()
this._instance!.ce = undefined
if (this._instance) this._instance.ce = undefined
this._app = this._instance = null
}
})
Expand Down Expand Up @@ -601,7 +609,7 @@ export class VueElement
}

/**
* Only called when shaddowRoot is false
* Only called when shadowRoot is false
*/
private _parseSlots() {
const slots: VueElement['_slots'] = (this._slots = {})
Expand All @@ -615,10 +623,10 @@ export class VueElement
}

/**
* Only called when shaddowRoot is false
* Only called when shadowRoot 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

0 comments on commit f2d8019

Please sign in to comment.