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>
shouldReflect?: boolean,
shouldUpdate?: boolean,
): void
+ /**
+ * @internal attached by the nested Teleport when shadowRoot is false.
+ */
+ _teleportTarget?: RendererElement
}
// 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,
import {
type HMRRuntime,
type Ref,
+ Teleport,
type VueElement,
createApp,
defineAsyncComponent,
h,
inject,
nextTick,
+ onMounted,
provide,
ref,
render,
`<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', () => {
*/
_nonce: string | undefined = this._def.nonce
+ /**
+ * @internal
+ */
+ _teleportTarget?: HTMLElement
+
private _connected = false
private _resolved = false
private _numberProps: Record<string, true> | null = null
}
connectedCallback(): void {
+ // avoid resolving component if it's not connected
+ if (!this.isConnected) return
+
if (!this.shadowRoot) {
this._parseSlots()
}
}
// unmount
this._app && this._app.unmount()
- this._instance!.ce = undefined
+ if (this._instance) this._instance.ce = undefined
this._app = this._instance = null
}
})
}
/**
- * Only called when shaddowRoot is false
+ * Only called when shadowRoot is false
*/
private _parseSlots() {
const slots: VueElement['_slots'] = (this._slots = {})
}
/**
- * 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
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[] = []