diff --git a/packages/runtime-core/__tests__/components/Teleport.spec.ts b/packages/runtime-core/__tests__/components/Teleport.spec.ts
index aca9432b6e1..24400f6ed40 100644
--- a/packages/runtime-core/__tests__/components/Teleport.spec.ts
+++ b/packages/runtime-core/__tests__/components/Teleport.spec.ts
@@ -7,617 +7,703 @@ import {
Text,
createApp,
defineComponent,
- h,
markRaw,
nextTick,
nodeOps,
+ h as originalH,
ref,
render,
serializeInner,
withDirectives,
} from '@vue/runtime-test'
import { Fragment, createCommentVNode, createVNode } from '../../src/vnode'
-import { compile, render as domRender } from 'vue'
+import { compile, createApp as createDOMApp, render as domRender } from 'vue'
describe('renderer: teleport', () => {
- test('should work', () => {
- const target = nodeOps.createElement('div')
- const root = nodeOps.createElement('div')
-
- render(
- h(() => [
- h(Teleport, { to: target }, h('div', 'teleported')),
- h('div', 'root'),
- ]),
- root,
- )
-
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"
root
"`,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(
- `"teleported
"`,
- )
+ describe('eager mode', () => {
+ runSharedTests(false)
})
- test('should work with SVG', async () => {
- const root = document.createElement('div')
- const svg = ref()
- const circle = ref()
+ describe('defer mode', () => {
+ runSharedTests(true)
- const Comp = defineComponent({
- setup() {
- return {
- svg,
- circle,
- }
- },
- template: `
-
-
-
- `,
- })
+ const h = originalH
- domRender(h(Comp), root)
+ test('should be able to target content appearing later than the teleport with defer', () => {
+ const root = document.createElement('div')
+ document.body.appendChild(root)
- await nextTick()
+ createDOMApp({
+ render() {
+ return [
+ h(Teleport, { to: '#target', defer: true }, h('div', 'teleported')),
+ h('div', { id: 'target' }),
+ ]
+ },
+ }).mount(root)
- expect(root.innerHTML).toMatchInlineSnapshot(
- `""`,
- )
+ expect(root.innerHTML).toMatchInlineSnapshot(
+ `""`,
+ )
+ })
- expect(svg.value.namespaceURI).toBe('http://www.w3.org/2000/svg')
- expect(circle.value.namespaceURI).toBe('http://www.w3.org/2000/svg')
+ test('defer mode should work inside suspense', async () => {
+ const root = document.createElement('div')
+ document.body.appendChild(root)
+
+ let p: Promise
+
+ const Comp = defineComponent({
+ template: `
+
+
+ `,
+ components: {
+ async: {
+ setup() {
+ p = Promise.resolve(() => 'async')
+ return p
+ },
+ },
+ },
+ })
+
+ domRender(h(Comp), root)
+ expect(root.innerHTML).toBe(``)
+
+ await p!.then(() => Promise.resolve())
+ await nextTick()
+ expect(root.innerHTML).toBe(
+ `` +
+ `async` +
+ `` +
+ `
` +
+ `
`,
+ )
+ })
})
- test('should update target', async () => {
- const targetA = nodeOps.createElement('div')
- const targetB = nodeOps.createElement('div')
- const target = ref(targetA)
- const root = nodeOps.createElement('div')
-
- render(
- h(() => [
- h(Teleport, { to: target.value }, h('div', 'teleported')),
- h('div', 'root'),
- ]),
- root,
- )
-
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"root
"`,
- )
- expect(serializeInner(targetA)).toMatchInlineSnapshot(
- `"teleported
"`,
- )
- expect(serializeInner(targetB)).toMatchInlineSnapshot(`""`)
-
- target.value = targetB
- await nextTick()
-
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"root
"`,
- )
- expect(serializeInner(targetA)).toMatchInlineSnapshot(`""`)
- expect(serializeInner(targetB)).toMatchInlineSnapshot(
- `"teleported
"`,
- )
- })
+ function runSharedTests(deferMode: boolean) {
+ const h = (deferMode
+ ? (type: any, props: any, ...args: any[]) => {
+ if (type === Teleport) {
+ props.defer = true
+ }
+ return originalH(type, props, ...args)
+ }
+ : originalH) as unknown as typeof originalH
- test('should update children', async () => {
- const target = nodeOps.createElement('div')
- const root = nodeOps.createElement('div')
- const children = ref([h('div', 'teleported')])
+ test('should work', () => {
+ const target = nodeOps.createElement('div')
+ const root = nodeOps.createElement('div')
- render(
- h(() => h(Teleport, { to: target }, children.value)),
- root,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(
- `"teleported
"`,
- )
+ render(
+ h(() => [
+ h(Teleport, { to: target }, h('div', 'teleported')),
+ h('div', 'root'),
+ ]),
+ root,
+ )
- children.value = []
- await nextTick()
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"root
"`,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ `"teleported
"`,
+ )
+ })
- expect(serializeInner(target)).toMatchInlineSnapshot(`""`)
+ test('should work with SVG', async () => {
+ const root = document.createElement('div')
+ const svg = ref()
+ const circle = ref()
+
+ const Comp = defineComponent({
+ setup() {
+ return {
+ svg,
+ circle,
+ }
+ },
+ template: `
+
+
+
+ `,
+ })
+
+ domRender(h(Comp), root)
+
+ await nextTick()
+
+ expect(root.innerHTML).toMatchInlineSnapshot(
+ `""`,
+ )
- children.value = [createVNode(Text, null, 'teleported')]
- await nextTick()
+ expect(svg.value.namespaceURI).toBe('http://www.w3.org/2000/svg')
+ expect(circle.value.namespaceURI).toBe('http://www.w3.org/2000/svg')
+ })
- expect(serializeInner(target)).toMatchInlineSnapshot(`"teleported"`)
- })
+ test('should update target', async () => {
+ const targetA = nodeOps.createElement('div')
+ const targetB = nodeOps.createElement('div')
+ const target = ref(targetA)
+ const root = nodeOps.createElement('div')
- test('should remove children when unmounted', () => {
- const target = nodeOps.createElement('div')
- const root = nodeOps.createElement('div')
+ render(
+ h(() => [
+ h(Teleport, { to: target.value }, h('div', 'teleported')),
+ h('div', 'root'),
+ ]),
+ root,
+ )
+
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"root
"`,
+ )
+ expect(serializeInner(targetA)).toMatchInlineSnapshot(
+ `"teleported
"`,
+ )
+ expect(serializeInner(targetB)).toMatchInlineSnapshot(`""`)
+
+ target.value = targetB
+ await nextTick()
+
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"root
"`,
+ )
+ expect(serializeInner(targetA)).toMatchInlineSnapshot(`""`)
+ expect(serializeInner(targetB)).toMatchInlineSnapshot(
+ `"teleported
"`,
+ )
+ })
+
+ test('should update children', async () => {
+ const target = nodeOps.createElement('div')
+ const root = nodeOps.createElement('div')
+ const children = ref([h('div', 'teleported')])
- function testUnmount(props: any) {
render(
- h(() => [h(Teleport, props, h('div', 'teleported')), h('div', 'root')]),
+ h(() => h(Teleport, { to: target }, children.value)),
root,
)
expect(serializeInner(target)).toMatchInlineSnapshot(
- props.disabled ? `""` : `"teleported
"`,
+ `"teleported
"`,
+ )
+
+ children.value = []
+ await nextTick()
+
+ expect(serializeInner(target)).toMatchInlineSnapshot(`""`)
+
+ children.value = [createVNode(Text, null, 'teleported')]
+ await nextTick()
+
+ expect(serializeInner(target)).toMatchInlineSnapshot(`"teleported"`)
+ })
+
+ test('should remove children when unmounted', () => {
+ const target = nodeOps.createElement('div')
+ const root = nodeOps.createElement('div')
+
+ function testUnmount(props: any) {
+ render(
+ h(() => [
+ h(Teleport, props, h('div', 'teleported')),
+ h('div', 'root'),
+ ]),
+ root,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ props.disabled ? `""` : `"teleported
"`,
+ )
+
+ render(null, root)
+ expect(serializeInner(target)).toBe('')
+ expect(target.children.length).toBe(0)
+ }
+
+ testUnmount({ to: target, disabled: false })
+ testUnmount({ to: target, disabled: true })
+ testUnmount({ to: null, disabled: true })
+ })
+
+ test('component with multi roots should be removed when unmounted', () => {
+ const target = nodeOps.createElement('div')
+ const root = nodeOps.createElement('div')
+
+ const Comp = {
+ render() {
+ return [h('p'), h('p')]
+ },
+ }
+
+ render(
+ h(() => [h(Teleport, { to: target }, h(Comp)), h('div', 'root')]),
+ root,
)
+ expect(serializeInner(target)).toMatchInlineSnapshot(`""`)
render(null, root)
expect(serializeInner(target)).toBe('')
- expect(target.children.length).toBe(0)
- }
+ })
- testUnmount({ to: target, disabled: false })
- testUnmount({ to: target, disabled: true })
- testUnmount({ to: null, disabled: true })
- })
+ // #6347
+ test('descendent component should be unmounted when teleport is disabled and unmounted', () => {
+ const root = nodeOps.createElement('div')
- test('component with multi roots should be removed when unmounted', () => {
- const target = nodeOps.createElement('div')
- const root = nodeOps.createElement('div')
+ const CompWithHook = {
+ render() {
+ return [h('p'), h('p')]
+ },
+ beforeUnmount: vi.fn(),
+ unmounted: vi.fn(),
+ }
- const Comp = {
- render() {
- return [h('p'), h('p')]
- },
- }
+ render(
+ h(() => [h(Teleport, { to: null, disabled: true }, h(CompWithHook))]),
+ root,
+ )
+ expect(CompWithHook.beforeUnmount).toBeCalledTimes(0)
+ expect(CompWithHook.unmounted).toBeCalledTimes(0)
- render(
- h(() => [h(Teleport, { to: target }, h(Comp)), h('div', 'root')]),
- root,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(`""`)
+ render(null, root)
- render(null, root)
- expect(serializeInner(target)).toBe('')
- })
+ expect(CompWithHook.beforeUnmount).toBeCalledTimes(1)
+ expect(CompWithHook.unmounted).toBeCalledTimes(1)
+ })
- // #6347
- test('descendent component should be unmounted when teleport is disabled and unmounted', () => {
- const root = nodeOps.createElement('div')
-
- const CompWithHook = {
- render() {
- return [h('p'), h('p')]
- },
- beforeUnmount: vi.fn(),
- unmounted: vi.fn(),
- }
-
- render(
- h(() => [h(Teleport, { to: null, disabled: true }, h(CompWithHook))]),
- root,
- )
- expect(CompWithHook.beforeUnmount).toBeCalledTimes(0)
- expect(CompWithHook.unmounted).toBeCalledTimes(0)
-
- render(null, root)
-
- expect(CompWithHook.beforeUnmount).toBeCalledTimes(1)
- expect(CompWithHook.unmounted).toBeCalledTimes(1)
- })
+ test('multiple teleport with same target', () => {
+ const target = nodeOps.createElement('div')
+ const root = nodeOps.createElement('div')
- test('multiple teleport with same target', () => {
- const target = nodeOps.createElement('div')
- const root = nodeOps.createElement('div')
-
- render(
- h('div', [
- h(Teleport, { to: target }, h('div', 'one')),
- h(Teleport, { to: target }, 'two'),
- ]),
- root,
- )
-
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `""`,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(`"one
two"`)
-
- // update existing content
- render(
- h('div', [
- h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
- h(Teleport, { to: target }, 'three'),
- ]),
- root,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(
- `"one
two
three"`,
- )
-
- // toggling
- render(h('div', [null, h(Teleport, { to: target }, 'three')]), root)
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `""`,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(`"three"`)
-
- // toggle back
- render(
- h('div', [
- h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
- h(Teleport, { to: target }, 'three'),
- ]),
- root,
- )
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `""`,
- )
- // should append
- expect(serializeInner(target)).toMatchInlineSnapshot(
- `"threeone
two
"`,
- )
-
- // toggle the other teleport
- render(
- h('div', [
- h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
- null,
- ]),
- root,
- )
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `""`,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(
- `"one
two
"`,
- )
- })
+ render(
+ h('div', [
+ h(Teleport, { to: target }, h('div', 'one')),
+ h(Teleport, { to: target }, 'two'),
+ ]),
+ root,
+ )
- test('should work when using template ref as target', async () => {
- const root = nodeOps.createElement('div')
- const target = ref(null)
- const disabled = ref(true)
-
- const App = {
- setup() {
- return () =>
- h(Fragment, [
- h('div', { ref: target }),
- h(
- Teleport,
- { to: target.value, disabled: disabled.value },
- h('div', 'teleported'),
- ),
- ])
- },
- }
- render(h(App), root)
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"teleported
"`,
- )
-
- disabled.value = false
- await nextTick()
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `""`,
- )
- })
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `""`,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ `"one
two"`,
+ )
- test('disabled', () => {
- const target = nodeOps.createElement('div')
- const root = nodeOps.createElement('div')
-
- const renderWithDisabled = (disabled: boolean) => {
- return h(Fragment, [
- h(Teleport, { to: target, disabled }, h('div', 'teleported')),
- h('div', 'root'),
- ])
- }
-
- render(renderWithDisabled(false), root)
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"root
"`,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(
- `"teleported
"`,
- )
-
- render(renderWithDisabled(true), root)
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"teleported
root
"`,
- )
- expect(serializeInner(target)).toBe(``)
-
- // toggle back
- render(renderWithDisabled(false), root)
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"root
"`,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(
- `"teleported
"`,
- )
- })
+ // update existing content
+ render(
+ h('div', [
+ h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
+ h(Teleport, { to: target }, 'three'),
+ ]),
+ root,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ `"one
two
three"`,
+ )
- test('moving teleport while enabled', () => {
- const target = nodeOps.createElement('div')
- const root = nodeOps.createElement('div')
-
- render(
- h(Fragment, [
- h(Teleport, { to: target }, h('div', 'teleported')),
- h('div', 'root'),
- ]),
- root,
- )
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"root
"`,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(
- `"teleported
"`,
- )
-
- render(
- h(Fragment, [
- h('div', 'root'),
- h(Teleport, { to: target }, h('div', 'teleported')),
- ]),
- root,
- )
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"root
"`,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(
- `"teleported
"`,
- )
-
- render(
- h(Fragment, [
- h(Teleport, { to: target }, h('div', 'teleported')),
- h('div', 'root'),
- ]),
- root,
- )
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"root
"`,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(
- `"teleported
"`,
- )
- })
+ // toggling
+ render(h('div', [null, h(Teleport, { to: target }, 'three')]), root)
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `""`,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(`"three"`)
- test('moving teleport while disabled', () => {
- const target = nodeOps.createElement('div')
- const root = nodeOps.createElement('div')
-
- render(
- h(Fragment, [
- h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
- h('div', 'root'),
- ]),
- root,
- )
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"teleported
root
"`,
- )
- expect(serializeInner(target)).toBe('')
-
- render(
- h(Fragment, [
- h('div', 'root'),
- h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
- ]),
- root,
- )
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"root
teleported
"`,
- )
- expect(serializeInner(target)).toBe('')
-
- render(
- h(Fragment, [
- h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
- h('div', 'root'),
- ]),
- root,
- )
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"teleported
root
"`,
- )
- expect(serializeInner(target)).toBe('')
- })
+ // toggle back
+ render(
+ h('div', [
+ h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
+ h(Teleport, { to: target }, 'three'),
+ ]),
+ root,
+ )
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `""`,
+ )
+ // should append
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ `"threeone
two
"`,
+ )
- test('should work with block tree', async () => {
- const target = nodeOps.createElement('div')
- const root = nodeOps.createElement('div')
- const disabled = ref(false)
+ // toggle the other teleport
+ render(
+ h('div', [
+ h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
+ null,
+ ]),
+ root,
+ )
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `""`,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ `"one
two
"`,
+ )
+ })
- const App = {
- setup() {
- return {
- target: markRaw(target),
- disabled,
- }
- },
- render: compile(`
-
- teleported
{{ disabled }}
-
- root
- `),
- }
- render(h(App), root)
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"root
"`,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(
- `"teleported
false"`,
- )
-
- disabled.value = true
- await nextTick()
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"teleported
trueroot
"`,
- )
- expect(serializeInner(target)).toBe(``)
-
- // toggle back
- disabled.value = false
- await nextTick()
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `"root
"`,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(
- `"teleported
false"`,
- )
- })
+ test('should work when using template ref as target', async () => {
+ const root = nodeOps.createElement('div')
+ const target = ref(null)
+ const disabled = ref(true)
+
+ const App = {
+ setup() {
+ return () =>
+ h(Fragment, [
+ h('div', { ref: target }),
+ h(
+ Teleport,
+ { to: target.value, disabled: disabled.value },
+ h('div', 'teleported'),
+ ),
+ ])
+ },
+ }
+ render(h(App), root)
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"teleported
"`,
+ )
- // #3497
- test(`the dir hooks of the Teleport's children should be called correctly`, async () => {
- const target = nodeOps.createElement('div')
- const root = nodeOps.createElement('div')
- const toggle = ref(true)
- const dir = {
- mounted: vi.fn(),
- unmounted: vi.fn(),
- }
-
- const app = createApp({
- setup() {
- return () => {
- return toggle.value
- ? h(Teleport, { to: target }, [
- withDirectives(h('div', ['foo']), [[dir]]),
- ])
- : null
- }
- },
+ disabled.value = false
+ await nextTick()
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `""`,
+ )
})
- app.mount(root)
-
- expect(serializeInner(root)).toMatchInlineSnapshot(
- `""`,
- )
- expect(serializeInner(target)).toMatchInlineSnapshot(`"foo
"`)
- expect(dir.mounted).toHaveBeenCalledTimes(1)
- expect(dir.unmounted).toHaveBeenCalledTimes(0)
-
- toggle.value = false
- await nextTick()
- expect(serializeInner(root)).toMatchInlineSnapshot(`""`)
- expect(serializeInner(target)).toMatchInlineSnapshot(`""`)
- expect(dir.mounted).toHaveBeenCalledTimes(1)
- expect(dir.unmounted).toHaveBeenCalledTimes(1)
- })
- // #7835
- test(`ensure that target changes when disabled are updated correctly when enabled`, async () => {
- const root = nodeOps.createElement('div')
- const target1 = nodeOps.createElement('div')
- const target2 = nodeOps.createElement('div')
- const target3 = nodeOps.createElement('div')
- const target = ref(target1)
- const disabled = ref(true)
-
- const App = {
- setup() {
- return () =>
- h(Fragment, [
- h(
- Teleport,
- { to: target.value, disabled: disabled.value },
- h('div', 'teleported'),
- ),
- ])
- },
- }
- render(h(App), root)
- disabled.value = false
- await nextTick()
- expect(serializeInner(target1)).toMatchInlineSnapshot(
- `"teleported
"`,
- )
- expect(serializeInner(target2)).toMatchInlineSnapshot(`""`)
- expect(serializeInner(target3)).toMatchInlineSnapshot(`""`)
-
- disabled.value = true
- await nextTick()
- target.value = target2
- await nextTick()
- expect(serializeInner(target1)).toMatchInlineSnapshot(`""`)
- expect(serializeInner(target2)).toMatchInlineSnapshot(`""`)
- expect(serializeInner(target3)).toMatchInlineSnapshot(`""`)
-
- target.value = target3
- await nextTick()
- expect(serializeInner(target1)).toMatchInlineSnapshot(`""`)
- expect(serializeInner(target2)).toMatchInlineSnapshot(`""`)
- expect(serializeInner(target3)).toMatchInlineSnapshot(`""`)
-
- disabled.value = false
- await nextTick()
- expect(serializeInner(target1)).toMatchInlineSnapshot(`""`)
- expect(serializeInner(target2)).toMatchInlineSnapshot(`""`)
- expect(serializeInner(target3)).toMatchInlineSnapshot(
- `"teleported
"`,
- )
- })
+ test('disabled', () => {
+ const target = nodeOps.createElement('div')
+ const root = nodeOps.createElement('div')
- //#9071
- test('toggle sibling node inside target node', async () => {
- const root = document.createElement('div')
- const show = ref(false)
- const App = defineComponent({
- setup() {
- return () => {
- return show.value
- ? h(Teleport, { to: root }, [h('div', 'teleported')])
- : h('div', 'foo')
- }
- },
+ const renderWithDisabled = (disabled: boolean) => {
+ return h(Fragment, [
+ h(Teleport, { to: target, disabled }, h('div', 'teleported')),
+ h('div', 'root'),
+ ])
+ }
+
+ render(renderWithDisabled(false), root)
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"root
"`,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ `"teleported
"`,
+ )
+
+ render(renderWithDisabled(true), root)
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"teleported
root
"`,
+ )
+ expect(serializeInner(target)).toBe(``)
+
+ // toggle back
+ render(renderWithDisabled(false), root)
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"root
"`,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ `"teleported
"`,
+ )
})
- domRender(h(App), root)
- expect(root.innerHTML).toMatchInlineSnapshot('"foo
"')
+ test('moving teleport while enabled', () => {
+ const target = nodeOps.createElement('div')
+ const root = nodeOps.createElement('div')
- show.value = true
- await nextTick()
+ render(
+ h(Fragment, [
+ h(Teleport, { to: target }, h('div', 'teleported')),
+ h('div', 'root'),
+ ]),
+ root,
+ )
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"root
"`,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ `"teleported
"`,
+ )
- expect(root.innerHTML).toMatchInlineSnapshot(
- '"teleported
"',
- )
+ render(
+ h(Fragment, [
+ h('div', 'root'),
+ h(Teleport, { to: target }, h('div', 'teleported')),
+ ]),
+ root,
+ )
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"root
"`,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ `"teleported
"`,
+ )
- show.value = false
- await nextTick()
+ render(
+ h(Fragment, [
+ h(Teleport, { to: target }, h('div', 'teleported')),
+ h('div', 'root'),
+ ]),
+ root,
+ )
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"root
"`,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ `"teleported
"`,
+ )
+ })
- expect(root.innerHTML).toMatchInlineSnapshot('"foo
"')
- })
+ test('moving teleport while disabled', () => {
+ const target = nodeOps.createElement('div')
+ const root = nodeOps.createElement('div')
- test('unmount previous sibling node inside target node', async () => {
- const root = document.createElement('div')
- const parentShow = ref(false)
- const childShow = ref(true)
-
- const Comp = {
- setup() {
- return () => h(Teleport, { to: root }, [h('div', 'foo')])
- },
- }
-
- const App = defineComponent({
- setup() {
- return () => {
- return parentShow.value
- ? h(Fragment, { key: 0 }, [
- childShow.value ? h(Comp) : createCommentVNode('v-if'),
- ])
- : createCommentVNode('v-if')
- }
- },
+ render(
+ h(Fragment, [
+ h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
+ h('div', 'root'),
+ ]),
+ root,
+ )
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"teleported
root
"`,
+ )
+ expect(serializeInner(target)).toBe('')
+
+ render(
+ h(Fragment, [
+ h('div', 'root'),
+ h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
+ ]),
+ root,
+ )
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"root
teleported
"`,
+ )
+ expect(serializeInner(target)).toBe('')
+
+ render(
+ h(Fragment, [
+ h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
+ h('div', 'root'),
+ ]),
+ root,
+ )
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"teleported
root
"`,
+ )
+ expect(serializeInner(target)).toBe('')
})
- domRender(h(App), root)
- expect(root.innerHTML).toMatchInlineSnapshot('""')
+ test('should work with block tree', async () => {
+ const target = nodeOps.createElement('div')
+ const root = nodeOps.createElement('div')
+ const disabled = ref(false)
+
+ const App = {
+ setup() {
+ return {
+ target: markRaw(target),
+ disabled,
+ }
+ },
+ render: compile(`
+
+ teleported
{{ disabled }}
+
+ root
+ `),
+ }
+ render(h(App), root)
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"root
"`,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ `"teleported
false"`,
+ )
- parentShow.value = true
- await nextTick()
- expect(root.innerHTML).toMatchInlineSnapshot(
- '"foo
"',
- )
+ disabled.value = true
+ await nextTick()
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"teleported
trueroot
"`,
+ )
+ expect(serializeInner(target)).toBe(``)
- parentShow.value = false
- await nextTick()
- expect(root.innerHTML).toMatchInlineSnapshot('""')
- })
+ // toggle back
+ disabled.value = false
+ await nextTick()
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `"root
"`,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(
+ `"teleported
false"`,
+ )
+ })
+
+ // #3497
+ test(`the dir hooks of the Teleport's children should be called correctly`, async () => {
+ const target = nodeOps.createElement('div')
+ const root = nodeOps.createElement('div')
+ const toggle = ref(true)
+ const dir = {
+ mounted: vi.fn(),
+ unmounted: vi.fn(),
+ }
+
+ const app = createApp({
+ setup() {
+ return () => {
+ return toggle.value
+ ? h(Teleport, { to: target }, [
+ withDirectives(h('div', ['foo']), [[dir]]),
+ ])
+ : null
+ }
+ },
+ })
+ app.mount(root)
+
+ expect(serializeInner(root)).toMatchInlineSnapshot(
+ `""`,
+ )
+ expect(serializeInner(target)).toMatchInlineSnapshot(`"foo
"`)
+ await nextTick()
+ expect(dir.mounted).toHaveBeenCalledTimes(1)
+ expect(dir.unmounted).toHaveBeenCalledTimes(0)
+
+ toggle.value = false
+ await nextTick()
+ expect(serializeInner(root)).toMatchInlineSnapshot(`""`)
+ expect(serializeInner(target)).toMatchInlineSnapshot(`""`)
+ expect(dir.mounted).toHaveBeenCalledTimes(1)
+ expect(dir.unmounted).toHaveBeenCalledTimes(1)
+ })
+
+ // #7835
+ test(`ensure that target changes when disabled are updated correctly when enabled`, async () => {
+ const root = nodeOps.createElement('div')
+ const target1 = nodeOps.createElement('div')
+ const target2 = nodeOps.createElement('div')
+ const target3 = nodeOps.createElement('div')
+ const target = ref(target1)
+ const disabled = ref(true)
+
+ const App = {
+ setup() {
+ return () =>
+ h(Fragment, [
+ h(
+ Teleport,
+ { to: target.value, disabled: disabled.value },
+ h('div', 'teleported'),
+ ),
+ ])
+ },
+ }
+ render(h(App), root)
+ disabled.value = false
+ await nextTick()
+ expect(serializeInner(target1)).toMatchInlineSnapshot(
+ `"teleported
"`,
+ )
+ expect(serializeInner(target2)).toMatchInlineSnapshot(`""`)
+ expect(serializeInner(target3)).toMatchInlineSnapshot(`""`)
+
+ disabled.value = true
+ await nextTick()
+ target.value = target2
+ await nextTick()
+ expect(serializeInner(target1)).toMatchInlineSnapshot(`""`)
+ expect(serializeInner(target2)).toMatchInlineSnapshot(`""`)
+ expect(serializeInner(target3)).toMatchInlineSnapshot(`""`)
+
+ target.value = target3
+ await nextTick()
+ expect(serializeInner(target1)).toMatchInlineSnapshot(`""`)
+ expect(serializeInner(target2)).toMatchInlineSnapshot(`""`)
+ expect(serializeInner(target3)).toMatchInlineSnapshot(`""`)
+
+ disabled.value = false
+ await nextTick()
+ expect(serializeInner(target1)).toMatchInlineSnapshot(`""`)
+ expect(serializeInner(target2)).toMatchInlineSnapshot(`""`)
+ expect(serializeInner(target3)).toMatchInlineSnapshot(
+ `"teleported
"`,
+ )
+ })
+
+ //#9071
+ test('toggle sibling node inside target node', async () => {
+ const root = document.createElement('div')
+ const show = ref(false)
+ const App = defineComponent({
+ setup() {
+ return () => {
+ return show.value
+ ? h(Teleport, { to: root }, [h('div', 'teleported')])
+ : h('div', 'foo')
+ }
+ },
+ })
+
+ domRender(h(App), root)
+ expect(root.innerHTML).toMatchInlineSnapshot('"foo
"')
+
+ show.value = true
+ await nextTick()
+
+ expect(root.innerHTML).toMatchInlineSnapshot(
+ '"teleported
"',
+ )
+
+ show.value = false
+ await nextTick()
+
+ expect(root.innerHTML).toMatchInlineSnapshot('"foo
"')
+ })
+
+ test('unmount previous sibling node inside target node', async () => {
+ const root = document.createElement('div')
+ const parentShow = ref(false)
+ const childShow = ref(true)
+
+ const Comp = {
+ setup() {
+ return () => h(Teleport, { to: root }, [h('div', 'foo')])
+ },
+ }
+
+ const App = defineComponent({
+ setup() {
+ return () => {
+ return parentShow.value
+ ? h(Fragment, { key: 0 }, [
+ childShow.value ? h(Comp) : createCommentVNode('v-if'),
+ ])
+ : createCommentVNode('v-if')
+ }
+ },
+ })
+
+ domRender(h(App), root)
+ expect(root.innerHTML).toMatchInlineSnapshot('""')
+
+ parentShow.value = true
+ await nextTick()
+ expect(root.innerHTML).toMatchInlineSnapshot(
+ '"foo
"',
+ )
+
+ parentShow.value = false
+ await nextTick()
+ expect(root.innerHTML).toMatchInlineSnapshot('""')
+ })
+ }
})
diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
index f4d5eba9a9a..2658f40718f 100644
--- a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
+++ b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
@@ -618,7 +618,7 @@ describe('renderer: optimized mode', () => {
})
//#3623
- test('nested teleport unmount need exit the optimization mode', () => {
+ test('nested teleport unmount need exit the optimization mode', async () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
@@ -647,6 +647,7 @@ describe('renderer: optimized mode', () => {
])),
root,
)
+ await nextTick()
expect(inner(target)).toMatchInlineSnapshot(
`"foo
"`,
)
diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts
index 65437300cff..997b83cc520 100644
--- a/packages/runtime-core/src/components/Teleport.ts
+++ b/packages/runtime-core/src/components/Teleport.ts
@@ -7,6 +7,7 @@ import {
type RendererInternals,
type RendererNode,
type RendererOptions,
+ queuePostRenderEffect,
traverseStaticChildren,
} from '../renderer'
import type { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
@@ -19,6 +20,7 @@ export type TeleportVNode = VNode
export interface TeleportProps {
to: string | RendererElement | null | undefined
disabled?: boolean
+ defer?: boolean
}
export const TeleportEndKey = Symbol('_vte')
@@ -28,6 +30,9 @@ export const isTeleport = (type: any): boolean => type.__isTeleport
const isTeleportDisabled = (props: VNode['props']): boolean =>
props && (props.disabled || props.disabled === '')
+const isTeleportDeferred = (props: VNode['props']): boolean =>
+ props && (props.defer || props.defer === '')
+
const isTargetSVG = (target: RendererElement): boolean =>
typeof SVGElement !== 'undefined' && target instanceof SVGElement
@@ -107,7 +112,6 @@ export const TeleportImpl = {
const mainAnchor = (n2.anchor = __DEV__
? createComment('teleport end')
: createText(''))
- const target = (n2.target = resolveTarget(n2.props, querySelector))
const targetStart = (n2.targetStart = createText(''))
const targetAnchor = (n2.targetAnchor = createText(''))
insert(placeholder, container, anchor)
@@ -115,18 +119,6 @@ export const TeleportImpl = {
// attach a special property so we can skip teleported content in
// renderer's nextSibling search
targetStart[TeleportEndKey] = targetAnchor
- if (target) {
- insert(targetStart, target)
- insert(targetAnchor, target)
- // #2652 we could be teleporting from a non-SVG tree into an SVG tree
- if (namespace === 'svg' || isTargetSVG(target)) {
- namespace = 'svg'
- } else if (namespace === 'mathml' || isTargetMathML(target)) {
- namespace = 'mathml'
- }
- } else if (__DEV__ && !disabled) {
- warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
- }
const mount = (container: RendererElement, anchor: RendererNode) => {
// Teleport *always* has Array children. This is enforced in both the
@@ -145,10 +137,39 @@ export const TeleportImpl = {
}
}
+ const mountToTarget = () => {
+ const target = (n2.target = resolveTarget(n2.props, querySelector))
+ if (target) {
+ insert(targetStart, target)
+ insert(targetAnchor, target)
+ // #2652 we could be teleporting from a non-SVG tree into an SVG tree
+ if (namespace !== 'svg' && isTargetSVG(target)) {
+ namespace = 'svg'
+ } else if (namespace !== 'mathml' && isTargetMathML(target)) {
+ namespace = 'mathml'
+ }
+ if (!disabled) {
+ mount(target, targetAnchor)
+ updateCssVars(n2)
+ }
+ } else if (__DEV__ && !disabled) {
+ warn(
+ 'Invalid Teleport target on mount:',
+ target,
+ `(${typeof target})`,
+ )
+ }
+ }
+
if (disabled) {
mount(container, mainAnchor)
- } else if (target) {
- mount(target, targetAnchor)
+ updateCssVars(n2)
+ }
+
+ if (isTeleportDeferred(n2.props)) {
+ queuePostRenderEffect(mountToTarget, parentSuspense)
+ } else {
+ mountToTarget()
}
} else {
// update content
@@ -249,9 +270,8 @@ export const TeleportImpl = {
)
}
}
+ updateCssVars(n2)
}
-
- updateCssVars(n2)
},
remove(
@@ -441,7 +461,7 @@ function updateCssVars(vnode: VNode) {
// code path here can assume browser environment.
const ctx = vnode.ctx
if (ctx && ctx.ut) {
- let node = (vnode.children as VNode[])[0].el!
+ let node = vnode.targetStart
while (node && node !== vnode.targetAnchor) {
if (node.nodeType === 1) node.setAttribute('data-v-owner', ctx.uid)
node = node.nextSibling
diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts
index cf730195dbc..ac90bc4f55d 100644
--- a/packages/runtime-core/src/scheduler.ts
+++ b/packages/runtime-core/src/scheduler.ts
@@ -127,7 +127,9 @@ export function invalidateJob(job: SchedulerJob) {
export function queuePostFlushCb(cb: SchedulerJobs) {
if (!isArray(cb)) {
- if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
+ if (activePostFlushCbs && cb.id === -1) {
+ activePostFlushCbs.splice(postFlushIndex + 1, 0, cb)
+ } else if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
pendingPostFlushCbs.push(cb)
if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
cb.flags! |= SchedulerJobFlags.QUEUED