]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(teleport): support deferred Teleport (#11387)
authorEvan You <evan@vuejs.org>
Thu, 18 Jul 2024 13:06:48 +0000 (21:06 +0800)
committerGitHub <noreply@github.com>
Thu, 18 Jul 2024 13:06:48 +0000 (21:06 +0800)
close #2015
close #11386

packages/runtime-core/__tests__/components/Teleport.spec.ts
packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
packages/runtime-core/src/components/Teleport.ts
packages/runtime-core/src/scheduler.ts

index aca9432b6e16dd7547fbb20143190a5db62af966..24400f6ed40a79e5118c99ae6bd61062a7f67d37 100644 (file)
@@ -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(
-      `"<!--teleport start--><!--teleport end--><div>root</div>"`,
-    )
-    expect(serializeInner(target)).toMatchInlineSnapshot(
-      `"<div>teleported</div>"`,
-    )
+  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: `
-      <svg ref="svg"></svg>
-      <teleport :to="svg" v-if="svg">
-      <circle ref="circle"></circle>
-      </teleport>`,
-    })
+    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(
-      `"<svg><circle></circle></svg><!--teleport start--><!--teleport end-->"`,
-    )
+      expect(root.innerHTML).toMatchInlineSnapshot(
+        `"<!--teleport start--><!--teleport end--><div id="target"><div>teleported</div></div>"`,
+      )
+    })
 
-    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<any>
+
+      const Comp = defineComponent({
+        template: `
+        <suspense>
+          <div>
+            <async />
+            <teleport defer to="#target-suspense">
+              <div ref="tel">teleported</div>
+            </teleport>
+            <div id="target-suspense" />
+          </div>
+        </suspense>`,
+        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(
+        `<div>` +
+          `async` +
+          `<!--teleport start--><!--teleport end-->` +
+          `<div id="target-suspense"><div>teleported</div></div>` +
+          `</div>`,
+      )
+    })
   })
 
-  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(
-      `"<!--teleport start--><!--teleport end--><div>root</div>"`,
-    )
-    expect(serializeInner(targetA)).toMatchInlineSnapshot(
-      `"<div>teleported</div>"`,
-    )
-    expect(serializeInner(targetB)).toMatchInlineSnapshot(`""`)
-
-    target.value = targetB
-    await nextTick()
-
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<!--teleport start--><!--teleport end--><div>root</div>"`,
-    )
-    expect(serializeInner(targetA)).toMatchInlineSnapshot(`""`)
-    expect(serializeInner(targetB)).toMatchInlineSnapshot(
-      `"<div>teleported</div>"`,
-    )
-  })
+  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(
-      `"<div>teleported</div>"`,
-    )
+      render(
+        h(() => [
+          h(Teleport, { to: target }, h('div', 'teleported')),
+          h('div', 'root'),
+        ]),
+        root,
+      )
 
-    children.value = []
-    await nextTick()
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<!--teleport start--><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(target)).toMatchInlineSnapshot(
+        `"<div>teleported</div>"`,
+      )
+    })
 
-    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: `
+        <svg ref="svg"></svg>
+        <teleport :to="svg" v-if="svg">
+        <circle ref="circle"></circle>
+        </teleport>`,
+      })
+
+      domRender(h(Comp), root)
+
+      await nextTick()
+
+      expect(root.innerHTML).toMatchInlineSnapshot(
+        `"<svg><circle></circle></svg><!--teleport start--><!--teleport end-->"`,
+      )
 
