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 type { DefineComponent } from './apiDefineComponent'
|
||||||
import { markAsyncBoundary } from './helpers/useId'
|
import { markAsyncBoundary } from './helpers/useId'
|
||||||
import { isAsyncWrapper } from './apiAsyncComponent'
|
import { isAsyncWrapper } from './apiAsyncComponent'
|
||||||
|
import type { RendererElement } from './renderer'
|
||||||
|
|
||||||
export type Data = Record<string, unknown>
|
export type Data = Record<string, unknown>
|
||||||
|
|
||||||
|
@ -1263,4 +1264,8 @@ export interface ComponentCustomElementInterface {
|
||||||
shouldReflect?: boolean,
|
shouldReflect?: boolean,
|
||||||
shouldUpdate?: boolean,
|
shouldUpdate?: boolean,
|
||||||
): void
|
): 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
|
// Teleport *always* has Array children. This is enforced in both the
|
||||||
// compiler and vnode children normalization.
|
// compiler and vnode children normalization.
|
||||||
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
|
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
|
||||||
|
if (parentComponent && parentComponent.isCE) {
|
||||||
|
parentComponent.ce!._teleportTarget = container
|
||||||
|
}
|
||||||
mountChildren(
|
mountChildren(
|
||||||
children as VNodeArrayChildren,
|
children as VNodeArrayChildren,
|
||||||
container,
|
container,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { MockedFunction } from 'vitest'
|
||||||
import {
|
import {
|
||||||
type HMRRuntime,
|
type HMRRuntime,
|
||||||
type Ref,
|
type Ref,
|
||||||
|
Teleport,
|
||||||
type VueElement,
|
type VueElement,
|
||||||
createApp,
|
createApp,
|
||||||
defineAsyncComponent,
|
defineAsyncComponent,
|
||||||
|
@ -10,6 +11,7 @@ import {
|
||||||
h,
|
h,
|
||||||
inject,
|
inject,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
onMounted,
|
||||||
provide,
|
provide,
|
||||||
ref,
|
ref,
|
||||||
render,
|
render,
|
||||||
|
@ -975,6 +977,113 @@ describe('defineCustomElement', () => {
|
||||||
`<span>default</span>text` + `<!---->` + `<div>fallback</div>`,
|
`<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', () => {
|
describe('helpers', () => {
|
||||||
|
|
|
@ -221,6 +221,11 @@ export class VueElement
|
||||||
*/
|
*/
|
||||||
_nonce: string | undefined = this._def.nonce
|
_nonce: string | undefined = this._def.nonce
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_teleportTarget?: HTMLElement
|
||||||
|
|
||||||
private _connected = false
|
private _connected = false
|
||||||
private _resolved = false
|
private _resolved = false
|
||||||
private _numberProps: Record<string, true> | null = null
|
private _numberProps: Record<string, true> | null = null
|
||||||
|
@ -272,6 +277,9 @@ export class VueElement
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
|
// avoid resolving component if it's not connected
|
||||||
|
if (!this.isConnected) return
|
||||||
|
|
||||||
if (!this.shadowRoot) {
|
if (!this.shadowRoot) {
|
||||||
this._parseSlots()
|
this._parseSlots()
|
||||||
}
|
}
|
||||||
|
@ -322,7 +330,7 @@ export class VueElement
|
||||||
}
|
}
|
||||||
// unmount
|
// unmount
|
||||||
this._app && this._app.unmount()
|
this._app && this._app.unmount()
|
||||||
this._instance!.ce = undefined
|
if (this._instance) this._instance.ce = undefined
|
||||||
this._app = this._instance = null
|
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() {
|
private _parseSlots() {
|
||||||
const slots: VueElement['_slots'] = (this._slots = {})
|
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() {
|
private _renderSlots() {
|
||||||
const outlets = this.querySelectorAll('slot')
|
const outlets = (this._teleportTarget || this).querySelectorAll('slot')
|
||||||
const scopeId = this._instance!.type.__scopeId
|
const scopeId = this._instance!.type.__scopeId
|
||||||
for (let i = 0; i < outlets.length; i++) {
|
for (let i = 0; i < outlets.length; i++) {
|
||||||
const o = outlets[i] as HTMLSlotElement
|
const o = outlets[i] as HTMLSlotElement
|
||||||
|
|
|
@ -78,6 +78,49 @@ test('ssr custom element hydration', async () => {
|
||||||
await assertInteraction('my-element-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
|
// #11641
|
||||||
test('pass key to custom element', async () => {
|
test('pass key to custom element', async () => {
|
||||||
const messages: string[] = []
|
const messages: string[] = []
|
||||||
|
|
Loading…
Reference in New Issue