Skip to content

Commit a65da3a

Browse files
committed
wip: render fallback nodes for vfor
1 parent e28b96b commit a65da3a

File tree

3 files changed

+162
-31
lines changed

3 files changed

+162
-31
lines changed

packages/runtime-vapor/__tests__/componentSlots.spec.ts

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.
22

33
import {
4+
child,
45
createComponent,
6+
createFor,
57
createForSlots,
68
createIf,
79
createSlot,
@@ -12,10 +14,15 @@ import {
1214
renderEffect,
1315
template,
1416
} from '../src'
15-
import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
17+
import {
18+
currentInstance,
19+
nextTick,
20+
ref,
21+
toDisplayString,
22+
} from '@vue/runtime-dom'
1623
import { makeRender } from './_utils'
1724
import type { DynamicSlot } from '../src/componentSlots'
18-
import { setElementText } from '../src/dom/prop'
25+
import { setElementText, setText } from '../src/dom/prop'
1926

2027
const define = makeRender<any>()
2128

@@ -562,7 +569,7 @@ describe('component: slots', () => {
562569
expect(html()).toBe('fallback<!--if--><!--slot-->')
563570
})
564571

565-
test('render fallback with nested v-if ', async () => {
572+
test('render fallback with nested v-if', async () => {
566573
const Child = {
567574
setup() {
568575
return createSlot('default', null, () =>
@@ -620,5 +627,101 @@ describe('component: slots', () => {
620627
await nextTick()
621628
expect(html()).toBe('content<!--if--><!--if--><!--slot-->')
622629
})
630+
631+
test('render fallback with v-for', async () => {
632+
const Child = {
633+
setup() {
634+
return createSlot('default', null, () =>
635+
document.createTextNode('fallback'),
636+
)
637+
},
638+
}
639+
640+
const items = ref<number[]>([1])
641+
const { html } = define({
642+
setup() {
643+
return createComponent(Child, null, {
644+
default: () => {
645+
const n2 = createFor(
646+
() => items.value,
647+
for_item0 => {
648+
const n4 = template('<span> </span>')() as any
649+
const x4 = child(n4) as any
650+
renderEffect(() =>
651+
setText(x4, toDisplayString(for_item0.value)),
652+
)
653+
return n4
654+
},
655+
)
656+
return n2
657+
},
658+
})
659+
},
660+
}).render()
661+
662+
expect(html()).toBe('<span>1</span><!--for--><!--slot-->')
663+
664+
items.value.pop()
665+
await nextTick()
666+
expect(html()).toBe('fallback<!--for--><!--slot-->')
667+
668+
items.value.pop()
669+
await nextTick()
670+
expect(html()).toBe('fallback<!--for--><!--slot-->')
671+
672+
items.value.push(2)
673+
await nextTick()
674+
expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
675+
})
676+
677+
test('render fallback with v-for (empty source)', async () => {
678+
const Child = {
679+
setup() {
680+
return createSlot('default', null, () =>
681+
document.createTextNode('fallback'),
682+
)
683+
},
684+
}
685+
686+
const items = ref<number[]>([])
687+
const { html } = define({
688+
setup() {
689+
return createComponent(Child, null, {
690+
default: () => {
691+
const n2 = createFor(
692+
() => items.value,
693+
for_item0 => {
694+
const n4 = template('<span> </span>')() as any
695+
const x4 = child(n4) as any
696+
renderEffect(() =>
697+
setText(x4, toDisplayString(for_item0.value)),
698+
)
699+
return n4
700+
},
701+
)
702+
return n2
703+
},
704+
})
705+
},
706+
}).render()
707+
708+
expect(html()).toBe('fallback<!--for--><!--slot-->')
709+
710+
items.value.push(1)
711+
await nextTick()
712+
expect(html()).toBe('<span>1</span><!--for--><!--slot-->')
713+
714+
items.value.pop()
715+
await nextTick()
716+
expect(html()).toBe('fallback<!--for--><!--slot-->')
717+
718+
items.value.pop()
719+
await nextTick()
720+
expect(html()).toBe('fallback<!--for--><!--slot-->')
721+
722+
items.value.push(2)
723+
await nextTick()
724+
expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
725+
})
623726
})
624727
})

packages/runtime-vapor/src/apiCreateFor.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import { isArray, isObject, isString } from '@vue/shared'
1515
import { createComment, createTextNode } from './dom/node'
1616
import {
1717
type Block,
18+
ForFragment,
1819
VaporFragment,
1920
insert,
21+
remove,
2022
remove as removeBlock,
2123
} from './block'
2224
import { warn } from '@vue/runtime-dom'
@@ -77,7 +79,7 @@ export const createFor = (
7779
setup?: (_: {
7880
createSelector: (source: () => any) => (cb: () => void) => void
7981
}) => void,
80-
): VaporFragment => {
82+
): ForFragment => {
8183
const _insertionParent = insertionParent
8284
const _insertionAnchor = insertionAnchor
8385
if (isHydrating) {
@@ -94,7 +96,7 @@ export const createFor = (
9496
let currentKey: any
9597
// TODO handle this in hydration
9698
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
97-
const frag = new VaporFragment(oldBlocks)
99+
const frag = new ForFragment(oldBlocks)
98100
const instance = currentInstance!
99101
const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE)
100102
const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT)
@@ -112,6 +114,7 @@ export const createFor = (
112114
const newLength = source.values.length
113115
const oldLength = oldBlocks.length
114116
newBlocks = new Array(newLength)
117+
let isFallback = false
115118

116119
const prevSub = setActiveSub()
117120

@@ -123,6 +126,11 @@ export const createFor = (
123126
} else {
124127
parent = parent || parentAnchor!.parentNode
125128
if (!oldLength) {
129+
// remove fallback nodes
130+
if (frag.fallback && (frag.nodes[0] as Block[]).length > 0) {
131+
remove(frag.nodes[0], parent!)
132+
}
133+
126134
// fast path for all new
127135
for (let i = 0; i < newLength; i++) {
128136
mount(source, i)
@@ -140,6 +148,13 @@ export const createFor = (
140148
parent!.textContent = ''
141149
parent!.appendChild(parentAnchor)
142150
}
151+
152+
// render fallback nodes
153+
if (frag.fallback) {
154+
insert((frag.nodes[0] = frag.fallback()), parent!, parentAnchor)
155+
oldBlocks = []
156+
isFallback = true
157+
}
143158
} else if (!getKey) {
144159
// unkeyed fast path
145160
const commonLength = Math.min(newLength, oldLength)
@@ -324,11 +339,12 @@ export const createFor = (
324339
}
325340
}
326341

327-
frag.nodes = [(oldBlocks = newBlocks)]
328-
if (parentAnchor) {
329-
frag.nodes.push(parentAnchor)
342+
if (!isFallback) {
343+
frag.nodes = [(oldBlocks = newBlocks)]
344+
if (parentAnchor) {
345+
frag.nodes.push(parentAnchor)
346+
}
330347
}
331-
332348
setActiveSub(prevSub)
333349
}
334350

packages/runtime-vapor/src/block.ts

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,24 @@ export type Block =
1818

1919
export type BlockFn = (...args: any[]) => Block
2020

21-
export class VaporFragment {
22-
nodes: Block
21+
export class VaporFragment<T extends Block = Block> {
22+
nodes: T
2323
anchor?: Node
2424
insert?: (parent: ParentNode, anchor: Node | null) => void
2525
remove?: (parent?: ParentNode) => void
26+
fallback?: BlockFn
2627

27-
constructor(nodes: Block) {
28+
constructor(nodes: T) {
2829
this.nodes = nodes
2930
}
3031
}
3132

33+
export class ForFragment extends VaporFragment<Block[]> {
34+
constructor(nodes: Block[]) {
35+
super(nodes)
36+
}
37+
}
38+
3239
export class DynamicFragment extends VaporFragment {
3340
anchor: Node
3441
scope: EffectScope | undefined
@@ -65,36 +72,41 @@ export class DynamicFragment extends VaporFragment {
6572
this.nodes = []
6673
}
6774

68-
if (this.fallback && !isValidBlock(this.nodes)) {
75+
if (this.fallback) {
6976
parent && remove(this.nodes, parent)
70-
// handle nested dynamic fragment
71-
if (isFragment(this.nodes)) {
72-
renderFallback(this.nodes, this.fallback, key)
73-
} else {
74-
this.nodes =
75-
(this.scope || (this.scope = new EffectScope())).run(this.fallback) ||
76-
[]
77-
}
77+
const scope = this.scope || (this.scope = new EffectScope())
78+
scope.run(() => {
79+
// handle nested fragment
80+
if (isFragment(this.nodes)) {
81+
ensureFallback(this.nodes, this.fallback!)
82+
} else if (!isValidBlock(this.nodes)) {
83+
this.nodes = this.fallback!() || []
84+
}
85+
})
86+
7887
parent && insert(this.nodes, parent, this.anchor)
7988
}
8089

8190
setActiveSub(prevSub)
8291
}
8392
}
8493

85-
function renderFallback(
86-
fragment: VaporFragment,
87-
fallback: BlockFn,
88-
key: any,
89-
): void {
94+
function ensureFallback(fragment: VaporFragment, fallback: BlockFn): void {
95+
if (!fragment.fallback) fragment.fallback = fallback
96+
9097
if (fragment instanceof DynamicFragment) {
9198
const nodes = fragment.nodes
9299
if (isFragment(nodes)) {
93-
renderFallback(nodes, fallback, key)
94-
} else {
95-
if (!fragment.fallback) fragment.fallback = fallback
96-
fragment.update(fragment.fallback, key)
100+
ensureFallback(nodes, fallback)
101+
} else if (!isValidBlock(nodes)) {
102+
fragment.update(fragment.fallback)
97103
}
104+
} else if (fragment instanceof ForFragment) {
105+
if (!isValidBlock(fragment.nodes[0])) {
106+
fragment.nodes[0] = [fallback() || []] as Block[]
107+
}
108+
} else {
109+
// vdom slots
98110
}
99111
}
100112

@@ -117,7 +129,7 @@ export function isValidBlock(block: Block): boolean {
117129
} else if (isVaporComponent(block)) {
118130
return isValidBlock(block.block)
119131
} else if (isArray(block)) {
120-
return block.length > 0 && block.every(isValidBlock)
132+
return block.length > 0 && block.some(isValidBlock)
121133
} else {
122134
// fragment
123135
return isValidBlock(block.nodes)

0 commit comments

Comments
 (0)