// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.
import {
+ child,
createComponent,
+ createFor,
createForSlots,
createIf,
createSlot,
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>()
expect(html()).toBe('fallback<!--if--><!--slot-->')
})
- test('render fallback with nested v-if ', async () => {
+ test('render fallback with nested v-if', async () => {
const Child = {
setup() {
return createSlot('default', null, () =>
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-->')
+ })
})
})
import { createComment, createTextNode } from './dom/node'
import {
type Block,
+ ForFragment,
VaporFragment,
insert,
+ remove,
remove as removeBlock,
} from './block'
import { warn } from '@vue/runtime-dom'
setup?: (_: {
createSelector: (source: () => any) => (cb: () => void) => void
}) => void,
-): VaporFragment => {
+): ForFragment => {
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
if (isHydrating) {
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)
const newLength = source.values.length
const oldLength = oldBlocks.length
newBlocks = new Array(newLength)
+ let isFallback = false
const prevSub = setActiveSub()
} 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)
parent!.textContent = ''
parent!.appendChild(parentAnchor)
}
+
+ // render fallback nodes
+ if (frag.fallback) {
+ insert((frag.nodes[0] = frag.fallback()), parent!, parentAnchor)
+ oldBlocks = []
+ isFallback = true
+ }
} else if (!getKey) {
// unkeyed fast path
const commonLength = Math.min(newLength, oldLength)
}
}
- frag.nodes = [(oldBlocks = newBlocks)]
- if (parentAnchor) {
- frag.nodes.push(parentAnchor)
+ if (!isFallback) {
+ frag.nodes = [(oldBlocks = newBlocks)]
+ if (parentAnchor) {
+ frag.nodes.push(parentAnchor)
+ }
}
-
setActiveSub(prevSub)
}
export type BlockFn = (...args: any[]) => Block
-export class VaporFragment {
- nodes: Block
+export class VaporFragment<T extends Block = Block> {
+ nodes: T
anchor?: Node
insert?: (parent: ParentNode, anchor: Node | null) => void
remove?: (parent?: ParentNode) => void
+ fallback?: BlockFn
- constructor(nodes: Block) {
+ constructor(nodes: T) {
this.nodes = nodes
}
}
+export class ForFragment extends VaporFragment<Block[]> {
+ constructor(nodes: Block[]) {
+ super(nodes)
+ }
+}
+
export class DynamicFragment extends VaporFragment {
anchor: Node
scope: EffectScope | undefined
this.nodes = []
}
- if (this.fallback && !isValidBlock(this.nodes)) {
+ if (this.fallback) {
parent && remove(this.nodes, parent)
- // handle nested dynamic fragment
- if (isFragment(this.nodes)) {
- renderFallback(this.nodes, this.fallback, key)
- } else {
- this.nodes =
- (this.scope || (this.scope = new EffectScope())).run(this.fallback) ||
- []
- }
+ const scope = this.scope || (this.scope = new EffectScope())
+ scope.run(() => {
+ // handle nested fragment
+ if (isFragment(this.nodes)) {
+ ensureFallback(this.nodes, this.fallback!)
+ } else if (!isValidBlock(this.nodes)) {
+ this.nodes = this.fallback!() || []
+ }
+ })
+
parent && insert(this.nodes, parent, this.anchor)
}
}
}
-function renderFallback(
- fragment: VaporFragment,
- fallback: BlockFn,
- key: any,
-): void {
+function ensureFallback(fragment: VaporFragment, fallback: BlockFn): void {
+ if (!fragment.fallback) fragment.fallback = fallback
+
if (fragment instanceof DynamicFragment) {
const nodes = fragment.nodes
if (isFragment(nodes)) {
- renderFallback(nodes, fallback, key)
- } else {
- if (!fragment.fallback) fragment.fallback = fallback
- fragment.update(fragment.fallback, key)
+ ensureFallback(nodes, fallback)
+ } else if (!isValidBlock(nodes)) {
+ fragment.update(fragment.fallback)
}
+ } else if (fragment instanceof ForFragment) {
+ if (!isValidBlock(fragment.nodes[0])) {
+ fragment.nodes[0] = [fallback() || []] as Block[]
+ }
+ } else {
+ // vdom slots
}
}
} else if (isVaporComponent(block)) {
return isValidBlock(block.block)
} else if (isArray(block)) {
- return block.length > 0 && block.every(isValidBlock)
+ return block.length > 0 && block.some(isValidBlock)
} else {
// fragment
return isValidBlock(block.nodes)