-    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(
+        `"<!--teleport start--><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(targetA)).toMatchInlineSnapshot(
+        `"<div>teleported</div>"`,
+      )
+      expect(serializeInner(targetB)).toMatchInlineSnapshot(`""`)
+
+      target.value = targetB
+      await nextTick()
+
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<!--teleport start--><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(targetA)).toMatchInlineSnapshot(`""`)
+      expect(serializeInner(targetB)).toMatchInlineSnapshot(
+        `"<div>teleported</div>"`,
+      )
+    })
+
+    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 ? `""` : `"<div>teleported</div>"`,
+        `"<div>teleported</div>"`,
+      )
+
+      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 ? `""` : `"<div>teleported</div>"`,
+        )
+
+        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(`"<p></p><p></p>"`)
 
       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(`"<p></p><p></p>"`)
+      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(
-      `"<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>"`,
-    )
-    expect(serializeInner(target)).toMatchInlineSnapshot(`"<div>one</div>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(
-      `"<div>one</div><div>two</div>three"`,
-    )
-
-    // toggling
-    render(h('div', [null, h(Teleport, { to: target }, 'three')]), root)
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<div><!----><!--teleport start--><!--teleport end--></div>"`,
-    )
-    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(
-      `"<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>"`,
-    )
-    // should append
-    expect(serializeInner(target)).toMatchInlineSnapshot(
-      `"three<div>one</div><div>two</div>"`,
-    )
-
-    // toggle the other teleport
-    render(
-      h('div', [
-        h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
-        null,
-      ]),
-      root,
-    )
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<div><!--teleport start--><!--teleport end--><!----></div>"`,
-    )
-    expect(serializeInner(target)).toMatchInlineSnapshot(
-      `"<div>one</div><div>two</div>"`,
-    )
-  })
+      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(
-      `"<div></div><!--teleport start--><div>teleported</div><!--teleport end-->"`,
-    )
-
-    disabled.value = false
-    await nextTick()
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<div><div>teleported</div></div><!--teleport start--><!--teleport end-->"`,
-    )
-  })
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>"`,
+      )
+      expect(serializeInner(target)).toMatchInlineSnapshot(
+        `"<div>one</div>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(
-      `"<!--teleport start--><!--teleport end--><div>root</div>"`,
-    )
-    expect(serializeInner(target)).toMatchInlineSnapshot(
-      `"<div>teleported</div>"`,
-    )
-
-    render(renderWithDisabled(true), root)
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<!--teleport start--><div>teleported</div><!--teleport end--><div>root</div>"`,
-    )
-    expect(serializeInner(target)).toBe(``)
-
-    // toggle back
-    render(renderWithDisabled(false), root)
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<!--teleport start--><!--teleport end--><div>root</div>"`,
-    )
-    expect(serializeInner(target)).toMatchInlineSnapshot(
-      `"<div>teleported</div>"`,
-    )
-  })
+      // 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(
+        `"<div>one</div><div>two</div>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(
-      `"<!--teleport start--><!--teleport end--><div>root</div>"`,
-    )
-    expect(serializeInner(target)).toMatchInlineSnapshot(
-      `"<div>teleported</div>"`,
-    )
-
-    render(
-      h(Fragment, [
-        h('div', 'root'),
-        h(Teleport, { to: target }, h('div', 'teleported')),
-      ]),
-      root,
-    )
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<div>root</div><!--teleport start--><!--teleport end-->"`,
-    )
-    expect(serializeInner(target)).toMatchInlineSnapshot(
-      `"<div>teleported</div>"`,
-    )
-
-    render(
-      h(Fragment, [
-        h(Teleport, { to: target }, h('div', 'teleported')),
-        h('div', 'root'),
-      ]),
-      root,
-    )
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<!--teleport start--><!--teleport end--><div>root</div>"`,
-    )
-    expect(serializeInner(target)).toMatchInlineSnapshot(
-      `"<div>teleported</div>"`,
-    )
-  })
+      // toggling
+      render(h('div', [null, h(Teleport, { to: target }, 'three')]), root)
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<div><!----><!--teleport start--><!--teleport end--></div>"`,
+      )
+      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(
-      `"<!--teleport start--><div>teleported</div><!--teleport end--><div>root</div>"`,
-    )
-    expect(serializeInner(target)).toBe('')
-
-    render(
-      h(Fragment, [
-        h('div', 'root'),
-        h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
-      ]),
-      root,
-    )
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<div>root</div><!--teleport start--><div>teleported</div><!--teleport end-->"`,
-    )
-    expect(serializeInner(target)).toBe('')
-
-    render(
-      h(Fragment, [
-        h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
-        h('div', 'root'),
-      ]),
-      root,
-    )
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<!--teleport start--><div>teleported</div><!--teleport end--><div>root</div>"`,
-    )
-    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(
+        `"<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>"`,
+      )
+      // should append
+      expect(serializeInner(target)).toMatchInlineSnapshot(
+        `"three<div>one</div><div>two</div>"`,
+      )
 
