(
props: TeleportProps | null,
select: RendererOptions['querySelector'],
): T | null => {
diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts
index ed5d8b081a0..acc9593d137 100644
--- a/packages/runtime-core/src/hmr.ts
+++ b/packages/runtime-core/src/hmr.ts
@@ -119,7 +119,7 @@ function reload(id: string, newComp: HMRComponent): void {
// create a snapshot which avoids the set being mutated during updates
const instances = [...record.instances]
- if (newComp.vapor) {
+ if (newComp.__vapor) {
for (const instance of instances) {
instance.hmrReload!(newComp)
}
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index e309554f2f6..fabe46dc310 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -557,3 +557,11 @@ export { startMeasure, endMeasure } from './profiling'
* @internal
*/
export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export {
+ resolveTarget as resolveTeleportTarget,
+ isTeleportDisabled,
+ isTeleportDeferred,
+} from './components/Teleport'
diff --git a/packages/runtime-vapor/__tests__/block.spec.ts b/packages/runtime-vapor/__tests__/block.spec.ts
index 9f76c7f0333..f0144dee3df 100644
--- a/packages/runtime-vapor/__tests__/block.spec.ts
+++ b/packages/runtime-vapor/__tests__/block.spec.ts
@@ -1,10 +1,5 @@
-import {
- VaporFragment,
- insert,
- normalizeBlock,
- prepend,
- remove,
-} from '../src/block'
+import { insert, normalizeBlock, prepend, remove } from '../src/block'
+import { VaporFragment } from '../src/fragment'
const node1 = document.createTextNode('node1')
const node2 = document.createTextNode('node2')
diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts
new file mode 100644
index 00000000000..6863d398bfc
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts
@@ -0,0 +1,1168 @@
+import {
+ type LooseRawProps,
+ type VaporComponent,
+ createComponent as createComp,
+} from '../../src/component'
+import {
+ type VaporDirective,
+ VaporTeleport,
+ child,
+ createIf,
+ createTemplateRefSetter,
+ defineVaporComponent,
+ renderEffect,
+ setInsertionState,
+ setText,
+ template,
+ withVaporDirectives,
+} from '@vue/runtime-vapor'
+import { makeRender } from '../_utils'
+import {
+ nextTick,
+ onBeforeUnmount,
+ onMounted,
+ onUnmounted,
+ ref,
+ shallowRef,
+} from 'vue'
+
+import type { HMRRuntime } from '@vue/runtime-dom'
+declare var __VUE_HMR_RUNTIME__: HMRRuntime
+const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__
+
+const define = makeRender()
+
+describe('renderer: VaporTeleport', () => {
+ describe('eager mode', () => {
+ runSharedTests(false)
+ })
+
+ describe('defer mode', () => {
+ runSharedTests(true)
+
+ test('should be able to target content appearing later than the teleport with defer', () => {
+ const root = document.createElement('div')
+ document.body.appendChild(root)
+
+ const { mount } = define({
+ setup() {
+ const n1 = createComp(
+ VaporTeleport,
+ {
+ to: () => '#target',
+ defer: () => true,
+ },
+ {
+ default: () => template('teleported
')(),
+ },
+ )
+ const n2 = template('')()
+ return [n1, n2]
+ },
+ }).create()
+ mount(root)
+
+ expect(root.innerHTML).toBe(
+ '',
+ )
+ })
+
+ test.todo('defer mode should work inside suspense', () => {})
+
+ test('update before mounted with defer', async () => {
+ const root = document.createElement('div')
+ document.body.appendChild(root)
+
+ const show = ref(false)
+ const foo = ref('foo')
+ const Header = defineVaporComponent({
+ props: { foo: String },
+ setup(props) {
+ const n0 = template(`
`)()
+ const x0 = child(n0 as any)
+ renderEffect(() => setText(x0 as any, props.foo))
+ return [n0]
+ },
+ })
+ const Footer = defineVaporComponent({
+ setup() {
+ foo.value = 'bar'
+ return template('Footer
')()
+ },
+ })
+
+ const { mount } = define({
+ setup() {
+ return createIf(
+ () => show.value,
+ () => {
+ const n1 = createComp(
+ VaporTeleport,
+ { to: () => '#targetId', defer: () => true },
+ { default: () => createComp(Header, { foo: () => foo.value }) },
+ )
+ const n2 = createComp(Footer)
+ const n3 = template('')()
+ return [n1, n2, n3]
+ },
+ () => template('')(),
+ )
+ },
+ }).create()
+ mount(root)
+
+ expect(root.innerHTML).toBe('')
+
+ show.value = true
+ await nextTick()
+ expect(root.innerHTML).toBe(
+ `Footer
`,
+ )
+ })
+ })
+
+ describe('HMR', () => {
+ test('rerender child + rerender parent', async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+ const childId = 'test1-child-rerender'
+ const parentId = 'test1-parent-rerender'
+
+ const { component: Child } = define({
+ __hmrId: childId,
+ render() {
+ return template('teleported
')()
+ },
+ })
+ createRecord(childId, Child as any)
+
+ const { mount, component: Parent } = define({
+ __hmrId: parentId,
+ render() {
+ const n0 = createComp(
+ VaporTeleport,
+ {
+ to: () => target,
+ },
+ {
+ default: () => createComp(Child),
+ },
+ )
+ const n1 = template('root
')()
+ return [n0, n1]
+ },
+ }).create()
+ createRecord(parentId, Parent as any)
+ mount(root)
+
+ expect(root.innerHTML).toBe('root
')
+ expect(target.innerHTML).toBe('teleported
')
+
+ // rerender child
+ rerender(childId, () => {
+ return template('teleported 2
')()
+ })
+
+ expect(root.innerHTML).toBe('root
')
+ expect(target.innerHTML).toBe('teleported 2
')
+
+ // rerender parent
+ rerender(parentId, () => {
+ const n0 = createComp(
+ VaporTeleport,
+ {
+ to: () => target,
+ },
+ {
+ default: () => createComp(Child),
+ },
+ )
+ const n1 = template('root 2
')()
+ return [n0, n1]
+ })
+
+ expect(root.innerHTML).toBe('root 2
')
+ expect(target.innerHTML).toBe('teleported 2
')
+ })
+
+ test('parent rerender + toggle disabled', async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+ const parentId = 'test3-parent-rerender'
+ const disabled = ref(true)
+
+ const Child = defineVaporComponent({
+ render() {
+ return template('teleported
')()
+ },
+ })
+
+ const { mount, component: Parent } = define({
+ __hmrId: parentId,
+ render() {
+ const n2 = template('', true)() as any
+ setInsertionState(n2, 0)
+ createComp(
+ VaporTeleport,
+ {
+ to: () => target,
+ disabled: () => disabled.value,
+ },
+ {
+ default: () => createComp(Child),
+ },
+ )
+ return n2
+ },
+ }).create()
+ createRecord(parentId, Parent as any)
+ mount(root)
+
+ expect(root.innerHTML).toBe(
+ '',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // rerender parent
+ rerender(parentId, () => {
+ const n2 = template('', true)() as any
+ setInsertionState(n2, 0)
+ createComp(
+ VaporTeleport,
+ {
+ to: () => target,
+ disabled: () => disabled.value,
+ },
+ {
+ default: () => createComp(Child),
+ },
+ )
+ return n2
+ })
+
+ expect(root.innerHTML).toBe(
+ '',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // toggle disabled
+ disabled.value = false
+ await nextTick()
+ expect(root.innerHTML).toBe('')
+ expect(target.innerHTML).toBe('teleported
')
+ })
+
+ test('reload child + reload parent', async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+ const childId = 'test1-child-reload'
+ const parentId = 'test1-parent-reload'
+
+ const { component: Child } = define({
+ __hmrId: childId,
+ setup() {
+ const msg = ref('teleported')
+ return { msg }
+ },
+ render(ctx) {
+ const n0 = template(`
`)()
+ const x0 = child(n0 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0]
+ },
+ })
+ createRecord(childId, Child as any)
+
+ const { mount, component: Parent } = define({
+ __hmrId: parentId,
+ setup() {
+ const msg = ref('root')
+ const disabled = ref(false)
+ return { msg, disabled }
+ },
+ render(ctx) {
+ const n0 = createComp(
+ VaporTeleport,
+ {
+ to: () => target,
+ disabled: () => ctx.disabled,
+ },
+ {
+ default: () => createComp(Child),
+ },
+ )
+ const n1 = template(`
`)()
+ const x0 = child(n1 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0, n1]
+ },
+ }).create()
+ createRecord(parentId, Parent as any)
+ mount(root)
+
+ expect(root.innerHTML).toBe('root
')
+ expect(target.innerHTML).toBe('teleported
')
+
+ // reload child by changing msg
+ reload(childId, {
+ __hmrId: childId,
+ __vapor: true,
+ setup() {
+ const msg = ref('teleported 2')
+ return { msg }
+ },
+ render(ctx: any) {
+ const n0 = template(`
`)()
+ const x0 = child(n0 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0]
+ },
+ })
+ expect(root.innerHTML).toBe('root
')
+ expect(target.innerHTML).toBe('teleported 2
')
+
+ // reload parent by changing msg
+ reload(parentId, {
+ __hmrId: parentId,
+ __vapor: true,
+ setup() {
+ const msg = ref('root 2')
+ const disabled = ref(false)
+ return { msg, disabled }
+ },
+ render(ctx: any) {
+ const n0 = createComp(
+ VaporTeleport,
+ {
+ to: () => target,
+ disabled: () => ctx.disabled,
+ },
+ {
+ default: () => createComp(Child),
+ },
+ )
+ const n1 = template(`
`)()
+ const x0 = child(n1 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0, n1]
+ },
+ })
+
+ expect(root.innerHTML).toBe('root 2
')
+ expect(target.innerHTML).toBe('teleported 2
')
+
+ // reload parent again by changing disabled
+ reload(parentId, {
+ __hmrId: parentId,
+ __vapor: true,
+ setup() {
+ const msg = ref('root 2')
+ const disabled = ref(true)
+ return { msg, disabled }
+ },
+ render(ctx: any) {
+ const n0 = createComp(
+ VaporTeleport,
+ {
+ to: () => target,
+ disabled: () => ctx.disabled,
+ },
+ {
+ default: () => createComp(Child),
+ },
+ )
+ const n1 = template(`
`)()
+ const x0 = child(n1 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0, n1]
+ },
+ })
+
+ expect(root.innerHTML).toBe(
+ 'teleported 2
root 2
',
+ )
+ expect(target.innerHTML).toBe('')
+ })
+
+ test('reload single root child + toggle disabled', async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+ const childId = 'test2-child-reload'
+ const parentId = 'test2-parent-reload'
+
+ const disabled = ref(true)
+ const { component: Child } = define({
+ __hmrId: childId,
+ setup() {
+ const msg = ref('teleported')
+ return { msg }
+ },
+ render(ctx) {
+ const n0 = template(`
`)()
+ const x0 = child(n0 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0]
+ },
+ })
+ createRecord(childId, Child as any)
+
+ const { mount, component: Parent } = define({
+ __hmrId: parentId,
+ setup() {
+ const msg = ref('root')
+ return { msg, disabled }
+ },
+ render(ctx) {
+ const n0 = createComp(
+ VaporTeleport,
+ {
+ to: () => target,
+ disabled: () => ctx.disabled,
+ },
+ {
+ // with single root child
+ default: () => createComp(Child),
+ },
+ )
+ const n1 = template(`
`)()
+ const x0 = child(n1 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0, n1]
+ },
+ }).create()
+ createRecord(parentId, Parent as any)
+ mount(root)
+
+ expect(root.innerHTML).toBe(
+ 'teleported
root
',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // reload child by changing msg
+ reload(childId, {
+ __hmrId: childId,
+ __vapor: true,
+ setup() {
+ const msg = ref('teleported 2')
+ return { msg }
+ },
+ render(ctx: any) {
+ const n0 = template(`
`)()
+ const x0 = child(n0 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0]
+ },
+ })
+ expect(root.innerHTML).toBe(
+ 'teleported 2
root
',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // reload child again by changing msg
+ reload(childId, {
+ __hmrId: childId,
+ __vapor: true,
+ setup() {
+ const msg = ref('teleported 3')
+ return { msg }
+ },
+ render(ctx: any) {
+ const n0 = template(`
`)()
+ const x0 = child(n0 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0]
+ },
+ })
+ expect(root.innerHTML).toBe(
+ 'teleported 3
root
',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // toggle disabled
+ disabled.value = false
+ await nextTick()
+ expect(root.innerHTML).toBe('root
')
+ expect(target.innerHTML).toBe('teleported 3
')
+ })
+
+ test('reload multiple root children + toggle disabled', async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+ const childId = 'test3-child-reload'
+ const parentId = 'test3-parent-reload'
+
+ const disabled = ref(true)
+ const { component: Child } = define({
+ __hmrId: childId,
+ setup() {
+ const msg = ref('teleported')
+ return { msg }
+ },
+ render(ctx) {
+ const n0 = template(`
`)()
+ const x0 = child(n0 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0]
+ },
+ })
+ createRecord(childId, Child as any)
+
+ const { mount, component: Parent } = define({
+ __hmrId: parentId,
+ setup() {
+ const msg = ref('root')
+ return { msg, disabled }
+ },
+ render(ctx) {
+ const n0 = createComp(
+ VaporTeleport,
+ {
+ to: () => target,
+ disabled: () => ctx.disabled,
+ },
+ {
+ default: () => {
+ // with multiple root children
+ return [createComp(Child), template(`child`)()]
+ },
+ },
+ )
+ const n1 = template(`
`)()
+ const x0 = child(n1 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0, n1]
+ },
+ }).create()
+ createRecord(parentId, Parent as any)
+ mount(root)
+
+ expect(root.innerHTML).toBe(
+ 'teleported
childroot
',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // reload child by changing msg
+ reload(childId, {
+ __hmrId: childId,
+ __vapor: true,
+ setup() {
+ const msg = ref('teleported 2')
+ return { msg }
+ },
+ render(ctx: any) {
+ const n0 = template(`
`)()
+ const x0 = child(n0 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0]
+ },
+ })
+ expect(root.innerHTML).toBe(
+ 'teleported 2
childroot
',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // reload child again by changing msg
+ reload(childId, {
+ __hmrId: childId,
+ __vapor: true,
+ setup() {
+ const msg = ref('teleported 3')
+ return { msg }
+ },
+ render(ctx: any) {
+ const n0 = template(`
`)()
+ const x0 = child(n0 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0]
+ },
+ })
+ expect(root.innerHTML).toBe(
+ 'teleported 3
childroot
',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // toggle disabled
+ disabled.value = false
+ await nextTick()
+ expect(root.innerHTML).toBe('root
')
+ expect(target.innerHTML).toBe('teleported 3
child')
+ })
+ })
+})
+
+function runSharedTests(deferMode: boolean): void {
+ const createComponent = deferMode
+ ? (
+ component: VaporComponent,
+ rawProps?: LooseRawProps | null,
+ ...args: any[]
+ ) => {
+ if (component === VaporTeleport) {
+ rawProps!.defer = () => true
+ }
+ return createComp(component, rawProps, ...args)
+ }
+ : createComp
+
+ test('should work', () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+
+ const { mount } = define({
+ setup() {
+ const n0 = createComponent(
+ VaporTeleport,
+ {
+ to: () => target,
+ },
+ {
+ default: () => template('teleported
')(),
+ },
+ )
+ const n1 = template('root
')()
+ return [n0, n1]
+ },
+ }).create()
+ mount(root)
+
+ expect(root.innerHTML).toBe('root
')
+ expect(target.innerHTML).toBe('teleported
')
+ })
+
+ test.todo('should work with SVG', async () => {})
+
+ test('should update target', async () => {
+ const targetA = document.createElement('div')
+ const targetB = document.createElement('div')
+ const target = ref(targetA)
+ const root = document.createElement('div')
+
+ const { mount } = define({
+ setup() {
+ const n0 = createComponent(
+ VaporTeleport,
+ {
+ to: () => target.value,
+ },
+ {
+ default: () => template('teleported
')(),
+ },
+ )
+ const n1 = template('root
')()
+ return [n0, n1]
+ },
+ }).create()
+ mount(root)
+
+ expect(root.innerHTML).toBe('root
')
+ expect(targetA.innerHTML).toBe('teleported
')
+ expect(targetB.innerHTML).toBe('')
+
+ target.value = targetB
+ await nextTick()
+
+ expect(root.innerHTML).toBe('root
')
+ expect(targetA.innerHTML).toBe('')
+ expect(targetB.innerHTML).toBe('teleported
')
+ })
+
+ test('should update children', async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+ const children = shallowRef([template('teleported
')()])
+
+ const { mount } = define({
+ setup() {
+ const n0 = createComponent(
+ VaporTeleport,
+ {
+ to: () => target,
+ },
+ {
+ default: () => children.value,
+ },
+ )
+ const n1 = template('root
')()
+ return [n0, n1]
+ },
+ }).create()
+ mount(root)
+
+ expect(target.innerHTML).toBe('teleported
')
+
+ children.value = [template('')()]
+ await nextTick()
+ expect(target.innerHTML).toBe('')
+
+ children.value = [template('teleported')()]
+ await nextTick()
+ expect(target.innerHTML).toBe('teleported')
+ })
+
+ test('should remove children when unmounted', async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+
+ function testUnmount(props: any) {
+ const { app } = define({
+ setup() {
+ const n0 = createComponent(VaporTeleport, props, {
+ default: () => template('teleported
')(),
+ })
+ const n1 = template('root
')()
+ return [n0, n1]
+ },
+ }).create()
+ app.mount(root)
+
+ expect(target.innerHTML).toBe(
+ props.disabled() ? '' : 'teleported
',
+ )
+
+ app.unmount()
+ expect(target.innerHTML).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', async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+
+ const { component: Comp } = define({
+ setup() {
+ return [template('')(), template('
')()]
+ },
+ })
+
+ const { app } = define({
+ setup() {
+ const n0 = createComponent(
+ VaporTeleport,
+ {
+ to: () => target,
+ },
+ {
+ default: () => createComponent(Comp),
+ },
+ )
+ const n1 = template('
root
')()
+ return [n0, n1]
+ },
+ }).create()
+
+ app.mount(root)
+ expect(target.innerHTML).toBe('')
+
+ app.unmount()
+ expect(target.innerHTML).toBe('')
+ })
+
+ test('descendent component should be unmounted when teleport is disabled and unmounted', async () => {
+ const root = document.createElement('div')
+ const beforeUnmount = vi.fn()
+ const unmounted = vi.fn()
+ const { component: Comp } = define({
+ setup() {
+ onBeforeUnmount(beforeUnmount)
+ onUnmounted(unmounted)
+ return [template('')(), template('
')()]
+ },
+ })
+
+ const { app } = define({
+ setup() {
+ const n0 = createComponent(
+ VaporTeleport,
+ {
+ to: () => null,
+ disabled: () => true,
+ },
+ {
+ default: () => createComponent(Comp),
+ },
+ )
+ return [n0]
+ },
+ }).create()
+ app.mount(root)
+
+ expect(beforeUnmount).toHaveBeenCalledTimes(0)
+ expect(unmounted).toHaveBeenCalledTimes(0)
+
+ app.unmount()
+ await nextTick()
+ expect(beforeUnmount).toHaveBeenCalledTimes(1)
+ expect(unmounted).toHaveBeenCalledTimes(1)
+ })
+
+ test('multiple teleport with same target', async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+
+ const child1 = shallowRef(template('
one
')())
+ const child2 = shallowRef(template('two')())
+
+ const { mount } = define({
+ setup() {
+ const n0 = template('')()
+ setInsertionState(n0 as any)
+ createComponent(
+ VaporTeleport,
+ {
+ to: () => target,
+ },
+ {
+ default: () => child1.value,
+ },
+ )
+ createComponent(
+ VaporTeleport,
+ {
+ to: () => target,
+ },
+ {
+ default: () => child2.value,
+ },
+ )
+ return [n0]
+ },
+ }).create()
+ mount(root)
+ expect(root.innerHTML).toBe('')
+ expect(target.innerHTML).toBe('one
two')
+
+ // update existing content
+ child1.value = [
+ template('one
')(),
+ template('two
')(),
+ ] as any
+ child2.value = [template('three')()] as any
+ await nextTick()
+ expect(target.innerHTML).toBe('one
two
three')
+
+ // toggling
+ child1.value = [] as any
+ await nextTick()
+ expect(root.innerHTML).toBe('')
+ expect(target.innerHTML).toBe('three')
+
+ // toggle back
+ child1.value = [
+ template('one
')(),
+ template('two
')(),
+ ] as any
+ child2.value = [template('three')()] as any
+ await nextTick()
+ expect(root.innerHTML).toBe('')
+ // should append
+ expect(target.innerHTML).toBe('one
two
three')
+
+ // toggle the other teleport
+ child2.value = [] as any
+ await nextTick()
+ expect(root.innerHTML).toBe('')
+ expect(target.innerHTML).toBe('one
two
')
+ })
+
+ test('should work when using template ref as target', async () => {
+ const root = document.createElement('div')
+ const target = ref(null)
+ const disabled = ref(true)
+
+ const { mount } = define({
+ setup() {
+ const setTemplateRef = createTemplateRefSetter()
+ const n0 = template('')() as any
+ setTemplateRef(n0, target)
+
+ const n1 = createComponent(
+ VaporTeleport,
+ {
+ to: () => target.value,
+ disabled: () => disabled.value,
+ },
+ {
+ default: () => template('teleported
')(),
+ },
+ )
+ return [n0, n1]
+ },
+ }).create()
+ mount(root)
+
+ expect(root.innerHTML).toBe(
+ 'teleported
',
+ )
+ disabled.value = false
+ await nextTick()
+ expect(root.innerHTML).toBe(
+ '',
+ )
+ })
+
+ test('disabled', async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+
+ const disabled = ref(false)
+ const { mount } = define({
+ setup() {
+ const n0 = createComponent(
+ VaporTeleport,
+ {
+ to: () => target,
+ disabled: () => disabled.value,
+ },
+ {
+ default: () => template('teleported
')(),
+ },
+ )
+ const n1 = template('root
')()
+ return [n0, n1]
+ },
+ }).create()
+ mount(root)
+
+ expect(root.innerHTML).toBe('root
')
+ expect(target.innerHTML).toBe('teleported
')
+
+ disabled.value = true
+ await nextTick()
+ expect(root.innerHTML).toBe(
+ 'teleported
root
',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // toggle back
+ disabled.value = false
+ await nextTick()
+ expect(root.innerHTML).toBe(
+ 'root
',
+ )
+ expect(target.innerHTML).toBe('teleported
')
+ })
+
+ test(`the dir hooks of the Teleport's children should be called correctly`, async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+ const toggle = ref(true)
+
+ const spy = vi.fn()
+ const teardown = vi.fn()
+ const dir: VaporDirective = vi.fn((el, source) => {
+ spy()
+ return teardown
+ })
+
+ const { mount } = define({
+ setup() {
+ return createComponent(
+ VaporTeleport,
+ {
+ to: () => target,
+ },
+ {
+ default: () => {
+ return createIf(
+ () => toggle.value,
+ () => {
+ const n1 = template('foo
')() as any
+ withVaporDirectives(n1, [[dir]])
+ return n1
+ },
+ )
+ },
+ },
+ )
+ },
+ }).create()
+
+ mount(root)
+ expect(root.innerHTML).toBe('')
+ expect(target.innerHTML).toBe('foo
')
+ expect(spy).toHaveBeenCalledTimes(1)
+ expect(teardown).not.toHaveBeenCalled()
+
+ toggle.value = false
+ await nextTick()
+ expect(root.innerHTML).toBe('')
+ expect(target.innerHTML).toBe('')
+ expect(spy).toHaveBeenCalledTimes(1)
+ expect(teardown).toHaveBeenCalledTimes(1)
+ })
+
+ test(`ensure that target changes when disabled are updated correctly when enabled`, async () => {
+ const root = document.createElement('div')
+ const target1 = document.createElement('div')
+ const target2 = document.createElement('div')
+ const target3 = document.createElement('div')
+ const target = ref(target1)
+ const disabled = ref(true)
+
+ const { mount } = define({
+ setup() {
+ return createComponent(
+ VaporTeleport,
+ {
+ to: () => target.value,
+ disabled: () => disabled.value,
+ },
+ {
+ default: () => template('teleported
')(),
+ },
+ )
+ },
+ }).create()
+ mount(root)
+
+ disabled.value = false
+ await nextTick()
+ expect(target1.innerHTML).toBe('teleported
')
+ expect(target2.innerHTML).toBe('')
+ expect(target3.innerHTML).toBe('')
+
+ disabled.value = true
+ await nextTick()
+ target.value = target2
+ await nextTick()
+ expect(target1.innerHTML).toBe('')
+ expect(target2.innerHTML).toBe('')
+ expect(target3.innerHTML).toBe('')
+
+ target.value = target3
+ await nextTick()
+ expect(target1.innerHTML).toBe('')
+ expect(target2.innerHTML).toBe('')
+ expect(target3.innerHTML).toBe('')
+
+ disabled.value = false
+ await nextTick()
+ expect(target1.innerHTML).toBe('')
+ expect(target2.innerHTML).toBe('')
+ expect(target3.innerHTML).toBe('teleported
')
+ })
+
+ test('toggle sibling node inside target node', async () => {
+ const root = document.createElement('div')
+ const show = ref(false)
+ const { mount } = define({
+ setup() {
+ return createIf(
+ () => show.value,
+ () => {
+ return createComponent(
+ VaporTeleport,
+ {
+ to: () => root,
+ },
+ {
+ default: () => template('teleported
')(),
+ },
+ )
+ },
+ () => {
+ return template('foo
')()
+ },
+ )
+ },
+ }).create()
+
+ mount(root)
+ expect(root.innerHTML).toBe('foo
')
+
+ show.value = true
+ await nextTick()
+ expect(root.innerHTML).toBe('teleported
')
+
+ show.value = false
+ await nextTick()
+ expect(root.innerHTML).toBe('foo
')
+ })
+
+ test('unmount previous sibling node inside target node', async () => {
+ const root = document.createElement('div')
+ const parentShow = ref(false)
+ const childShow = ref(true)
+
+ const { component: Comp } = define({
+ setup() {
+ return createComponent(
+ VaporTeleport,
+ { to: () => root },
+ {
+ default: () => {
+ return template('foo
')()
+ },
+ },
+ )
+ },
+ })
+
+ const { mount } = define({
+ setup() {
+ return createIf(
+ () => parentShow.value,
+ () =>
+ createIf(
+ () => childShow.value,
+ () => createComponent(Comp),
+ () => template('bar')(),
+ ),
+ () => template('foo')(),
+ )
+ },
+ }).create()
+
+ mount(root)
+ expect(root.innerHTML).toBe('foo')
+
+ parentShow.value = true
+ await nextTick()
+ expect(root.innerHTML).toBe(
+ 'foo
',
+ )
+
+ parentShow.value = false
+ await nextTick()
+ expect(root.innerHTML).toBe('foo')
+ })
+
+ test('accessing template refs inside teleport', async () => {
+ const target = document.createElement('div')
+ const tRef = ref()
+ let tRefInMounted
+
+ const { mount } = define({
+ setup() {
+ onMounted(() => {
+ tRefInMounted = tRef.value
+ })
+ const n1 = createComponent(
+ VaporTeleport,
+ {
+ to: () => target,
+ },
+ {
+ default: () => {
+ const setTemplateRef = createTemplateRefSetter()
+ const n0 = template('teleported
')() as any
+ setTemplateRef(n0, tRef)
+ return n0
+ },
+ },
+ )
+ return n1
+ },
+ }).create()
+ mount(target)
+
+ const child = target.children[0]
+ expect(child.outerHTML).toBe(`teleported
`)
+ expect(tRefInMounted).toBe(child)
+ })
+}
diff --git a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts
index 2126611d718..abdc1a1cf24 100644
--- a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts
+++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts
@@ -1,9 +1,9 @@
import { resolveDynamicComponent } from '@vue/runtime-dom'
-import { DynamicFragment, type VaporFragment } from './block'
import { createComponentWithFallback } from './component'
import { renderEffect } from './renderEffect'
import type { RawProps } from './componentProps'
import type { RawSlots } from './componentSlots'
+import { DynamicFragment, type VaporFragment } from './fragment'
export function createDynamicComponent(
getter: () => any,
diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts
index 0cd8317532f..ca976aa8ccd 100644
--- a/packages/runtime-vapor/src/apiCreateFor.ts
+++ b/packages/runtime-vapor/src/apiCreateFor.ts
@@ -11,12 +11,7 @@ import {
} from '@vue/reactivity'
import { getSequence, isArray, isObject, isString } from '@vue/shared'
import { createComment, createTextNode } from './dom/node'
-import {
- type Block,
- VaporFragment,
- insert,
- remove as removeBlock,
-} from './block'
+import { type Block, insert, remove as removeBlock } from './block'
import { warn } from '@vue/runtime-dom'
import { currentInstance, isVaporComponent } from './component'
import type { DynamicSlot } from './componentSlots'
@@ -24,6 +19,7 @@ import { renderEffect } from './renderEffect'
import { VaporVForFlags } from '../../shared/src/vaporFlags'
import { isHydrating, locateHydrationNode } from './dom/hydration'
import { insertionAnchor, insertionParent } from './insertionState'
+import { VaporFragment } from './fragment'
class ForBlock extends VaporFragment {
scope: EffectScope | undefined
diff --git a/packages/runtime-vapor/src/apiCreateIf.ts b/packages/runtime-vapor/src/apiCreateIf.ts
index 71bfa32d5d3..e83b251d069 100644
--- a/packages/runtime-vapor/src/apiCreateIf.ts
+++ b/packages/runtime-vapor/src/apiCreateIf.ts
@@ -1,7 +1,8 @@
-import { type Block, type BlockFn, DynamicFragment, insert } from './block'
+import { type Block, type BlockFn, insert } from './block'
import { isHydrating, locateHydrationNode } from './dom/hydration'
import { insertionAnchor, insertionParent } from './insertionState'
import { renderEffect } from './renderEffect'
+import { DynamicFragment } from './fragment'
export function createIf(
condition: () => any,
diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts
index b782afd38d3..f1791904ce8 100644
--- a/packages/runtime-vapor/src/block.ts
+++ b/packages/runtime-vapor/src/block.ts
@@ -5,9 +5,12 @@ import {
mountComponent,
unmountComponent,
} from './component'
-import { createComment, createTextNode } from './dom/node'
-import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
import { isHydrating } from './dom/hydration'
+import {
+ type DynamicFragment,
+ type VaporFragment,
+ isFragment,
+} from './fragment'
export type Block =
| Node
@@ -18,69 +21,6 @@ export type Block =
export type BlockFn = (...args: any[]) => Block
-export class VaporFragment {
- nodes: Block
- anchor?: Node
- insert?: (parent: ParentNode, anchor: Node | null) => void
- remove?: (parent?: ParentNode) => void
-
- constructor(nodes: Block) {
- this.nodes = nodes
- }
-}
-
-export class DynamicFragment extends VaporFragment {
- anchor: Node
- scope: EffectScope | undefined
- current?: BlockFn
- fallback?: BlockFn
-
- constructor(anchorLabel?: string) {
- super([])
- this.anchor =
- __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
- }
-
- update(render?: BlockFn, key: any = render): void {
- if (key === this.current) {
- return
- }
- this.current = key
-
- pauseTracking()
- const parent = this.anchor.parentNode
-
- // teardown previous branch
- if (this.scope) {
- this.scope.stop()
- parent && remove(this.nodes, parent)
- }
-
- if (render) {
- this.scope = new EffectScope()
- this.nodes = this.scope.run(render) || []
- if (parent) insert(this.nodes, parent, this.anchor)
- } else {
- this.scope = undefined
- this.nodes = []
- }
-
- if (this.fallback && !isValidBlock(this.nodes)) {
- parent && remove(this.nodes, parent)
- this.nodes =
- (this.scope || (this.scope = new EffectScope())).run(this.fallback) ||
- []
- parent && insert(this.nodes, parent, this.anchor)
- }
-
- resetTracking()
- }
-}
-
-export function isFragment(val: NonNullable): val is VaporFragment {
- return val instanceof VaporFragment
-}
-
export function isBlock(val: NonNullable): val is Block {
return (
val instanceof Node ||
@@ -129,7 +69,7 @@ export function insert(
// TODO handle hydration for vdom interop
block.insert(parent, anchor)
} else {
- insert(block.nodes, parent, anchor)
+ insert(block.nodes, block.target || parent, block.targetAnchor || anchor)
}
if (block.anchor) insert(block.anchor, parent, anchor)
}
@@ -182,7 +122,11 @@ export function normalizeBlock(block: Block): Node[] {
} else if (isVaporComponent(block)) {
nodes.push(...normalizeBlock(block.block!))
} else {
- nodes.push(...normalizeBlock(block.nodes))
+ if (block.getNodes) {
+ nodes.push(...normalizeBlock(block.getNodes()))
+ } else {
+ nodes.push(...normalizeBlock(block.nodes))
+ }
block.anchor && nodes.push(block.anchor)
}
return nodes
diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts
index 548babebf8b..672bb75dc1e 100644
--- a/packages/runtime-vapor/src/component.ts
+++ b/packages/runtime-vapor/src/component.ts
@@ -60,6 +60,7 @@ import {
import { hmrReload, hmrRerender } from './hmr'
import { isHydrating, locateHydrationNode } from './dom/hydration'
import { insertionAnchor, insertionParent } from './insertionState'
+import { isVaporTeleport } from './components/Teleport'
export { currentInstance } from '@vue/runtime-dom'
@@ -157,6 +158,18 @@ export function createComponent(
return frag
}
+ // teleport
+ if (isVaporTeleport(component)) {
+ const frag = component.process(rawProps!, rawSlots!)
+ if (!isHydrating && _insertionParent) {
+ insert(frag, _insertionParent, _insertionAnchor)
+ } else {
+ frag.hydrate()
+ }
+
+ return frag as any
+ }
+
if (
isSingleRoot &&
component.inheritAttrs !== false &&
@@ -270,7 +283,7 @@ export function createComponent(
onScopeDispose(() => unmountComponent(instance), true)
if (!isHydrating && _insertionParent) {
- insert(instance.block, _insertionParent, _insertionAnchor)
+ mountComponent(instance, _insertionParent, _insertionAnchor)
}
return instance
@@ -370,6 +383,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
setupState?: Record
devtoolsRawSetupState?: any
hmrRerender?: () => void
+ hmrRerenderEffects?: (() => void)[]
hmrReload?: (newComp: VaporComponent) => void
propsOptions?: NormalizedPropsOptions
emitsOptions?: ObjectEmitsOptions | null
diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts
index 74296e09466..3d17e5c0a5b 100644
--- a/packages/runtime-vapor/src/componentSlots.ts
+++ b/packages/runtime-vapor/src/componentSlots.ts
@@ -1,11 +1,12 @@
import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
-import { type Block, type BlockFn, DynamicFragment, insert } from './block'
+import { type Block, type BlockFn, insert } from './block'
import { rawPropsProxyHandlers } from './componentProps'
import { currentInstance, isRef } from '@vue/runtime-dom'
import type { LooseRawProps, VaporComponentInstance } from './component'
import { renderEffect } from './renderEffect'
import { insertionAnchor, insertionParent } from './insertionState'
import { isHydrating, locateHydrationNode } from './dom/hydration'
+import { DynamicFragment } from './fragment'
export type RawSlots = Record & {
$?: DynamicSlotSource[]
diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts
new file mode 100644
index 00000000000..ca1f184d5dd
--- /dev/null
+++ b/packages/runtime-vapor/src/components/Teleport.ts
@@ -0,0 +1,254 @@
+import {
+ type TeleportProps,
+ currentInstance,
+ isTeleportDeferred,
+ isTeleportDisabled,
+ queuePostFlushCb,
+ resolveTeleportTarget,
+ warn,
+} from '@vue/runtime-dom'
+import { type Block, type BlockFn, insert, remove } from '../block'
+import { createComment, createTextNode, querySelector } from '../dom/node'
+import {
+ type LooseRawProps,
+ type LooseRawSlots,
+ type VaporComponentInstance,
+ isVaporComponent,
+} from '../component'
+import { rawPropsProxyHandlers } from '../componentProps'
+import { renderEffect } from '../renderEffect'
+import { extend, isArray } from '@vue/shared'
+import { VaporFragment } from '../fragment'
+
+const instanceToTeleportMap: WeakMap =
+ __DEV__ ? new WeakMap() : (undefined as any)
+
+export const VaporTeleportImpl = {
+ name: 'VaporTeleport',
+ __isTeleport: true,
+ __vapor: true,
+
+ process(props: LooseRawProps, slots: LooseRawSlots): TeleportFragment {
+ const frag = new TeleportFragment()
+ const updateChildrenEffect = renderEffect(() =>
+ frag.updateChildren(slots.default && (slots.default as BlockFn)()),
+ )
+
+ const updateEffect = renderEffect(() => {
+ frag.update(
+ // access the props to trigger tracking
+ extend(
+ {},
+ new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps,
+ ),
+ )
+ })
+
+ if (__DEV__) {
+ // used in `normalizeBlock` to get nodes of TeleportFragment during
+ // HMR updates. returns empty array if content is mounted in target
+ // container to prevent incorrect parent node lookup.
+ frag.getNodes = () => {
+ return frag.parent !== frag.currentParent ? [] : frag.nodes
+ }
+
+ // for HMR rerender
+ const instance = currentInstance as VaporComponentInstance
+ ;(
+ instance!.hmrRerenderEffects || (instance!.hmrRerenderEffects = [])
+ ).push(() => {
+ // remove the teleport content
+ frag.remove()
+
+ // stop effects
+ updateChildrenEffect.stop()
+ updateEffect.stop()
+ })
+
+ // for HMR reload
+ const nodes = frag.nodes
+ if (isVaporComponent(nodes)) {
+ instanceToTeleportMap.set(nodes, frag)
+ } else if (isArray(nodes)) {
+ nodes.forEach(
+ node =>
+ isVaporComponent(node) && instanceToTeleportMap.set(node, frag),
+ )
+ }
+ }
+
+ return frag
+ },
+}
+
+export class TeleportFragment extends VaporFragment {
+ anchor: Node
+
+ private targetStart?: Node
+ private mainAnchor?: Node
+ private placeholder?: Node
+ private mountContainer?: ParentNode | null
+ private mountAnchor?: Node | null
+
+ constructor() {
+ super([])
+ this.anchor = __DEV__ ? createComment('teleport') : createTextNode()
+ }
+
+ get currentParent(): ParentNode {
+ return (this.mountContainer || this.parent)!
+ }
+
+ get currentAnchor(): Node | null {
+ return this.mountAnchor || this.anchor
+ }
+
+ get parent(): ParentNode | null {
+ return this.anchor.parentNode
+ }
+
+ updateChildren(children: Block): void {
+ // not mounted yet
+ if (!this.parent) {
+ this.nodes = children
+ return
+ }
+
+ // teardown previous nodes
+ remove(this.nodes, this.currentParent)
+ // mount new nodes
+ insert((this.nodes = children), this.currentParent, this.currentAnchor)
+ }
+
+ update(props: TeleportProps): void {
+ const mount = (parent: ParentNode, anchor: Node | null) => {
+ insert(
+ this.nodes,
+ (this.mountContainer = parent),
+ (this.mountAnchor = anchor),
+ )
+ }
+
+ const mountToTarget = () => {
+ const target = (this.target = resolveTeleportTarget(props, querySelector))
+ if (target) {
+ if (
+ // initial mount into target
+ !this.targetAnchor ||
+ // target changed
+ this.targetAnchor.parentNode !== target
+ ) {
+ insert((this.targetStart = createTextNode('')), target)
+ insert((this.targetAnchor = createTextNode('')), target)
+ }
+
+ mount(target, this.targetAnchor!)
+ } else if (__DEV__) {
+ warn(
+ `Invalid Teleport target on ${this.targetAnchor ? 'update' : 'mount'}:`,
+ target,
+ `(${typeof target})`,
+ )
+ }
+ }
+
+ // mount into main container
+ if (isTeleportDisabled(props)) {
+ if (this.parent) {
+ if (!this.mainAnchor) {
+ this.mainAnchor = __DEV__
+ ? createComment('teleport end')
+ : createTextNode()
+ }
+ if (!this.placeholder) {
+ this.placeholder = __DEV__
+ ? createComment('teleport start')
+ : createTextNode()
+ }
+ if (!this.mainAnchor.isConnected) {
+ insert(this.placeholder, this.parent, this.anchor)
+ insert(this.mainAnchor, this.parent, this.anchor)
+ }
+
+ mount(this.parent, this.mainAnchor)
+ }
+ }
+ // mount into target container
+ else {
+ if (isTeleportDeferred(props)) {
+ queuePostFlushCb(mountToTarget)
+ } else {
+ mountToTarget()
+ }
+ }
+ }
+
+ remove = (parent: ParentNode | undefined = this.parent!): void => {
+ // remove nodes
+ if (this.nodes) {
+ remove(this.nodes, this.currentParent)
+ this.nodes = []
+ }
+
+ // remove anchors
+ if (this.targetStart) {
+ remove(this.targetStart!, this.target!)
+ this.targetStart = undefined
+ remove(this.targetAnchor!, this.target!)
+ this.targetAnchor = undefined
+ }
+
+ if (this.placeholder) {
+ remove(this.placeholder!, parent)
+ this.placeholder = undefined
+ remove(this.mainAnchor!, parent)
+ this.mainAnchor = undefined
+ }
+
+ this.mountContainer = undefined
+ this.mountAnchor = undefined
+ }
+
+ hydrate(): void {
+ // TODO
+ }
+}
+
+export const VaporTeleport = VaporTeleportImpl as unknown as {
+ __vapor: true
+ __isTeleport: true
+ new (): {
+ $props: TeleportProps
+ $slots: {
+ default(): Block
+ }
+ }
+}
+
+export function isVaporTeleport(
+ value: unknown,
+): value is typeof VaporTeleportImpl {
+ return value === VaporTeleportImpl
+}
+
+/**
+ * dev only
+ * during root component HMR reload, since the old component will be unmounted
+ * and a new one will be mounted, we need to update the teleport's nodes
+ * to ensure they are up to date.
+ */
+export function handleTeleportRootComponentHmrReload(
+ instance: VaporComponentInstance,
+ newInstance: VaporComponentInstance,
+): void {
+ const teleport = instanceToTeleportMap.get(instance)
+ if (teleport) {
+ instanceToTeleportMap.set(newInstance, teleport)
+ if (teleport.nodes === instance) {
+ teleport.nodes = newInstance
+ } else if (isArray(teleport.nodes)) {
+ const i = teleport.nodes.indexOf(instance)
+ if (i !== -1) teleport.nodes[i] = newInstance
+ }
+ }
+}
diff --git a/packages/runtime-vapor/src/directives/vShow.ts b/packages/runtime-vapor/src/directives/vShow.ts
index ac4c066b71d..b0fc22c14c8 100644
--- a/packages/runtime-vapor/src/directives/vShow.ts
+++ b/packages/runtime-vapor/src/directives/vShow.ts
@@ -6,8 +6,9 @@ import {
} from '@vue/runtime-dom'
import { renderEffect } from '../renderEffect'
import { isVaporComponent } from '../component'
-import { type Block, DynamicFragment } from '../block'
+import type { Block } from '../block'
import { isArray } from '@vue/shared'
+import { DynamicFragment } from '../fragment'
export function applyVShow(target: Block, source: () => any): void {
if (isVaporComponent(target)) {
diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts
new file mode 100644
index 00000000000..3e4fcb221c3
--- /dev/null
+++ b/packages/runtime-vapor/src/fragment.ts
@@ -0,0 +1,69 @@
+import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
+import { createComment, createTextNode } from './dom/node'
+import { type Block, type BlockFn, insert, isValidBlock, remove } from './block'
+
+export class VaporFragment {
+ nodes: Block
+ target?: ParentNode | null
+ targetAnchor?: Node | null
+ anchor?: Node
+ insert?: (parent: ParentNode, anchor: Node | null) => void
+ remove?: (parent?: ParentNode) => void
+ getNodes?: () => Block
+
+ constructor(nodes: Block) {
+ this.nodes = nodes
+ }
+}
+
+export class DynamicFragment extends VaporFragment {
+ anchor: Node
+ scope: EffectScope | undefined
+ current?: BlockFn
+ fallback?: BlockFn
+
+ constructor(anchorLabel?: string) {
+ super([])
+ this.anchor =
+ __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
+ }
+
+ update(render?: BlockFn, key: any = render): void {
+ if (key === this.current) {
+ return
+ }
+ this.current = key
+
+ pauseTracking()
+ const parent = this.anchor.parentNode
+
+ // teardown previous branch
+ if (this.scope) {
+ this.scope.stop()
+ parent && remove(this.nodes, parent)
+ }
+
+ if (render) {
+ this.scope = new EffectScope()
+ this.nodes = this.scope.run(render) || []
+ if (parent) insert(this.nodes, parent, this.anchor)
+ } else {
+ this.scope = undefined
+ this.nodes = []
+ }
+
+ if (this.fallback && !isValidBlock(this.nodes)) {
+ parent && remove(this.nodes, parent)
+ this.nodes =
+ (this.scope || (this.scope = new EffectScope())).run(this.fallback) ||
+ []
+ parent && insert(this.nodes, parent, this.anchor)
+ }
+
+ resetTracking()
+ }
+}
+
+export function isFragment(val: NonNullable): val is VaporFragment {
+ return val instanceof VaporFragment
+}
diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts
index 741f385861d..63e5376896a 100644
--- a/packages/runtime-vapor/src/hmr.ts
+++ b/packages/runtime-vapor/src/hmr.ts
@@ -13,12 +13,17 @@ import {
mountComponent,
unmountComponent,
} from './component'
+import { handleTeleportRootComponentHmrReload } from './components/Teleport'
export function hmrRerender(instance: VaporComponentInstance): void {
const normalized = normalizeBlock(instance.block)
const parent = normalized[0].parentNode!
const anchor = normalized[normalized.length - 1].nextSibling
remove(instance.block, parent)
+ if (instance.hmrRerenderEffects) {
+ instance.hmrRerenderEffects.forEach(e => e())
+ instance.hmrRerenderEffects.length = 0
+ }
const prev = currentInstance
simpleSetCurrentInstance(instance)
pushWarningContext(instance)
@@ -46,4 +51,5 @@ export function hmrReload(
)
simpleSetCurrentInstance(prev, instance.parent)
mountComponent(newInstance, parent, anchor)
+ handleTeleportRootComponentHmrReload(instance, newInstance)
}
diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts
index 682532fa4d8..c2716059df2 100644
--- a/packages/runtime-vapor/src/index.ts
+++ b/packages/runtime-vapor/src/index.ts
@@ -3,9 +3,10 @@ export { createVaporApp, createVaporSSRApp } from './apiCreateApp'
export { defineVaporComponent } from './apiDefineComponent'
export { vaporInteropPlugin } from './vdomInterop'
export type { VaporDirective } from './directives/custom'
+export { VaporTeleport } from './components/Teleport'
// compiler-use only
-export { insert, prepend, remove, isFragment, VaporFragment } from './block'
+export { insert, prepend, remove } from './block'
export { setInsertionState } from './insertionState'
export { createComponent, createComponentWithFallback } from './component'
export { renderEffect } from './renderEffect'
@@ -42,3 +43,5 @@ export {
applyDynamicModel,
} from './directives/vModel'
export { withVaporDirectives } from './directives/custom'
+export { isFragment } from './fragment'
+export { VaporFragment } from './fragment'
diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts
index a9fa9b33562..227d7933e78 100644
--- a/packages/runtime-vapor/src/renderEffect.ts
+++ b/packages/runtime-vapor/src/renderEffect.ts
@@ -11,7 +11,10 @@ import {
import { type VaporComponentInstance, isVaporComponent } from './component'
import { invokeArrayFns } from '@vue/shared'
-export function renderEffect(fn: () => void, noLifecycle = false): void {
+export function renderEffect(
+ fn: () => void,
+ noLifecycle = false,
+): ReactiveEffect {
const instance = currentInstance as VaporComponentInstance | null
const scope = getCurrentScope()
if (__DEV__ && !__TEST__ && !scope && !isVaporComponent(instance)) {
@@ -66,5 +69,6 @@ export function renderEffect(fn: () => void, noLifecycle = false): void {
effect.scheduler = () => queueJob(job)
effect.run()
+ return effect
// TODO recurse handling
}
diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts
index 77228fd72a0..2249bbb2fdc 100644
--- a/packages/runtime-vapor/src/vdomInterop.ts
+++ b/packages/runtime-vapor/src/vdomInterop.ts
@@ -26,13 +26,14 @@ import {
mountComponent,
unmountComponent,
} from './component'
-import { type Block, VaporFragment, insert, remove } from './block'
+import { type Block, insert, remove } from './block'
import { EMPTY_OBJ, extend, isFunction } from '@vue/shared'
import { type RawProps, rawPropsProxyHandlers } from './componentProps'
import type { RawSlots, VaporSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
import { createTextNode } from './dom/node'
import { optimizePropertyLookup } from './dom/prop'
+import { VaporFragment } from './fragment'
// mounting vapor components and slots in vdom
const vaporInteropImpl: Omit<