Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions packages/runtime-vapor/__tests__/vdomInterop.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
createIf,
createSlot,
createTemplateRefSetter,
createVaporApp,
defineVaporAsyncComponent,
defineVaporComponent,
insert,
Expand Down Expand Up @@ -2085,6 +2086,16 @@ describe('vdomInterop', () => {
})

describe('KeepAlive', () => {
const VDomCommentWrapper = defineComponent({
setup(_, { slots }) {
return () => [
createCommentVNode('before'),
renderSlot(slots, 'default'),
createCommentVNode('after'),
]
},
})

function assertHookCalls(
hooks: {
beforeMount: any
Expand Down Expand Up @@ -2318,6 +2329,231 @@ describe('vdomInterop', () => {
expect(html()).toBe('<div><!----></div>')
})

test('should remove teleported slot content when unmounting comment-wrapped vdom slot inside VaporKeepAlive', async () => {
const show = ref(true)
const target = document.createElement('div')
target.id = 'keepalive-teleport-target'
document.body.appendChild(target)

const App = defineVaporComponent({
setup() {
return createIf(
() => show.value,
() =>
createComponent(VDomCommentWrapper as any, null, {
default: withVaporCtx(() =>
createComponent(VaporKeepAlive, null, {
default: withVaporCtx(() =>
createComponent(
VaporTeleport,
{ to: () => '#keepalive-teleport-target' },
{
default: () => template('<input>')(),
},
),
),
}),
),
}),
)
},
})

const host = document.createElement('div')
const app = createVaporApp(App)
app.use(vaporInteropPlugin)
app.mount(host)

try {
await nextTick()
expect(target.innerHTML).toBe('<input>')

show.value = false
await nextTick()

expect(target.innerHTML).toBe('')
} finally {
app.unmount()
host.remove()
target.remove()
}
})

test('should remove inline teleported slot content when disabled inside comment-wrapped vdom slot under VaporKeepAlive', async () => {
const show = ref(true)
const target = document.createElement('div')
target.id = 'keepalive-disabled-teleport-target'
document.body.appendChild(target)

const App = defineVaporComponent({
setup() {
return createIf(
() => show.value,
() =>
createComponent(VDomCommentWrapper as any, null, {
default: withVaporCtx(() =>
createComponent(VaporKeepAlive, null, {
default: withVaporCtx(() =>
createComponent(
VaporTeleport,
{
to: () => '#keepalive-disabled-teleport-target',
disabled: () => true,
},
{
default: () => template('<input>')(),
},
),
),
}),
),
}),
)
},
})

const host = document.createElement('div')
const app = createVaporApp(App)
app.use(vaporInteropPlugin)
app.mount(host)

try {
await nextTick()
expect(host.querySelector('input')).not.toBeNull()
expect(target.innerHTML).toBe('')

show.value = false
await nextTick()

expect(host.querySelector('input')).toBeNull()
expect(target.innerHTML).toBe('')
} finally {
app.unmount()
host.remove()
target.remove()
}
})

test('should remove moved teleported slot content when comment-wrapped vdom slot under VaporKeepAlive unmounts', async () => {
const show = ref(true)
const to = ref('#keepalive-teleport-target-a')
const targetA = document.createElement('div')
targetA.id = 'keepalive-teleport-target-a'
const targetB = document.createElement('div')
targetB.id = 'keepalive-teleport-target-b'
document.body.append(targetA, targetB)

const App = defineVaporComponent({
setup() {
return createIf(
() => show.value,
() =>
createComponent(VDomCommentWrapper as any, null, {
default: withVaporCtx(() =>
createComponent(VaporKeepAlive, null, {
default: withVaporCtx(() =>
createComponent(
VaporTeleport,
{ to: () => to.value },
{
default: () => template('<input>')(),
},
),
),
}),
),
}),
)
},
})

const host = document.createElement('div')
const app = createVaporApp(App)
app.use(vaporInteropPlugin)
app.mount(host)

try {
await nextTick()
expect(targetA.innerHTML).toBe('<input>')
expect(targetB.innerHTML).toBe('')

to.value = '#keepalive-teleport-target-b'
await nextTick()

expect(targetA.innerHTML).toBe('')
expect(targetB.innerHTML).toBe('<input>')

show.value = false
await nextTick()

expect(targetA.innerHTML).toBe('')
expect(targetB.innerHTML).toBe('')
} finally {
app.unmount()
host.remove()
targetA.remove()
targetB.remove()
}
})

test('should remove teleported slot content when KeepAlive is nested inside a vapor wrapper in comment-wrapped vdom slot', async () => {
const show = ref(true)
const target = document.createElement('div')
target.id = 'nested-keepalive-teleport-target'
document.body.appendChild(target)

const NestedKeepAlive = defineVaporComponent({
setup() {
return createComponent(VaporKeepAlive, null, {
default: withVaporCtx(() => createSlot('default')),
})
},
})

const App = defineVaporComponent({
setup() {
return createIf(
() => show.value,
() =>
createComponent(VDomCommentWrapper as any, null, {
default: withVaporCtx(() =>
createComponent(NestedKeepAlive, null, {
default: withVaporCtx(() =>
createComponent(
VaporTeleport,
{ to: () => '#nested-keepalive-teleport-target' },
{
default: () => template('<input>')(),
},
),
),
}),
),
}),
)
},
})

const host = document.createElement('div')
const app = createVaporApp(App)
app.use(vaporInteropPlugin)
app.mount(host)

try {
await nextTick()
expect(target.innerHTML).toBe('<input>')

show.value = false
await nextTick()

expect(target.innerHTML).toBe('')
} finally {
app.unmount()
host.remove()
target.remove()
}
})

test('should update props on reactivation of vapor child in vdom KeepAlive', async () => {
const VaporChild = defineVaporComponent({
props: { msg: String },
Expand Down
26 changes: 25 additions & 1 deletion packages/runtime-vapor/src/vdomInterop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
createComponent,
getCurrentScopeId,
getRootElement,
isVaporComponent,
mountComponent,
unmountComponent,
} from './component'
Expand Down Expand Up @@ -275,7 +276,17 @@ const vaporInteropImpl: Omit<
unmountComponent(instance, container)
}
} else if (vnode.vb) {
remove(vnode.vb, container)
const anchor = vnode.anchor as Node | null
// Fragment child unmounts invoke VaporSlot with doRemove = false, so the
// renderer does not pass us a container. Most slot blocks can still
// clean themselves up without it, but KeepAlive needs the host container
// to remove its current block and reach nested Teleport cleanup.
const blockContainer =
container ||
(needsSlotBlockUnmountContainer(vnode.vb)
? ((anchor && anchor.parentNode) as ParentNode)
: undefined)
remove(vnode.vb, blockContainer)
stopVaporSlotScope(vnode)
}
if (doRemove) {
Expand Down Expand Up @@ -1172,6 +1183,19 @@ function renderVDOMSlot(
return frag
}

function needsSlotBlockUnmountContainer(block: Block): boolean {
if (isVaporComponent(block)) {
return isKeepAlive(block) || needsSlotBlockUnmountContainer(block.block)
}
if (isArray(block)) {
return block.some(needsSlotBlockUnmountContainer)
}
if (isFragment(block)) {
return needsSlotBlockUnmountContainer(block.nodes)
}
return false
}

export const vaporInteropPlugin: Plugin = app => {
setInteropEnabled()
const internals = ensureRenderer().internals
Expand Down
Loading