-  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(
+        `"<div><!--teleport start--><!--teleport end--><!----></div>"`,
+      )
+      expect(serializeInner(target)).toMatchInlineSnapshot(
+        `"<div>one</div><div>two</div>"`,
+      )
+    })
 
-    const App = {
-      setup() {
-        return {
-          target: markRaw(target),
-          disabled,
-        }
-      },
-      render: compile(`
-      <teleport :to="target" :disabled="disabled">
-        <div>teleported</div><span>{{ disabled }}</span><span v-if="disabled"/>
-      </teleport>
-      <div>root</div>
-      `),
-    }
-    render(h(App), root)
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<!--teleport start--><!--teleport end--><div>root</div>"`,
-    )
-    expect(serializeInner(target)).toMatchInlineSnapshot(
-      `"<div>teleported</div><span>false</span><!--v-if-->"`,
-    )
-
-    disabled.value = true
-    await nextTick()
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<!--teleport start--><div>teleported</div><span>true</span><span></span><!--teleport end--><div>root</div>"`,
-    )
-    expect(serializeInner(target)).toBe(``)
-
-    // toggle back
-    disabled.value = false
-    await nextTick()
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<!--teleport start--><!--teleport end--><div>root</div>"`,
-    )
-    expect(serializeInner(target)).toMatchInlineSnapshot(
-      `"<div>teleported</div><span>false</span><!--v-if-->"`,
-    )
-  })
+    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(
+        `"<div></div><!--teleport start--><div>teleported</div><!--teleport end-->"`,
+      )
 
-  // #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(
+        `"<div><div>teleported</div></div><!--teleport start--><!--teleport end-->"`,
+      )
     })
-    app.mount(root)
-
-    expect(serializeInner(root)).toMatchInlineSnapshot(
-      `"<!--teleport start--><!--teleport end-->"`,
-    )
-    expect(serializeInner(target)).toMatchInlineSnapshot(`"<div>foo</div>"`)
-    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(
-      `"<div>teleported</div>"`,
-    )
-    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(
-      `"<div>teleported</div>"`,
-    )
-  })
+    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(
+        `"<!--teleport start--><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(target)).toMatchInlineSnapshot(
+        `"<div>teleported</div>"`,
+      )
+
+      render(renderWithDisabled(true), root)
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<!--teleport start--><div>teleported</div><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(target)).toBe(``)
+
+      // toggle back
+      render(renderWithDisabled(false), root)
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<!--teleport start--><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(target)).toMatchInlineSnapshot(
+        `"<div>teleported</div>"`,
+      )
     })
 
-    domRender(h(App), root)
-    expect(root.innerHTML).toMatchInlineSnapshot('"<div>foo</div>"')
+    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(
+        `"<!--teleport start--><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(target)).toMatchInlineSnapshot(
+        `"<div>teleported</div>"`,
+      )
 
-    expect(root.innerHTML).toMatchInlineSnapshot(
-      '"<!--teleport start--><!--teleport end--><div>teleported</div>"',
-    )
+      render(
+        h(Fragment, [
+          h('div', 'root'),
+          h(Teleport, { to: target }, h('div', 'teleported')),
+        ]),
+        root,
+      )
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<div>root</div><!--teleport start--><!--teleport end-->"`,
+      )
+      expect(serializeInner(target)).toMatchInlineSnapshot(
+        `"<div>teleported</div>"`,
+      )
 
-    show.value = false
-    await nextTick()
+      render(
+        h(Fragment, [
+          h(Teleport, { to: target }, h('div', 'teleported')),
+          h('div', 'root'),
+        ]),
+        root,
+      )
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<!--teleport start--><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(target)).toMatchInlineSnapshot(
+        `"<div>teleported</div>"`,
+      )
+    })
 
