mirror of https://github.com/vuejs/core.git
fix(runtime-vapor): switch to fallback when slot is empty
This commit is contained in:
parent
7f3ca46523
commit
c223eb2684
|
@ -365,7 +365,7 @@ describe('component: slots', () => {
|
||||||
describe('createSlot', () => {
|
describe('createSlot', () => {
|
||||||
test('slot should be render correctly', () => {
|
test('slot should be render correctly', () => {
|
||||||
const Comp = defineComponent(() => {
|
const Comp = defineComponent(() => {
|
||||||
const n0 = template('<div></div>')()
|
const n0 = template('<div>')()
|
||||||
insert(createSlot('header'), n0 as any as ParentNode)
|
insert(createSlot('header'), n0 as any as ParentNode)
|
||||||
return n0
|
return n0
|
||||||
})
|
})
|
||||||
|
@ -589,7 +589,7 @@ describe('component: slots', () => {
|
||||||
return createComponent(Comp, {}, {})
|
return createComponent(Comp, {}, {})
|
||||||
}).render()
|
}).render()
|
||||||
|
|
||||||
expect(host.innerHTML).toBe('<div>fallback</div>')
|
expect(host.innerHTML).toBe('<div>fallback<!--slot--></div>')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dynamic slot should be updated correctly', async () => {
|
test('dynamic slot should be updated correctly', async () => {
|
||||||
|
@ -638,7 +638,7 @@ describe('component: slots', () => {
|
||||||
const slotOutletName = ref('one')
|
const slotOutletName = ref('one')
|
||||||
|
|
||||||
const Child = defineComponent(() => {
|
const Child = defineComponent(() => {
|
||||||
const temp0 = template('<p></p>')
|
const temp0 = template('<p>')
|
||||||
const el0 = temp0()
|
const el0 = temp0()
|
||||||
const slot1 = createSlot(
|
const slot1 = createSlot(
|
||||||
() => slotOutletName.value,
|
() => slotOutletName.value,
|
||||||
|
@ -672,5 +672,20 @@ describe('component: slots', () => {
|
||||||
|
|
||||||
expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
|
expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('non-exist slot', async () => {
|
||||||
|
const Child = defineComponent(() => {
|
||||||
|
const el0 = template('<p>')()
|
||||||
|
const slot = createSlot('not-exist', undefined)
|
||||||
|
insert(slot, el0 as any as ParentNode)
|
||||||
|
return el0
|
||||||
|
})
|
||||||
|
|
||||||
|
const { host } = define(() => {
|
||||||
|
return createComponent(Child)
|
||||||
|
}).render()
|
||||||
|
|
||||||
|
expect(host.innerHTML).toBe('<p></p>')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { renderEffect } from './renderEffect'
|
import { renderEffect } from './renderEffect'
|
||||||
import { type Block, type Fragment, fragmentKey } from './apiRender'
|
import { type Block, type Fragment, fragmentKey } from './apiRender'
|
||||||
import { type EffectScope, effectScope } from '@vue/reactivity'
|
import { type EffectScope, effectScope, shallowReactive } from '@vue/reactivity'
|
||||||
import { createComment, createTextNode, insert, remove } from './dom/element'
|
import { createComment, createTextNode, insert, remove } from './dom/element'
|
||||||
|
|
||||||
type BlockFn = () => Block
|
type BlockFn = () => Block
|
||||||
|
@ -16,15 +16,14 @@ export const createIf = (
|
||||||
let newValue: any
|
let newValue: any
|
||||||
let oldValue: any
|
let oldValue: any
|
||||||
let branch: BlockFn | undefined
|
let branch: BlockFn | undefined
|
||||||
let parent: ParentNode | undefined | null
|
|
||||||
let block: Block | undefined
|
let block: Block | undefined
|
||||||
let scope: EffectScope | undefined
|
let scope: EffectScope | undefined
|
||||||
const anchor = __DEV__ ? createComment('if') : createTextNode()
|
const anchor = __DEV__ ? createComment('if') : createTextNode()
|
||||||
const fragment: Fragment = {
|
const fragment: Fragment = shallowReactive({
|
||||||
nodes: [],
|
nodes: [],
|
||||||
anchor,
|
anchor,
|
||||||
[fragmentKey]: true,
|
[fragmentKey]: true,
|
||||||
}
|
})
|
||||||
|
|
||||||
// TODO: SSR
|
// TODO: SSR
|
||||||
// if (isHydrating) {
|
// if (isHydrating) {
|
||||||
|
@ -47,7 +46,7 @@ export const createIf = (
|
||||||
|
|
||||||
function doIf() {
|
function doIf() {
|
||||||
if ((newValue = !!condition()) !== oldValue) {
|
if ((newValue = !!condition()) !== oldValue) {
|
||||||
parent ||= anchor.parentNode
|
const parent = anchor.parentNode
|
||||||
if (block) {
|
if (block) {
|
||||||
scope!.stop()
|
scope!.stop()
|
||||||
remove(block, parent!)
|
remove(block, parent!)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
effectScope,
|
effectScope,
|
||||||
isReactive,
|
isReactive,
|
||||||
shallowReactive,
|
shallowReactive,
|
||||||
|
shallowRef,
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import {
|
import {
|
||||||
type ComponentInternalInstance,
|
type ComponentInternalInstance,
|
||||||
|
@ -12,7 +13,13 @@ import {
|
||||||
} from './component'
|
} from './component'
|
||||||
import { type Block, type Fragment, fragmentKey } from './apiRender'
|
import { type Block, type Fragment, fragmentKey } from './apiRender'
|
||||||
import { firstEffect, renderEffect } from './renderEffect'
|
import { firstEffect, renderEffect } from './renderEffect'
|
||||||
import { createComment, createTextNode, insert, remove } from './dom/element'
|
import {
|
||||||
|
createComment,
|
||||||
|
createTextNode,
|
||||||
|
insert,
|
||||||
|
normalizeBlock,
|
||||||
|
remove,
|
||||||
|
} from './dom/element'
|
||||||
import type { NormalizedRawProps } from './componentProps'
|
import type { NormalizedRawProps } from './componentProps'
|
||||||
import type { Data } from '@vue/runtime-shared'
|
import type { Data } from '@vue/runtime-shared'
|
||||||
import { mergeProps } from './dom/prop'
|
import { mergeProps } from './dom/prop'
|
||||||
|
@ -107,27 +114,30 @@ export function initSlots(
|
||||||
export function createSlot(
|
export function createSlot(
|
||||||
name: string | (() => string),
|
name: string | (() => string),
|
||||||
binds?: NormalizedRawProps,
|
binds?: NormalizedRawProps,
|
||||||
fallback?: () => Block,
|
fallback?: Slot,
|
||||||
): Block {
|
): Block {
|
||||||
let block: Block | undefined
|
const { slots } = currentInstance!
|
||||||
let branch: Slot | undefined
|
|
||||||
let oldBranch: Slot | undefined
|
|
||||||
let parent: ParentNode | undefined | null
|
|
||||||
let scope: EffectScope | undefined
|
|
||||||
const isDynamicName = isFunction(name)
|
|
||||||
const instance = currentInstance!
|
|
||||||
const { slots } = instance
|
|
||||||
|
|
||||||
// When not using dynamic slots, simplify the process to improve performance
|
const slotBlock = shallowRef<Block>()
|
||||||
if (!isDynamicName && !isReactive(slots)) {
|
let slotBranch: Slot | undefined
|
||||||
if ((branch = withProps(slots[name]) || fallback)) {
|
let slotScope: EffectScope | undefined
|
||||||
return branch(binds)
|
|
||||||
|
let fallbackBlock: Block | undefined
|
||||||
|
let fallbackBranch: Slot | undefined
|
||||||
|
let fallbackScope: EffectScope | undefined
|
||||||
|
|
||||||
|
const normalizeBinds = binds && normalizeSlotProps(binds)
|
||||||
|
|
||||||
|
const isDynamicName = isFunction(name)
|
||||||
|
// fast path for static slots & without fallback
|
||||||
|
if (!isDynamicName && !isReactive(slots) && !fallback) {
|
||||||
|
if ((slotBranch = slots[name])) {
|
||||||
|
return slotBranch(normalizeBinds)
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSlot = isDynamicName ? () => slots[name()] : () => slots[name]
|
|
||||||
const anchor = __DEV__ ? createComment('slot') : createTextNode()
|
const anchor = __DEV__ ? createComment('slot') : createTextNode()
|
||||||
const fragment: Fragment = {
|
const fragment: Fragment = {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
|
@ -137,29 +147,76 @@ export function createSlot(
|
||||||
|
|
||||||
// TODO lifecycle hooks
|
// TODO lifecycle hooks
|
||||||
renderEffect(() => {
|
renderEffect(() => {
|
||||||
if ((branch = withProps(getSlot()) || fallback) !== oldBranch) {
|
const parent = anchor.parentNode
|
||||||
parent ||= anchor.parentNode
|
|
||||||
if (block) {
|
if (
|
||||||
scope!.stop()
|
!slotBlock.value || // not initied
|
||||||
remove(block, parent!)
|
fallbackScope || // in fallback slot
|
||||||
}
|
isValidBlock(slotBlock.value) // slot block is valid
|
||||||
if ((oldBranch = branch)) {
|
) {
|
||||||
scope = effectScope()
|
renderSlot(parent)
|
||||||
fragment.nodes = block = scope.run(() => branch!(binds))!
|
|
||||||
parent && insert(block, parent, anchor)
|
|
||||||
} else {
|
} else {
|
||||||
scope = block = undefined
|
renderFallback(parent)
|
||||||
fragment.nodes = []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return fragment
|
return fragment
|
||||||
|
|
||||||
function withProps<T extends (p: any) => any>(fn?: T) {
|
function renderSlot(parent: ParentNode | null) {
|
||||||
if (fn)
|
// from fallback to slot
|
||||||
return (binds?: NormalizedRawProps): ReturnType<T> =>
|
const fromFallback = fallbackScope
|
||||||
fn(binds && normalizeSlotProps(binds))
|
if (fromFallback) {
|
||||||
|
// clean fallback slot
|
||||||
|
fallbackScope!.stop()
|
||||||
|
remove(fallbackBlock!, parent!)
|
||||||
|
fallbackScope = fallbackBlock = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotName = isFunction(name) ? name() : name
|
||||||
|
const branch = slots[slotName]!
|
||||||
|
|
||||||
|
if (branch) {
|
||||||
|
// init slot scope and block or switch branch
|
||||||
|
if (!slotScope || slotBranch !== branch) {
|
||||||
|
// clean previous slot
|
||||||
|
if (slotScope && !fromFallback) {
|
||||||
|
slotScope.stop()
|
||||||
|
remove(slotBlock.value!, parent!)
|
||||||
|
}
|
||||||
|
|
||||||
|
slotBranch = branch
|
||||||
|
slotScope = effectScope()
|
||||||
|
slotBlock.value = slotScope.run(() => slotBranch!(normalizeBinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
// if slot block is valid, render it
|
||||||
|
if (slotBlock.value && isValidBlock(slotBlock.value)) {
|
||||||
|
fragment.nodes = slotBlock.value
|
||||||
|
parent && insert(slotBlock.value, parent, anchor)
|
||||||
|
} else {
|
||||||
|
renderFallback(parent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
renderFallback(parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFallback(parent: ParentNode | null) {
|
||||||
|
// if slot branch is initied, remove it from DOM, but keep the scope
|
||||||
|
if (slotBranch) {
|
||||||
|
remove(slotBlock.value!, parent!)
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackBranch ||= fallback
|
||||||
|
if (fallbackBranch) {
|
||||||
|
fallbackScope = effectScope()
|
||||||
|
fragment.nodes = fallbackBlock = fallbackScope.run(() =>
|
||||||
|
fallbackBranch!(normalizeBinds),
|
||||||
|
)!
|
||||||
|
parent && insert(fallbackBlock, parent, anchor)
|
||||||
|
} else {
|
||||||
|
fragment.nodes = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,3 +271,9 @@ function normalizeSlotProps(rawPropsList: NormalizedRawProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValidBlock(block: Block) {
|
||||||
|
return (
|
||||||
|
normalizeBlock(block).filter(node => !(node instanceof Comment)).length > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { createVaporApp } from 'vue/vapor'
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
const modules = import.meta.glob<any>('./**/*.(vue|js)')
|
const modules = import.meta.glob<any>('./**/*.(vue|js|ts)')
|
||||||
const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
|
const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
|
||||||
|
|
||||||
mod.then(({ default: mod }) => {
|
mod.then(({ default: mod }) => {
|
||||||
|
|
Loading…
Reference in New Issue