mirror of https://github.com/vuejs/core.git
fix(custom-element): handle nested customElement mount w/ shadowRoot false (#11861)
close #11851 close #11871
This commit is contained in:
parent
1d99d61c1b
commit
f2d8019188
|
@ -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>
|
||||
|
||||
|
@ -1263,4 +1264,8 @@ export interface ComponentCustomElementInterface {
|
|||
shouldReflect?: boolean,
|
||||
shouldUpdate?: boolean,
|
||||
): void
|
||||
/**
|
||||
* @internal attached by the nested Teleport when shadowRoot is false.
|
||||
*/
|
||||
_teleportTarget?: RendererElement
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { MockedFunction } from 'vitest'
|
|||
import {
|
||||
type HMRRuntime,
|
||||
type Ref,
|
||||
Teleport,
|
||||
type VueElement,
|
||||
createApp,
|
||||
defineAsyncComponent,
|
||||
|
@ -10,6 +11,7 @@ import {
|
|||
h,
|
||||
inject,
|
||||
nextTick,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
render,
|
||||
|
@ -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', () => {
|
||||
|
|
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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 = {})
|
||||
|
@ -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
|
||||
|
|
|
@ -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[] = []
|
||||
|
|
Loading…
Reference in New Issue