-    expect(root.innerHTML).toMatchInlineSnapshot('"<div>foo</div>"')
-  })
+    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(
+        `"<!--teleport start--><div>teleported</div><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(target)).toBe('')
+
+      render(
+        h(Fragment, [
+          h('div', 'root'),
+          h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
+        ]),
+        root,
+      )
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<div>root</div><!--teleport start--><div>teleported</div><!--teleport end-->"`,
+      )
+      expect(serializeInner(target)).toBe('')
+
+      render(
+        h(Fragment, [
+          h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
+          h('div', 'root'),
+        ]),
+        root,
+      )
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<!--teleport start--><div>teleported</div><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(target)).toBe('')
     })
 
-    domRender(h(App), root)
-    expect(root.innerHTML).toMatchInlineSnapshot('"<!--v-if-->"')
+    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(`
+        <teleport :to="target" :disabled="disabled">
+          <div>teleported</div><span>{{ disabled }}</span><span v-if="disabled"/>
+        </teleport>
+        <div>root</div>
+        `),
+      }
+      render(h(App), root)
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<!--teleport start--><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(target)).toMatchInlineSnapshot(
+        `"<div>teleported</div><span>false</span><!--v-if-->"`,
+      )
 
-    parentShow.value = true
-    await nextTick()
-    expect(root.innerHTML).toMatchInlineSnapshot(
-      '"<!--teleport start--><!--teleport end--><div>foo</div>"',
-    )
+      disabled.value = true
+      await nextTick()
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<!--teleport start--><div>teleported</div><span>true</span><span></span><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(target)).toBe(``)
 
-    parentShow.value = false
-    await nextTick()
-    expect(root.innerHTML).toMatchInlineSnapshot('"<!--v-if-->"')
-  })
+      // toggle back
+      disabled.value = false
+      await nextTick()
+      expect(serializeInner(root)).toMatchInlineSnapshot(
+        `"<!--teleport start--><!--teleport end--><div>root</div>"`,
+      )
+      expect(serializeInner(target)).toMatchInlineSnapshot(
+        `"<div>teleported</div><span>false</span><!--v-if-->"`,
+      )
+    })
+
+    // #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(
+        `"<!--teleport start--><!--teleport end-->"`,
+      )
+      expect(serializeInner(target)).toMatchInlineSnapshot(`"<div>foo</div>"`)
+      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(
+        `"<div>teleported</div>"`,
+      )
+      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(
+        `"<div>teleported</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')
+          }
+        },
+      })
+
+      domRender(h(App), root)
+      expect(root.innerHTML).toMatchInlineSnapshot('"<div>foo</div>"')
+
+      show.value = true
+      await nextTick()
+
+      expect(root.innerHTML).toMatchInlineSnapshot(
+        '"<!--teleport start--><!--teleport end--><div>teleported</div>"',
+      )
+
+      show.value = false
+      await nextTick()
+
+      expect(root.innerHTML).toMatchInlineSnapshot('"<div>foo</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')
+          }
+        },
+      })
+
+      domRender(h(App), root)
+      expect(root.innerHTML).toMatchInlineSnapshot('"<!--v-if-->"')
+
+      parentShow.value = true
+      await nextTick()
+      expect(root.innerHTML).toMatchInlineSnapshot(
+        '"<!--teleport start--><!--teleport end--><div>foo</div>"',
+      )
+
+      parentShow.value = false
+      await nextTick()
+      expect(root.innerHTML).toMatchInlineSnapshot('"<!--v-if-->"')
+    })
+  }
 })
index f4d5eba9a9aef3cf527f71bb6f249a34a431adf5..2658f40718fea53a581c4b643bd9066652cbb09c 100644 (file)
@@ -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(
       `"<div><!--teleport start--><!--teleport end--></div><div>foo</div>"`,
     )
index 65437300cff7833d1f6082e9eaddc0ce1c6c75fe..997b83cc520bf59c8c497b18c888172d0946e241 100644 (file)
@@ -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<RendererNode, RendererElement, TeleportProps>
 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
index cf730195dbc566e233c0bd40e8c0efe06b259140..ac90bc4f55dc4aa912d9f28d7f9b7aae5e0affa2 100644 (file)
@@ -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