Skip to content

fix(runtime-vapor): render slot fallback if provided content is invalid #13669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: minor
Choose a base branch
from
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
225 changes: 223 additions & 2 deletions packages/runtime-vapor/__tests__/componentSlots.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.

import {
child,
createComponent,
createFor,
createForSlots,
createIf,
createSlot,
Expand All @@ -12,10 +14,15 @@ import {
renderEffect,
template,
} from '../src'
import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
import {
currentInstance,
nextTick,
ref,
toDisplayString,
} from '@vue/runtime-dom'
import { makeRender } from './_utils'
import type { DynamicSlot } from '../src/componentSlots'
import { setElementText } from '../src/dom/prop'
import { setElementText, setText } from '../src/dom/prop'

const define = makeRender<any>()

Expand Down Expand Up @@ -502,5 +509,219 @@ describe('component: slots', () => {
await nextTick()
expect(host.innerHTML).toBe('<div><h1></h1><!--slot--></div>')
})

test('render fallback when slot content is not valid', async () => {
const Child = {
setup() {
return createSlot('default', null, () =>
document.createTextNode('fallback'),
)
},
}

const { html } = define({
setup() {
return createComponent(Child, null, {
default: () => {
return template('<!--comment-->')()
},
})
},
}).render()

expect(html()).toBe('fallback<!--slot-->')
})

test('render fallback when v-if condition is false', async () => {
const Child = {
setup() {
return createSlot('default', null, () =>
document.createTextNode('fallback'),
)
},
}

const toggle = ref(false)

const { html } = define({
setup() {
return createComponent(Child, null, {
default: () => {
return createIf(
() => toggle.value,
() => {
return document.createTextNode('content')
},
)
},
})
},
}).render()

expect(html()).toBe('fallback<!--if--><!--slot-->')

toggle.value = true
await nextTick()
expect(html()).toBe('content<!--if--><!--slot-->')

toggle.value = false
await nextTick()
expect(html()).toBe('fallback<!--if--><!--slot-->')
})

test('render fallback with nested v-if', async () => {
const Child = {
setup() {
return createSlot('default', null, () =>
document.createTextNode('fallback'),
)
},
}

const outerShow = ref(false)
const innerShow = ref(false)

const { html } = define({
setup() {
return createComponent(Child, null, {
default: () => {
return createIf(
() => outerShow.value,
() => {
return createIf(
() => innerShow.value,
() => {
return document.createTextNode('content')
},
)
},
)
},
})
},
}).render()

expect(html()).toBe('fallback<!--if--><!--slot-->')

outerShow.value = true
await nextTick()
expect(html()).toBe('fallback<!--if--><!--if--><!--slot-->')

innerShow.value = true
await nextTick()
expect(html()).toBe('content<!--if--><!--if--><!--slot-->')

innerShow.value = false
await nextTick()
expect(html()).toBe('fallback<!--if--><!--if--><!--slot-->')

outerShow.value = false
await nextTick()
expect(html()).toBe('fallback<!--if--><!--slot-->')

outerShow.value = true
await nextTick()
expect(html()).toBe('fallback<!--if--><!--if--><!--slot-->')

innerShow.value = true
await nextTick()
expect(html()).toBe('content<!--if--><!--if--><!--slot-->')
})

test('render fallback with v-for', async () => {
const Child = {
setup() {
return createSlot('default', null, () =>
document.createTextNode('fallback'),
)
},
}

const items = ref<number[]>([1])
const { html } = define({
setup() {
return createComponent(Child, null, {
default: () => {
const n2 = createFor(
() => items.value,
for_item0 => {
const n4 = template('<span> </span>')() as any
const x4 = child(n4) as any
renderEffect(() =>
setText(x4, toDisplayString(for_item0.value)),
)
return n4
},
)
return n2
},
})
},
}).render()

expect(html()).toBe('<span>1</span><!--for--><!--slot-->')

items.value.pop()
await nextTick()
expect(html()).toBe('fallback<!--for--><!--slot-->')

items.value.pop()
await nextTick()
expect(html()).toBe('fallback<!--for--><!--slot-->')

items.value.push(2)
await nextTick()
expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
})

test('render fallback with v-for (empty source)', async () => {
const Child = {
setup() {
return createSlot('default', null, () =>
document.createTextNode('fallback'),
)
},
}

const items = ref<number[]>([])
const { html } = define({
setup() {
return createComponent(Child, null, {
default: () => {
const n2 = createFor(
() => items.value,
for_item0 => {
const n4 = template('<span> </span>')() as any
const x4 = child(n4) as any
renderEffect(() =>
setText(x4, toDisplayString(for_item0.value)),
)
return n4
},
)
return n2
},
})
},
}).render()

expect(html()).toBe('fallback<!--for--><!--slot-->')

items.value.push(1)
await nextTick()
expect(html()).toBe('<span>1</span><!--for--><!--slot-->')

items.value.pop()
await nextTick()
expect(html()).toBe('fallback<!--for--><!--slot-->')

items.value.pop()
await nextTick()
expect(html()).toBe('fallback<!--for--><!--slot-->')

items.value.push(2)
await nextTick()
expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
})
})
})
27 changes: 21 additions & 6 deletions packages/runtime-vapor/src/apiCreateFor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import { isArray, isObject, isString } from '@vue/shared'
import { createComment, createTextNode } from './dom/node'
import {
type Block,
ForFragment,
VaporFragment,
insert,
remove,
remove as removeBlock,
} from './block'
import { warn } from '@vue/runtime-dom'
Expand Down Expand Up @@ -77,7 +79,7 @@ export const createFor = (
setup?: (_: {
createSelector: (source: () => any) => (cb: () => void) => void
}) => void,
): VaporFragment => {
): ForFragment => {
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
if (isHydrating) {
Expand All @@ -94,7 +96,7 @@ export const createFor = (
let currentKey: any
// TODO handle this in hydration
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
const frag = new VaporFragment(oldBlocks)
const frag = new ForFragment(oldBlocks)
const instance = currentInstance!
const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE)
const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT)
Expand All @@ -112,6 +114,7 @@ export const createFor = (
const newLength = source.values.length
const oldLength = oldBlocks.length
newBlocks = new Array(newLength)
let isFallback = false

const prevSub = setActiveSub()

Expand All @@ -123,6 +126,11 @@ export const createFor = (
} else {
parent = parent || parentAnchor!.parentNode
if (!oldLength) {
// remove fallback nodes
if (frag.fallback && (frag.nodes[0] as Block[]).length > 0) {
remove(frag.nodes[0], parent!)
}

// fast path for all new
for (let i = 0; i < newLength; i++) {
mount(source, i)
Expand All @@ -140,6 +148,12 @@ export const createFor = (
parent!.textContent = ''
parent!.appendChild(parentAnchor)
}

// render fallback nodes
if (frag.fallback) {
insert((frag.nodes[0] = frag.fallback()), parent!, parentAnchor)
isFallback = true
}
} else if (!getKey) {
// unkeyed fast path
const commonLength = Math.min(newLength, oldLength)
Expand Down Expand Up @@ -324,11 +338,12 @@ export const createFor = (
}
}

frag.nodes = [(oldBlocks = newBlocks)]
if (parentAnchor) {
frag.nodes.push(parentAnchor)
if (!isFallback) {
frag.nodes = [(oldBlocks = newBlocks)]
if (parentAnchor) frag.nodes.push(parentAnchor)
} else {
oldBlocks = []
}

setActiveSub(prevSub)
}

Expand Down
Loading