mirror of https://github.com/vuejs/core.git
wip: render fallback nodes for vfor
This commit is contained in:
parent
e28b96bea8
commit
a65da3aee9
|
@ -1,7 +1,9 @@
|
||||||
// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.
|
// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
child,
|
||||||
createComponent,
|
createComponent,
|
||||||
|
createFor,
|
||||||
createForSlots,
|
createForSlots,
|
||||||
createIf,
|
createIf,
|
||||||
createSlot,
|
createSlot,
|
||||||
|
@ -12,10 +14,15 @@ import {
|
||||||
renderEffect,
|
renderEffect,
|
||||||
template,
|
template,
|
||||||
} from '../src'
|
} from '../src'
|
||||||
import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
|
import {
|
||||||
|
currentInstance,
|
||||||
|
nextTick,
|
||||||
|
ref,
|
||||||
|
toDisplayString,
|
||||||
|
} from '@vue/runtime-dom'
|
||||||
import { makeRender } from './_utils'
|
import { makeRender } from './_utils'
|
||||||
import type { DynamicSlot } from '../src/componentSlots'
|
import type { DynamicSlot } from '../src/componentSlots'
|
||||||
import { setElementText } from '../src/dom/prop'
|
import { setElementText, setText } from '../src/dom/prop'
|
||||||
|
|
||||||
const define = makeRender<any>()
|
const define = makeRender<any>()
|
||||||
|
|
||||||
|
@ -562,7 +569,7 @@ describe('component: slots', () => {
|
||||||
expect(html()).toBe('fallback<!--if--><!--slot-->')
|
expect(html()).toBe('fallback<!--if--><!--slot-->')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('render fallback with nested v-if ', async () => {
|
test('render fallback with nested v-if', async () => {
|
||||||
const Child = {
|
const Child = {
|
||||||
setup() {
|
setup() {
|
||||||
return createSlot('default', null, () =>
|
return createSlot('default', null, () =>
|
||||||
|
@ -620,5 +627,101 @@ describe('component: slots', () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(html()).toBe('content<!--if--><!--if--><!--slot-->')
|
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-->')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,8 +15,10 @@ import { isArray, isObject, isString } from '@vue/shared'
|
||||||
import { createComment, createTextNode } from './dom/node'
|
import { createComment, createTextNode } from './dom/node'
|
||||||
import {
|
import {
|
||||||
type Block,
|
type Block,
|
||||||
|
ForFragment,
|
||||||
VaporFragment,
|
VaporFragment,
|
||||||
insert,
|
insert,
|
||||||
|
remove,
|
||||||
remove as removeBlock,
|
remove as removeBlock,
|
||||||
} from './block'
|
} from './block'
|
||||||
import { warn } from '@vue/runtime-dom'
|
import { warn } from '@vue/runtime-dom'
|
||||||
|
@ -77,7 +79,7 @@ export const createFor = (
|
||||||
setup?: (_: {
|
setup?: (_: {
|
||||||
createSelector: (source: () => any) => (cb: () => void) => void
|
createSelector: (source: () => any) => (cb: () => void) => void
|
||||||
}) => void,
|
}) => void,
|
||||||
): VaporFragment => {
|
): ForFragment => {
|
||||||
const _insertionParent = insertionParent
|
const _insertionParent = insertionParent
|
||||||
const _insertionAnchor = insertionAnchor
|
const _insertionAnchor = insertionAnchor
|
||||||
if (isHydrating) {
|
if (isHydrating) {
|
||||||
|
@ -94,7 +96,7 @@ export const createFor = (
|
||||||
let currentKey: any
|
let currentKey: any
|
||||||
// TODO handle this in hydration
|
// TODO handle this in hydration
|
||||||
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
|
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
|
||||||
const frag = new VaporFragment(oldBlocks)
|
const frag = new ForFragment(oldBlocks)
|
||||||
const instance = currentInstance!
|
const instance = currentInstance!
|
||||||
const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE)
|
const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE)
|
||||||
const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT)
|
const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT)
|
||||||
|
@ -112,6 +114,7 @@ export const createFor = (
|
||||||
const newLength = source.values.length
|
const newLength = source.values.length
|
||||||
const oldLength = oldBlocks.length
|
const oldLength = oldBlocks.length
|
||||||
newBlocks = new Array(newLength)
|
newBlocks = new Array(newLength)
|
||||||
|
let isFallback = false
|
||||||
|
|
||||||
const prevSub = setActiveSub()
|
const prevSub = setActiveSub()
|
||||||
|
|
||||||
|
@ -123,6 +126,11 @@ export const createFor = (
|
||||||
} else {
|
} else {
|
||||||
parent = parent || parentAnchor!.parentNode
|
parent = parent || parentAnchor!.parentNode
|
||||||
if (!oldLength) {
|
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
|
// fast path for all new
|
||||||
for (let i = 0; i < newLength; i++) {
|
for (let i = 0; i < newLength; i++) {
|
||||||
mount(source, i)
|
mount(source, i)
|
||||||
|
@ -140,6 +148,13 @@ export const createFor = (
|
||||||
parent!.textContent = ''
|
parent!.textContent = ''
|
||||||
parent!.appendChild(parentAnchor)
|
parent!.appendChild(parentAnchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// render fallback nodes
|
||||||
|
if (frag.fallback) {
|
||||||
|
insert((frag.nodes[0] = frag.fallback()), parent!, parentAnchor)
|
||||||
|
oldBlocks = []
|
||||||
|
isFallback = true
|
||||||
|
}
|
||||||
} else if (!getKey) {
|
} else if (!getKey) {
|
||||||
// unkeyed fast path
|
// unkeyed fast path
|
||||||
const commonLength = Math.min(newLength, oldLength)
|
const commonLength = Math.min(newLength, oldLength)
|
||||||
|
@ -324,11 +339,12 @@ export const createFor = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
frag.nodes = [(oldBlocks = newBlocks)]
|
if (!isFallback) {
|
||||||
if (parentAnchor) {
|
frag.nodes = [(oldBlocks = newBlocks)]
|
||||||
frag.nodes.push(parentAnchor)
|
if (parentAnchor) {
|
||||||
|
frag.nodes.push(parentAnchor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveSub(prevSub)
|
setActiveSub(prevSub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,17 +18,24 @@ export type Block =
|
||||||
|
|
||||||
export type BlockFn = (...args: any[]) => Block
|
export type BlockFn = (...args: any[]) => Block
|
||||||
|
|
||||||
export class VaporFragment {
|
export class VaporFragment<T extends Block = Block> {
|
||||||
nodes: Block
|
nodes: T
|
||||||
anchor?: Node
|
anchor?: Node
|
||||||
insert?: (parent: ParentNode, anchor: Node | null) => void
|
insert?: (parent: ParentNode, anchor: Node | null) => void
|
||||||
remove?: (parent?: ParentNode) => void
|
remove?: (parent?: ParentNode) => void
|
||||||
|
fallback?: BlockFn
|
||||||
|
|
||||||
constructor(nodes: Block) {
|
constructor(nodes: T) {
|
||||||
this.nodes = nodes
|
this.nodes = nodes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ForFragment extends VaporFragment<Block[]> {
|
||||||
|
constructor(nodes: Block[]) {
|
||||||
|
super(nodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class DynamicFragment extends VaporFragment {
|
export class DynamicFragment extends VaporFragment {
|
||||||
anchor: Node
|
anchor: Node
|
||||||
scope: EffectScope | undefined
|
scope: EffectScope | undefined
|
||||||
|
@ -65,16 +72,18 @@ export class DynamicFragment extends VaporFragment {
|
||||||
this.nodes = []
|
this.nodes = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.fallback && !isValidBlock(this.nodes)) {
|
if (this.fallback) {
|
||||||
parent && remove(this.nodes, parent)
|
parent && remove(this.nodes, parent)
|
||||||
// handle nested dynamic fragment
|
const scope = this.scope || (this.scope = new EffectScope())
|
||||||
if (isFragment(this.nodes)) {
|
scope.run(() => {
|
||||||
renderFallback(this.nodes, this.fallback, key)
|
// handle nested fragment
|
||||||
} else {
|
if (isFragment(this.nodes)) {
|
||||||
this.nodes =
|
ensureFallback(this.nodes, this.fallback!)
|
||||||
(this.scope || (this.scope = new EffectScope())).run(this.fallback) ||
|
} else if (!isValidBlock(this.nodes)) {
|
||||||
[]
|
this.nodes = this.fallback!() || []
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
parent && insert(this.nodes, parent, this.anchor)
|
parent && insert(this.nodes, parent, this.anchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,19 +91,22 @@ export class DynamicFragment extends VaporFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFallback(
|
function ensureFallback(fragment: VaporFragment, fallback: BlockFn): void {
|
||||||
fragment: VaporFragment,
|
if (!fragment.fallback) fragment.fallback = fallback
|
||||||
fallback: BlockFn,
|
|
||||||
key: any,
|
|
||||||
): void {
|
|
||||||
if (fragment instanceof DynamicFragment) {
|
if (fragment instanceof DynamicFragment) {
|
||||||
const nodes = fragment.nodes
|
const nodes = fragment.nodes
|
||||||
if (isFragment(nodes)) {
|
if (isFragment(nodes)) {
|
||||||
renderFallback(nodes, fallback, key)
|
ensureFallback(nodes, fallback)
|
||||||
} else {
|
} else if (!isValidBlock(nodes)) {
|
||||||
if (!fragment.fallback) fragment.fallback = fallback
|
fragment.update(fragment.fallback)
|
||||||
fragment.update(fragment.fallback, key)
|
|
||||||
}
|
}
|
||||||
|
} else if (fragment instanceof ForFragment) {
|
||||||
|
if (!isValidBlock(fragment.nodes[0])) {
|
||||||
|
fragment.nodes[0] = [fallback() || []] as Block[]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// vdom slots
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +129,7 @@ export function isValidBlock(block: Block): boolean {
|
||||||
} else if (isVaporComponent(block)) {
|
} else if (isVaporComponent(block)) {
|
||||||
return isValidBlock(block.block)
|
return isValidBlock(block.block)
|
||||||
} else if (isArray(block)) {
|
} else if (isArray(block)) {
|
||||||
return block.length > 0 && block.every(isValidBlock)
|
return block.length > 0 && block.some(isValidBlock)
|
||||||
} else {
|
} else {
|
||||||
// fragment
|
// fragment
|
||||||
return isValidBlock(block.nodes)
|
return isValidBlock(block.nodes)
|
||||||
|
|
Loading…
Reference in New Issue