")
+
+export function render(_ctx) {
+ const n4 = _createAssetComponent("Comp", null, () => {
+ const n3 = t1()
+ _setInsertionState(n3, null, 0)
+ const n0 = _createIf(() => (_ctx.show), () => {
+ const n2 = t0()
+ return n2
+ })
+ return n3
+ }, true)
+ return n4
+}"
+`;
+
exports[`compiler: transform slot > dynamic slots name 1`] = `
"import { createAssetComponent as _createAssetComponent, template as _template } from 'vue';
const t0 = _template("foo", 2)
@@ -147,7 +166,7 @@ export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n2 = _createComponentWithFallback(_component_Comp, null, () => {
const n1 = _createComponentWithFallback(_component_Comp, null, () => {
- const n0 = _createSlot()
+ const n0 = _createSlot("default", null, null, 4)
return n0
})
return n1
@@ -161,7 +180,7 @@ exports[`compiler: transform slot > forwarded slots >
tag only 1`] = `
export function render(_ctx) {
const n1 = _createAssetComponent("Comp", null, () => {
- const n0 = _createSlot()
+ const n0 = _createSlot("default", null, null, 4)
return n0
}, true)
return n1
@@ -173,7 +192,7 @@ exports[`compiler: transform slot > forwarded slots > tag w/ template 1`]
export function render(_ctx) {
const n2 = _createAssetComponent("Comp", null, () => {
- const n0 = _createSlot()
+ const n0 = _createSlot("default", null, null, 4)
return n0
}, true)
return n2
@@ -186,9 +205,9 @@ exports[`compiler: transform slot > forwarded slots > tag w/ v-for 1`] =
export function render(_ctx) {
const n3 = _createAssetComponent("Comp", null, () => {
const n0 = _createFor(() => (_ctx.b), (_for_item0) => {
- const n2 = _createSlot()
+ const n2 = _createSlot("default", null, null, 4)
return n2
- }, undefined, 16)
+ }, undefined, 48)
return n0
}, true)
return n3
@@ -201,9 +220,9 @@ exports[`compiler: transform slot > forwarded slots > tag w/ v-if 1`] = `
export function render(_ctx) {
const n3 = _createAssetComponent("Comp", null, () => {
const n0 = _createIf(() => (_ctx.ok), () => {
- const n2 = _createSlot()
+ const n2 = _createSlot("default", null, null, 4)
return n2
- })
+ }, null, 129 /* BLOCK_SHAPE, SLOT_ROOT */)
return n0
}, true)
return n3
@@ -223,6 +242,41 @@ export function render(_ctx) {
}"
`;
+exports[`compiler: transform slot > marks root slot outlet fallback as slot root 1`] = `
+"import { createIf as _createIf, createSlot as _createSlot, createAssetComponent as _createAssetComponent, template as _template } from 'vue';
+const t0 = _template("", 2)
+
+export function render(_ctx) {
+ const n5 = _createAssetComponent("Comp", null, () => {
+ const n0 = _createSlot("default", null, () => {
+ const n2 = _createIf(() => (_ctx.show), () => {
+ const n4 = t0()
+ return n4
+ }, null, 129 /* BLOCK_SHAPE, SLOT_ROOT */)
+ return n2
+ }, 4)
+ return n0
+ }, true)
+ return n5
+}"
+`;
+
+exports[`compiler: transform slot > marks root v-if slot content as slot root 1`] = `
+"import { createIf as _createIf, createAssetComponent as _createAssetComponent, template as _template } from 'vue';
+const t0 = _template("", 2)
+
+export function render(_ctx) {
+ const n3 = _createAssetComponent("Comp", null, () => {
+ const n0 = _createIf(() => (_ctx.show), () => {
+ const n2 = t0()
+ return n2
+ }, null, 129 /* BLOCK_SHAPE, SLOT_ROOT */)
+ return n0
+ }, true)
+ return n3
+}"
+`;
+
exports[`compiler: transform slot > named slots w/ implicit default slot 1`] = `
"import { createAssetComponent as _createAssetComponent, template as _template } from 'vue';
const t0 = _template("foo", 2)
@@ -391,7 +445,7 @@ export function render(_ctx) {
() => (_createForSlots(_ctx.slots, (_, name) => ({
name: name,
fn: () => {
- const n0 = _createSlot(() => (name))
+ const n0 = _createSlot(() => (name), null, null, 4)
return n0
}
})))
@@ -413,7 +467,7 @@ export function render(_ctx) {
_setInsertionState(n3, null, 0)
const n2 = _createComponentWithFallback(_component_ChildComp)
return n3
- }, undefined, 8)
+ }, undefined, 40)
return n0
}, true)
return n5
@@ -432,7 +486,7 @@ export function render(_ctx) {
_setInsertionState(n3, null, 0)
const n2 = _createComponentWithFallback(_component_ChildComp)
return n3
- })
+ }, null, 129 /* BLOCK_SHAPE, SLOT_ROOT */)
return n0
}, true)
return n5
@@ -463,7 +517,7 @@ export function render(_ctx) {
_setInsertionState(n3, null, 0)
const n2 = _createPlainElement("my-element")
return n3
- })
+ }, null, 129 /* BLOCK_SHAPE, SLOT_ROOT */)
return n0
}, true)
return n5
@@ -500,7 +554,7 @@ export function render(_ctx) {
return n5
})
return n6
- })
+ }, null, 129 /* BLOCK_SHAPE, SLOT_ROOT */)
return n0
}, true)
return n8
@@ -539,7 +593,7 @@ exports[`compiler: transform slot > slot owner context > slot with slot outlet s
export function render(_ctx) {
const n2 = _createAssetComponent("Comp", null, () => {
- const n0 = _createSlot()
+ const n0 = _createSlot("default", null, null, 4)
return n0
}, true)
return n2
@@ -557,7 +611,7 @@ export function render(_ctx) {
const x2 = _txt(n2)
_renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value)))
return n2
- }, undefined, 8)
+ }, undefined, 40)
const x0 = _txt(n0)
_renderEffect(() => _setText(x0, _toDisplayString(_ctx.item)))
return n0
@@ -579,7 +633,7 @@ export function render(_ctx) {
}, () => {
const n4 = t1()
return n4
- }, 133 /* BLOCK_SHAPE, INDEX_SHIFT */)
+ }, 389 /* BLOCK_SHAPE, SLOT_ROOT, INDEX_SHIFT */)
return n0
}, true)
return n7
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap
index 80068f205b3..6c765bd657e 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap
@@ -48,7 +48,7 @@ exports[`v-text > work with dynamic component 1`] = `
"import { createDynamicComponent as _createDynamicComponent, toDisplayString as _toDisplayString, setBlockText as _setBlockText, renderEffect as _renderEffect } from 'vue';
export function render(_ctx) {
- const n0 = _createDynamicComponent(() => (_ctx.Comp), null, null, true)
+ const n0 = _createDynamicComponent(() => (_ctx.Comp), null, null, 1)
_renderEffect(() => _setBlockText(n0, _toDisplayString(_ctx.foo)))
return n0
}"
diff --git a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts
index ee5b4e382b0..8282c3e6b97 100644
--- a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts
@@ -7,6 +7,8 @@ import {
transformSlotOutlet,
transformText,
transformVBind,
+ transformVFor,
+ transformVIf,
transformVOn,
transformVShow,
} from '../../src'
@@ -15,6 +17,8 @@ import { makeCompile } from './_utils'
const compileWithSlotsOutlet = makeCompile({
nodeTransforms: [
transformText,
+ transformVIf,
+ transformVFor,
transformSlotOutlet,
transformElement,
transformChildren,
@@ -239,6 +243,36 @@ describe('compiler: transform outlets', () => {
})
})
+ test('root v-if fallback', () => {
+ const { code } = compileWithSlotsOutlet(``)
+
+ expect(code).toMatchSnapshot()
+ })
+
+ test('nested root v-for fallback', () => {
+ const { code } = compileWithSlotsOutlet(
+ `{{ item }}`,
+ )
+
+ expect(code).toMatchSnapshot()
+ })
+
+ test('does not mark non-root fallback v-if as slot root', () => {
+ const { code } = compileWithSlotsOutlet(
+ `
`,
+ )
+
+ expect(code).toMatchSnapshot()
+ })
+
+ test('root dynamic component fallback', () => {
+ const { code } = compileWithSlotsOutlet(
+ ``,
+ )
+
+ expect(code).toMatchSnapshot()
+ })
+
test('error on unexpected custom directive on ', () => {
const onError = vi.fn()
const source = ``
diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts
index f63c8b5fa5b..f7076b2766d 100644
--- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts
@@ -594,6 +594,28 @@ describe('compiler: transform slot', () => {
})
})
+ test('marks root v-if slot content as slot root', () => {
+ const { code } = compileWithSlots(``)
+
+ expect(code).toMatchSnapshot()
+ })
+
+ test('does not mark non-root v-if slot content as slot root', () => {
+ const { code } = compileWithSlots(
+ `
`,
+ )
+
+ expect(code).toMatchSnapshot()
+ })
+
+ test('marks root slot outlet fallback as slot root', () => {
+ const { code } = compileWithSlots(
+ ``,
+ )
+
+ expect(code).toMatchSnapshot()
+ })
+
describe('forwarded slots', () => {
test(' tag only', () => {
const { code } = compileWithSlots(``)
diff --git a/packages/compiler-vapor/src/generators/block.ts b/packages/compiler-vapor/src/generators/block.ts
index fd2879c0eaa..263a0987368 100644
--- a/packages/compiler-vapor/src/generators/block.ts
+++ b/packages/compiler-vapor/src/generators/block.ts
@@ -1,4 +1,12 @@
-import type { BlockIRNode, CoreHelper, IRDynamicInfo, IRSlots } from '../ir'
+import type {
+ BlockIRNode,
+ CoreHelper,
+ CreateComponentIRNode,
+ ForIRNode,
+ IRDynamicInfo,
+ IRSlots,
+ IfIRNode,
+} from '../ir'
import {
IRNodeTypes,
IRSlotType,
@@ -23,6 +31,7 @@ import {
} from './operation'
import { genChildren, genSelf } from './template'
import { toValidAssetId } from '@vue/compiler-dom'
+import { VaporSlotFlags } from '@vue/shared'
export function genBlock(
oper: BlockIRNode,
@@ -177,6 +186,71 @@ export function genBlockContent(
}
}
+export function markSlotRootOperations(block: BlockIRNode): void {
+ for (let i = 0; i < block.returns.length; i++) {
+ const child = findReturnedDynamic(block, block.returns[i])
+ const operation = child && child.operation
+ if (!operation) continue
+
+ if (operation.type === IRNodeTypes.IF) {
+ markSlotRootIf(operation)
+ } else if (operation.type === IRNodeTypes.FOR) {
+ markSlotRootFor(operation)
+ } else if (operation.type === IRNodeTypes.SLOT_OUTLET_NODE) {
+ markSlotRootSlotOutlet(operation)
+ } else if (operation.type === IRNodeTypes.CREATE_COMPONENT_NODE) {
+ markSlotRootComponent(operation)
+ }
+ }
+}
+
+function markSlotRootIf(operation: IfIRNode): void {
+ if (!operation.once) {
+ operation.slotRoot = true
+ }
+ markSlotRootOperations(operation.positive)
+
+ const negative = operation.negative
+ if (!negative) return
+ if (negative.type === IRNodeTypes.IF) {
+ markSlotRootIf(negative)
+ } else {
+ markSlotRootOperations(negative)
+ }
+}
+
+function markSlotRootFor(operation: ForIRNode): void {
+ if (!operation.once) {
+ operation.slotRoot = true
+ }
+ markSlotRootOperations(operation.render)
+}
+
+function markSlotRootSlotOutlet(
+ operation: Extract,
+): void {
+ operation.flags |= VaporSlotFlags.SLOT_ROOT
+ if (operation.fallback) {
+ markSlotRootOperations(operation.fallback)
+ }
+}
+
+function markSlotRootComponent(operation: CreateComponentIRNode): void {
+ if (!operation.once && operation.dynamic && !operation.dynamic.isStatic) {
+ operation.slotRoot = true
+ }
+}
+
+function findReturnedDynamic(
+ block: BlockIRNode,
+ id: number,
+): IRDynamicInfo | undefined {
+ for (let i = 0; i < block.dynamic.children.length; i++) {
+ const child = block.dynamic.children[i]
+ if (child.id === id) return child
+ }
+}
+
interface AssetComponentUsage {
count: number
root: boolean
diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts
index 901bb6caae0..23ac26739bd 100644
--- a/packages/compiler-vapor/src/generators/component.ts
+++ b/packages/compiler-vapor/src/generators/component.ts
@@ -1,4 +1,5 @@
import {
+ VaporDynamicComponentFlags,
camelize,
extend,
getModifierPropName,
@@ -42,7 +43,7 @@ import {
} from '@vue/compiler-dom'
import { genEventHandler } from './event'
import { genDirectiveModifiers, genDirectivesForElement } from './directive'
-import { genBlock } from './block'
+import { genBlock, markSlotRootOperations } from './block'
import {
type DestructureMap,
type DestructureMapValue,
@@ -69,7 +70,15 @@ export function genCreateComponent(
useAssetComponentHelper && operation.tag.endsWith('__self')
const tag = genTag()
- const { root, props, slots, once } = operation
+ const { root, props, slots, once, slotRoot } = operation
+ const isRuntimeDynamicComponent = !!(
+ operation.dynamic && !operation.dynamic.isStatic
+ )
+ const dynamicComponentFlags = isRuntimeDynamicComponent
+ ? (root ? VaporDynamicComponentFlags.SINGLE_ROOT : 0) |
+ (once ? VaporDynamicComponentFlags.ONCE : 0) |
+ (slotRoot ? VaporDynamicComponentFlags.SLOT_ROOT : 0)
+ : 0
const rawSlots = genRawSlots(slots, context)
const [ids, handlers] = processInlineHandlers(props, context)
const rawProps = context.withId(() => genRawProps(props, context, true), ids)
@@ -86,7 +95,7 @@ export function genCreateComponent(
...inlineHandlers,
`const n${operation.id} = `,
...genCall(
- operation.dynamic && !operation.dynamic.isStatic
+ isRuntimeDynamicComponent
? helper('createDynamicComponent')
: operation.useCreateElement
? helper('createPlainElement')
@@ -98,9 +107,15 @@ export function genCreateComponent(
tag,
rawProps,
rawSlots,
- root ? 'true' : false,
- once && 'true',
- maybeSelfReference && 'true',
+ isRuntimeDynamicComponent
+ ? dynamicComponentFlags
+ ? String(dynamicComponentFlags)
+ : false
+ : root
+ ? 'true'
+ : false,
+ isRuntimeDynamicComponent ? false : once && 'true',
+ isRuntimeDynamicComponent ? false : maybeSelfReference && 'true',
),
...genDirectivesForElement(operation.id, context),
]
@@ -734,6 +749,7 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
}
const exitSlotBlock = context.enterSlotBlock()
+ markSlotRootOperations(oper)
let blockFn = context.withId(
() => genBlock(oper, context, propsName ? [propsName] : []),
idMap,
diff --git a/packages/compiler-vapor/src/generators/for.ts b/packages/compiler-vapor/src/generators/for.ts
index 8b0588cec5c..8d69e5036eb 100644
--- a/packages/compiler-vapor/src/generators/for.ts
+++ b/packages/compiler-vapor/src/generators/for.ts
@@ -45,6 +45,7 @@ export function genFor(
id,
component,
onlyChild,
+ slotRoot,
} = oper
const rawValue = value && value.content
@@ -155,6 +156,9 @@ export function genFor(
if (once) {
flags |= VaporVForFlags.ONCE
}
+ if (slotRoot) {
+ flags |= VaporVForFlags.SLOT_ROOT
+ }
const onResetCalls: CodeFragment[] = []
for (let i = 0; i < selectorPatterns.length; i++) {
diff --git a/packages/compiler-vapor/src/generators/if.ts b/packages/compiler-vapor/src/generators/if.ts
index 70272ece737..9510e475981 100644
--- a/packages/compiler-vapor/src/generators/if.ts
+++ b/packages/compiler-vapor/src/generators/if.ts
@@ -11,9 +11,15 @@ export function genIf(
isNested = false,
): CodeFragment[] {
const { helper } = context
- const { condition, positive, negative, once, index, blockShape } = oper
+ const { condition, positive, negative, once, slotRoot, index, blockShape } =
+ oper
const [frag, push] = buildCodeFragment()
- const flags = genIfFlags(blockShape, once, negative ? index : undefined)
+ const flags = genIfFlags(
+ blockShape,
+ once,
+ slotRoot,
+ negative ? index : undefined,
+ )
const conditionExpr: CodeFragment[] = [
'() => (',
@@ -49,9 +55,13 @@ export function genIf(
function genIfFlags(
blockShape: number,
once: boolean | undefined,
+ slotRoot: boolean | undefined,
index: number | undefined,
): string | false {
let flags = blockShape
+ if (slotRoot) {
+ flags |= VaporIfFlags.SLOT_ROOT
+ }
if (once) {
flags |= VaporIfFlags.ONCE
} else if (index !== undefined) {
@@ -67,12 +77,13 @@ function genIfFlags(
}
return __DEV__
- ? `${flags} /* ${genIfFlagNames(once, index, blockShape)} */`
+ ? `${flags} /* ${genIfFlagNames(once, slotRoot, index, blockShape)} */`
: String(flags)
}
function genIfFlagNames(
once: boolean | undefined,
+ slotRoot: boolean | undefined,
index: number | undefined,
blockShape: number,
): string {
@@ -87,7 +98,11 @@ function genIfFlagNames(
if (once) {
names.push('ONCE')
- } else if (index !== undefined) {
+ }
+ if (slotRoot) {
+ names.push('SLOT_ROOT')
+ }
+ if (!once && index !== undefined) {
names.push('INDEX_SHIFT')
}
diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts
index 32557cb331e..3fdb5c9e981 100644
--- a/packages/compiler-vapor/src/generators/slotOutlet.ts
+++ b/packages/compiler-vapor/src/generators/slotOutlet.ts
@@ -1,6 +1,6 @@
import type { CodegenContext } from '../generate'
import type { SlotOutletIRNode } from '../ir'
-import { genBlock } from './block'
+import { genBlock, markSlotRootOperations } from './block'
import { genExpression } from './expression'
import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
import { genRawProps } from './component'
@@ -15,6 +15,7 @@ export function genSlotOutlet(
let fallbackArg: CodeFragment[] | undefined
if (fallback) {
+ markSlotRootOperations(fallback)
fallbackArg = genBlock(fallback, context)
}
const createSlot = helper('createSlot')
diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts
index e502ca8b556..78260c5ddf4 100644
--- a/packages/compiler-vapor/src/ir/index.ts
+++ b/packages/compiler-vapor/src/ir/index.ts
@@ -95,6 +95,7 @@ export interface IfIRNode extends BaseIRNode, EffectBoundary {
positive: BlockIRNode
negative?: BlockIRNode | IfIRNode
once?: boolean
+ slotRoot?: boolean
index?: number
parent?: number
anchor?: number
@@ -115,6 +116,7 @@ export interface ForIRNode extends BaseIRNode, IRFor, EffectBoundary {
keyProp?: SimpleExpressionNode
render: BlockIRNode
once: boolean
+ slotRoot?: boolean
component: boolean
onlyChild: boolean
parent?: number
@@ -235,6 +237,7 @@ export interface CreateComponentIRNode extends BaseIRNode, EffectBoundary {
asset: boolean
root: boolean
once: boolean
+ slotRoot?: boolean
dynamic?: SimpleExpressionNode
useCreateElement: boolean
parent?: number
diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts
index ddf2b165ec7..3a37323f0ae 100644
--- a/packages/runtime-core/src/apiCreateApp.ts
+++ b/packages/runtime-core/src/apiCreateApp.ts
@@ -252,6 +252,7 @@ export interface VaporInteropInterface {
parentComponent: any, // VaporComponentInstance
fallback?: any, // VaporSlot
once?: boolean,
+ slotRoot?: boolean,
) => any
vdomMountVNode: (
vnode: VNode,
diff --git a/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts b/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts
index e146ce6a67e..cd292cc523d 100644
--- a/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts
+++ b/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts
@@ -4,6 +4,7 @@ import {
nextTick,
resolveDynamicComponent,
} from '@vue/runtime-dom'
+import { VaporDynamicComponentFlags } from '@vue/shared'
import {
type VaporComponentInstance,
createComponent,
@@ -77,7 +78,13 @@ describe('api: createDynamicComponent', () => {
const { html } = define({
setup() {
- return createDynamicComponent(() => val.value, null, null, true, true)
+ return createDynamicComponent(
+ () => val.value,
+ null,
+ null,
+ VaporDynamicComponentFlags.SINGLE_ROOT |
+ VaporDynamicComponentFlags.ONCE,
+ )
},
}).render()
@@ -97,8 +104,8 @@ describe('api: createDynamicComponent', () => {
() => val.value,
{ id: () => id.value },
null,
- true,
- true,
+ VaporDynamicComponentFlags.SINGLE_ROOT |
+ VaporDynamicComponentFlags.ONCE,
)
},
}).render()
@@ -242,7 +249,12 @@ describe('api: createDynamicComponent', () => {
test('compiled static key on dynamic component fallback', () => {
const Comp = defineVaporComponent({
setup() {
- const n0 = createDynamicComponent(() => 'div', null, null, true)
+ const n0 = createDynamicComponent(
+ () => 'div',
+ null,
+ null,
+ VaporDynamicComponentFlags.SINGLE_ROOT,
+ )
setBlockKey(n0, 'foo')
return n0
},
diff --git a/packages/runtime-vapor/__tests__/componentAttrs.spec.ts b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts
index eab69de3499..5335597edf3 100644
--- a/packages/runtime-vapor/__tests__/componentAttrs.spec.ts
+++ b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts
@@ -22,7 +22,7 @@ import {
template,
} from '../src'
import { makeRender } from './_utils'
-import { stringifyStyle } from '@vue/shared'
+import { VaporDynamicComponentFlags, stringifyStyle } from '@vue/shared'
import { setElementText } from '../src/dom/prop'
const define = makeRender()
@@ -1120,7 +1120,7 @@ describe('attribute fallthrough', () => {
return n0
},
},
- true,
+ VaporDynamicComponentFlags.SINGLE_ROOT,
)
return n1
},
diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
index c6bfa553785..bbdd78cc765 100644
--- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts
+++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
@@ -4,6 +4,7 @@ import {
VaporTeleport,
child,
createComponent,
+ createDynamicComponent,
createFor,
createForSlots,
createIf,
@@ -34,8 +35,14 @@ import {
toDisplayString,
useSlots,
} from '@vue/runtime-dom'
-import { VaporSlotFlags } from '@vue/shared'
-import { makeRender } from './_utils'
+import {
+ VaporBlockShape,
+ VaporDynamicComponentFlags,
+ VaporIfFlags,
+ VaporSlotFlags,
+ VaporVForFlags,
+} from '@vue/shared'
+import { compile, makeRender } from './_utils'
import type { DynamicSlot } from '../src/componentSlots'
import { setElementText, setText } from '../src/dom/prop'
import { type Block, type BlockFn, isValidBlock } from '../src/block'
@@ -47,9 +54,8 @@ import {
} from '../src/dom/hydration'
import {
DynamicFragment,
- ForFragment,
type SlotBoundaryContext,
- type SlotFallbackOutlet,
+ type SlotFallbackState,
SlotFragment,
VaporFragment,
getCurrentSlotBoundary,
@@ -57,7 +63,6 @@ import {
isHydratingSlotFallbackActive,
markSlotFallbackDirty,
recheckSlotFallback,
- syncActiveSlotFallback,
trackSlotBoundaryDirtying,
withHydratingSlotBoundary,
withHydratingSlotFallbackActive,
@@ -65,6 +70,11 @@ import {
} from '../src/fragment'
const define = makeRender()
+const keyedIfShape =
+ VaporBlockShape.SINGLE_ROOT | (1 << VaporIfFlags.INDEX_SHIFT)
+const slotRootIfShape = VaporBlockShape.SINGLE_ROOT | VaporIfFlags.SLOT_ROOT
+const keyedSlotRootIfShape = keyedIfShape | VaporIfFlags.SLOT_ROOT
+const slotRootForFlags = VaporVForFlags.SLOT_ROOT
function renderWithSlots(slots: any): any {
let instance: any
@@ -87,22 +97,23 @@ function renderWithSlots(slots: any): any {
return instance
}
-function createTestSlotFallbackOutlet(options: {
+function createTestSlotFallbackState(options: {
fallback?: BlockFn
content?: Block
parentNode?: ParentNode | null
anchor?: Node | null
+ isBusy?: () => boolean
isContentValid?: () => boolean
isDisposed?: () => boolean
-}): SlotFallbackOutlet {
- let outlet!: SlotFallbackOutlet
+}): SlotFallbackState {
+ let state!: SlotFallbackState
const boundary: SlotBoundaryContext = {
parent: null,
getFallback: () => options.fallback,
run: fn => fn(),
- markDirty: () => markSlotFallbackDirty(outlet),
+ markDirty: () => markSlotFallbackDirty(state),
}
- outlet = {
+ state = {
boundary,
activeFallback: null,
pendingRecheck: false,
@@ -110,11 +121,14 @@ function createTestSlotFallbackOutlet(options: {
getContent: () => options.content || [],
getParentNode: () => options.parentNode || null,
getAnchor: () => options.anchor || null,
- isDisposed: options.isDisposed,
- isContentValid: options.isContentValid,
+ isBusy: options.isBusy || (() => false),
+ isDisposed: options.isDisposed || (() => false),
+ isContentValid:
+ options.isContentValid || (() => isValidBlock(options.content || [])),
+ syncNodes: () => {},
notifyFallbackValidityChange: vi.fn(),
}
- return outlet
+ return state
}
describe('component: slots', () => {
@@ -310,7 +324,7 @@ describe('component: slots', () => {
child.update(() => [])
- expect(container.innerHTML).toBe('outer fallback')
+ expect(container.innerHTML).toBe('outer fallback')
})
test('slot fragment local fallback ignores unrelated ancestor fallback refs', async () => {
@@ -340,52 +354,106 @@ describe('component: slots', () => {
expect(localFallback).toHaveBeenCalledTimes(1)
})
- test('slot fragment activates local fallback while preserving content carrier', () => {
- const container = document.createElement('div')
- const content = new DynamicFragment('if', false, false)
- const frag = new SlotFragment()
-
- frag.updateSlot(
- () => content,
- () => document.createTextNode('fallback'),
+ test('slot fragment switches local fallback and root v-if content without keeping the inactive anchor in DOM', async () => {
+ const show = ref(false)
+ const Child = defineVaporComponent(() =>
+ createSlot('default', null, () => document.createTextNode('fallback')),
)
- insert(frag, container)
+ const { host } = define(() =>
+ createComponent(Child, null, {
+ default: () =>
+ createIf(
+ () => show.value,
+ () => document.createTextNode('content'),
+ undefined,
+ keyedSlotRootIfShape,
+ ),
+ }),
+ ).render()
+
+ expect(host.innerHTML).toBe('fallback')
+
+ show.value = true
+ await nextTick()
- expect(container.innerHTML).toBe('fallback')
+ expect(host.innerHTML).toBe('content')
})
- test('slot fragment activates local fallback while preserving v-for carrier', () => {
- const container = document.createElement('div')
- const content = new ForFragment([[], document.createComment('for')])
- const frag = new SlotFragment()
+ test('slot fragment switches local fallback and root v-for content without keeping the inactive anchor in DOM', async () => {
+ const items = ref([])
+ const Child = defineVaporComponent(() =>
+ createSlot('default', null, () => document.createTextNode('fallback')),
+ )
+ const { host } = define(() =>
+ createComponent(Child, null, {
+ default: () =>
+ createFor(
+ () => items.value,
+ item => document.createTextNode(item.value),
+ undefined,
+ slotRootForFlags,
+ ),
+ }),
+ ).render()
- frag.updateSlot(
- () => content,
- () => document.createTextNode('fallback'),
+ expect(host.innerHTML).toBe('fallback')
+
+ items.value = ['content']
+ await nextTick()
+
+ expect(host.innerHTML).toBe('content')
+ })
+
+ test('slot fragment switches root dynamic component content to fallback', async () => {
+ const current = shallowRef(() => document.createTextNode('content'))
+ const Child = defineVaporComponent(() =>
+ createSlot('default', null, () => document.createTextNode('fallback')),
)
- insert(frag, container)
+ const { host } = define(() =>
+ createComponent(Child, null, {
+ default: () =>
+ createDynamicComponent(
+ () => current.value,
+ null,
+ null,
+ VaporDynamicComponentFlags.SINGLE_ROOT |
+ VaporDynamicComponentFlags.SLOT_ROOT,
+ ),
+ }),
+ ).render()
+
+ expect(host.innerHTML).toBe('content')
- expect(container.innerHTML).toBe('fallback')
+ current.value = null
+ await nextTick()
+
+ expect(host.innerHTML).toBe('fallback')
})
- test('slot fallback falls through while preserving local carrier', () => {
+ test('slot fallback falls through and restores local root v-if fallback without keeping inactive anchor in DOM', async () => {
const container = document.createElement('div')
const anchor = document.createComment('slot')
- const localCarrier = new DynamicFragment('if', false, false)
- let outlet!: SlotFallbackOutlet
+ const showLocal = ref(false)
+ let state!: SlotFallbackState
const parentBoundary: SlotBoundaryContext = {
parent: null,
- getFallback: () => () => document.createTextNode('outlet fallback'),
+ getFallback: () => () => document.createTextNode('parent fallback'),
run: fn => fn(),
markDirty: vi.fn(),
}
const boundary: SlotBoundaryContext = {
parent: parentBoundary,
- getFallback: () => () => localCarrier,
+ getFallback: () => () =>
+ createIf(
+ () => showLocal.value,
+ () => document.createTextNode('local fallback'),
+ undefined,
+ keyedSlotRootIfShape,
+ ),
run: fn => fn(),
- markDirty: () => markSlotFallbackDirty(outlet),
+ markDirty: () => markSlotFallbackDirty(state),
}
- outlet = {
+ state = {
boundary,
activeFallback: null,
pendingRecheck: false,
@@ -393,14 +461,22 @@ describe('component: slots', () => {
getContent: () => [],
getParentNode: () => container,
getAnchor: () => anchor,
+ isBusy: () => false,
+ isDisposed: () => false,
isContentValid: () => false,
+ syncNodes: () => {},
notifyFallbackValidityChange: vi.fn(),
}
container.append(anchor)
- recheckSlotFallback(outlet)
+ recheckSlotFallback(state)
- expect(container.innerHTML).toBe('outlet fallback')
+ expect(container.innerHTML).toBe('parent fallback')
+
+ showLocal.value = true
+ await nextTick()
+
+ expect(container.innerHTML).toBe('local fallback')
})
test('slot fallback falls through when local fallback is removed', () => {
@@ -408,10 +484,10 @@ describe('component: slots', () => {
const anchor = document.createComment('slot')
let localFallback: BlockFn | undefined = () =>
document.createTextNode('local fallback')
- let outlet!: SlotFallbackOutlet
+ let state!: SlotFallbackState
const parentBoundary: SlotBoundaryContext = {
parent: null,
- getFallback: () => () => document.createTextNode('outlet fallback'),
+ getFallback: () => () => document.createTextNode('parent fallback'),
run: fn => fn(),
markDirty: vi.fn(),
}
@@ -419,9 +495,9 @@ describe('component: slots', () => {
parent: parentBoundary,
getFallback: () => localFallback,
run: fn => fn(),
- markDirty: () => markSlotFallbackDirty(outlet),
+ markDirty: () => markSlotFallbackDirty(state),
}
- outlet = {
+ state = {
boundary,
activeFallback: null,
pendingRecheck: false,
@@ -429,17 +505,20 @@ describe('component: slots', () => {
getContent: () => [],
getParentNode: () => container,
getAnchor: () => anchor,
+ isBusy: () => false,
+ isDisposed: () => false,
isContentValid: () => false,
+ syncNodes: () => {},
notifyFallbackValidityChange: vi.fn(),
}
container.append(anchor)
- recheckSlotFallback(outlet)
+ recheckSlotFallback(state)
expect(container.innerHTML).toBe('local fallback')
localFallback = undefined
- recheckSlotFallback(outlet, true)
- expect(container.innerHTML).toBe('outlet fallback')
+ recheckSlotFallback(state, true)
+ expect(container.innerHTML).toBe('parent fallback')
})
test('slot fragment delays fallback activation until pending child validity resolves', () => {
@@ -467,16 +546,176 @@ describe('component: slots', () => {
expect(container.innerHTML).toBe('fallback')
})
- test('slot fallback outlet ignores dirty notifications after dispose', () => {
+ test('slot boundary dirtying ignores non-root v-if updates', async () => {
+ const markDirty = vi.fn()
+ const show = ref(true)
+ const boundary: SlotBoundaryContext = {
+ parent: null,
+ getFallback: () => undefined,
+ run: fn => fn(),
+ markDirty,
+ }
+ const Child = defineVaporComponent(() =>
+ withOwnedSlotBoundary(boundary, () =>
+ createIf(
+ () => show.value,
+ () => document.createTextNode('content'),
+ undefined,
+ keyedIfShape,
+ ),
+ ),
+ )
+
+ define(() => createComponent(Child)).render()
+ show.value = false
+ await nextTick()
+
+ expect(markDirty).not.toHaveBeenCalled()
+ })
+
+ test('slot boundary dirtying ignores non-root v-for updates', async () => {
+ const markDirty = vi.fn()
+ const items = ref([1])
+ const boundary: SlotBoundaryContext = {
+ parent: null,
+ getFallback: () => undefined,
+ run: fn => fn(),
+ markDirty,
+ }
+ const Child = defineVaporComponent(() =>
+ withOwnedSlotBoundary(boundary, () =>
+ createFor(
+ () => items.value,
+ item => document.createTextNode(String(item.value)),
+ ),
+ ),
+ )
+
+ define(() => createComponent(Child)).render()
+ items.value = []
+ await nextTick()
+
+ expect(markDirty).not.toHaveBeenCalled()
+ })
+
+ test('slot boundary dirtying tracks root v-if updates', async () => {
+ const markDirty = vi.fn()
+ const show = ref(true)
+ const boundary: SlotBoundaryContext = {
+ parent: null,
+ getFallback: () => undefined,
+ run: fn => fn(),
+ markDirty,
+ }
+ const Child = defineVaporComponent(() =>
+ withOwnedSlotBoundary(boundary, () =>
+ createIf(
+ () => show.value,
+ () => document.createTextNode('content'),
+ undefined,
+ keyedSlotRootIfShape,
+ ),
+ ),
+ )
+
+ define(() => createComponent(Child)).render()
+ show.value = false
+ await nextTick()
+
+ expect(markDirty).toHaveBeenCalledTimes(1)
+
+ show.value = true
+ await nextTick()
+
+ expect(markDirty).toHaveBeenCalledTimes(2)
+ })
+
+ test('slot boundary dirtying tracks root v-for updates', async () => {
+ const markDirty = vi.fn()
+ const items = ref([1])
+ const boundary: SlotBoundaryContext = {
+ parent: null,
+ getFallback: () => undefined,
+ run: fn => fn(),
+ markDirty,
+ }
+ const Child = defineVaporComponent(() =>
+ withOwnedSlotBoundary(boundary, () =>
+ createFor(
+ () => items.value,
+ item => document.createTextNode(String(item.value)),
+ undefined,
+ slotRootForFlags,
+ ),
+ ),
+ )
+
+ define(() => createComponent(Child)).render()
+ items.value = []
+ await nextTick()
+
+ expect(markDirty).toHaveBeenCalledTimes(1)
+
+ items.value = [1]
+ await nextTick()
+
+ expect(markDirty).toHaveBeenCalledTimes(2)
+ })
+
+ test('root vapor slot validity change dirties parent boundary', async () => {
+ const markDirty = vi.fn()
+ const show = ref(true)
+ const boundary: SlotBoundaryContext = {
+ parent: null,
+ getFallback: () => undefined,
+ run: fn => fn(),
+ markDirty,
+ }
+ const Child = defineVaporComponent(() =>
+ withOwnedSlotBoundary(boundary, () =>
+ createSlot('default', null, undefined, VaporSlotFlags.SLOT_ROOT),
+ ),
+ )
+
+ define(() =>
+ createComponent(Child, null, {
+ $: [
+ () =>
+ show.value
+ ? {
+ name: 'default',
+ fn: () => document.createTextNode('content'),
+ }
+ : {
+ name: 'default',
+ fn: () => [],
+ },
+ ],
+ }),
+ ).render()
+ markDirty.mockClear()
+
+ show.value = false
+ await nextTick()
+
+ expect(markDirty).toHaveBeenCalledTimes(1)
+
+ show.value = true
+ await nextTick()
+
+ expect(markDirty).toHaveBeenCalledTimes(2)
+ })
+
+ test('slot fallback state ignores dirty notifications after dispose', () => {
let disposed = false
- const outlet = createTestSlotFallbackOutlet({
+ const state = createTestSlotFallbackState({
isDisposed: () => disposed,
})
disposed = true
- outlet.boundary.markDirty()
+ state.boundary.markDirty()
- expect(outlet.pendingRecheck).toBe(false)
+ expect(state.pendingRecheck).toBe(false)
})
test('removed slot fragment ignores queued fallback dirty notifications', () => {
@@ -557,7 +796,7 @@ describe('component: slots', () => {
frag = new SlotFragment()
frag.forwarded = true
setCurrentHydrationNode(footer)
- frag.hydrate(true, true)
+ frag.hydrate(true)
})
})
} finally {
@@ -585,7 +824,7 @@ describe('component: slots', () => {
hydrateNode(start, () => {
withHydratingSlotBoundary(() => {
frag = new SlotFragment()
- frag.hydrate(true, true)
+ frag.hydrate(true)
})
})
} finally {
@@ -612,7 +851,7 @@ describe('component: slots', () => {
hydrateNode(start, () => {
withHydratingSlotBoundary(() => {
frag = new SlotFragment()
- frag.hydrate(true, true)
+ frag.hydrate(true)
})
})
} finally {
@@ -651,13 +890,13 @@ describe('component: slots', () => {
expect(frag.anchor).toBe(end.previousSibling)
})
- test('slot fallback outlet stops fallback scope when fallback body throws', async () => {
+ test('slot fallback state stops fallback scope when fallback body throws', async () => {
const source = ref(0)
const effectRuns = vi.fn()
const cleanup = vi.fn()
const err = new Error('fallback boom')
- const outlet = createTestSlotFallbackOutlet({
+ const state = createTestSlotFallbackState({
fallback: () => {
onScopeDispose(cleanup)
renderEffect(() => {
@@ -667,8 +906,8 @@ describe('component: slots', () => {
},
})
- expect(() => recheckSlotFallback(outlet)).toThrow(err)
- expect(outlet.activeFallback).toBe(null)
+ expect(() => recheckSlotFallback(state)).toThrow(err)
+ expect(state.activeFallback).toBe(null)
expect(cleanup).toHaveBeenCalledTimes(1)
expect(effectRuns).toHaveBeenCalledTimes(1)
@@ -678,96 +917,56 @@ describe('component: slots', () => {
expect(effectRuns).toHaveBeenCalledTimes(1)
})
- test('slot fallback outlet does not accumulate order-sync hooks', async () => {
- const fallback = new VaporFragment([
- document.createTextNode('a'),
- document.createTextNode('b'),
- ])
- const outlet = createTestSlotFallbackOutlet({
- fallback: () => fallback,
- })
-
- recheckSlotFallback(outlet)
- expect(fallback.onUpdated).toHaveLength(1)
-
- syncActiveSlotFallback(outlet)
- await nextTick()
-
- expect(fallback.onUpdated).toHaveLength(1)
- })
-
- test('slot fallback outlet re-syncs the whole carrier block order', async () => {
- const container = document.createElement('div')
- const carrierA = document.createTextNode('x')
- const carrierB = document.createTextNode('y')
- const marker = document.createTextNode('!')
- const slotAnchor = document.createComment('slot')
- const fallback = new VaporFragment([
- document.createTextNode('a'),
- document.createTextNode('b'),
- ])
- const outlet = createTestSlotFallbackOutlet({
+ test('slot fallback state rechecks when idle', () => {
+ const fallback = document.createTextNode('fallback')
+ const state = createTestSlotFallbackState({
fallback: () => fallback,
- content: [carrierA, carrierB],
- parentNode: container,
- anchor: slotAnchor,
- isContentValid: () => false,
})
- container.append(carrierA, marker, carrierB, slotAnchor)
- recheckSlotFallback(outlet)
-
- expect(container.innerHTML).toBe('abx!y')
-
- syncActiveSlotFallback(outlet)
- await nextTick()
+ state.boundary.markDirty()
- expect(container.innerHTML).toBe('abxy!')
+ expect(state.activeFallback).toBe(fallback)
})
- test('slot fallback outlet re-syncs carrier order when fallback ends with a fragment anchor', async () => {
- const container = document.createElement('div')
- const carrierA = document.createTextNode('x')
- const carrierB = document.createTextNode('y')
- const marker = document.createTextNode('!')
- const slotAnchor = document.createComment('slot')
- const trailingFragment = new DynamicFragment('if', false, false)
- trailingFragment.update(() => document.createTextNode('b'))
- const fallback = new VaporFragment([
- document.createTextNode('a'),
- trailingFragment,
- ])
- const outlet = createTestSlotFallbackOutlet({
- fallback: () => fallback,
- content: [carrierA, carrierB],
- parentNode: container,
- anchor: slotAnchor,
- isContentValid: () => false,
+ test('vdom slot does not dirty parent boundary when content stays valid', async () => {
+ const text = ref('A')
+ const boundary = {
+ parent: null,
+ getFallback: () => undefined,
+ run: (fn: () => any) => fn(),
+ markDirty: vi.fn(),
+ }
+ const instance = renderWithSlots({})
+ const app = createApp({ render: () => null })
+ app.use(vaporInteropPlugin)
+ const vapor = (app._context as any).vapor
+ const slotsRef = shallowRef({
+ default: () => [h('div', text.value)],
})
+ const frag = withOwnedSlotBoundary(boundary, () =>
+ vapor.vdomSlot(
+ slotsRef,
+ 'default',
+ {},
+ instance,
+ undefined,
+ false,
+ true,
+ ),
+ )
+ const host = document.createElement('div')
- container.append(carrierA, marker, carrierB, slotAnchor)
- recheckSlotFallback(outlet)
-
- expect(container.innerHTML).toBe('abx!y')
+ insert(frag, host)
+ boundary.markDirty.mockClear()
- syncActiveSlotFallback(outlet)
+ text.value = 'B'
await nextTick()
- expect(container.innerHTML).toBe('abxy!')
- })
-
- test('slot fallback outlet defaults to idle when isBusy is omitted', () => {
- const fallback = document.createTextNode('fallback')
- const outlet = createTestSlotFallbackOutlet({
- fallback: () => fallback,
- })
-
- outlet.boundary.markDirty()
-
- expect(outlet.activeFallback).toBe(fallback)
+ expect(host.innerHTML).toContain('B
')
+ expect(boundary.markDirty).not.toHaveBeenCalled()
})
- test('vdom slot dirties parent boundary once when content stays valid', async () => {
+ test('vdom slot ignores non-root updates inside slot boundary', async () => {
const text = ref('A')
const boundary = {
parent: null,
@@ -794,10 +993,10 @@ describe('component: slots', () => {
await nextTick()
expect(host.innerHTML).toContain('B
')
- expect(boundary.markDirty).toHaveBeenCalledTimes(1)
+ expect(boundary.markDirty).not.toHaveBeenCalled()
})
- test('vdom slot dirties parent boundary once when switching from valid content to local fallback', async () => {
+ test('vdom slot does not dirty parent boundary when local fallback keeps output valid', async () => {
const show = ref(true)
const boundary = {
parent: null,
@@ -813,8 +1012,14 @@ describe('component: slots', () => {
default: () => (show.value ? [h('div', 'content')] : []),
})
const frag = withOwnedSlotBoundary(boundary, () =>
- vapor.vdomSlot(slotsRef, 'default', {}, instance, () =>
- template('fallback')(),
+ vapor.vdomSlot(
+ slotsRef,
+ 'default',
+ {},
+ instance,
+ () => template('fallback')(),
+ false,
+ true,
),
)
const host = document.createElement('div')
@@ -826,10 +1031,53 @@ describe('component: slots', () => {
await nextTick()
expect(host.innerHTML).toBe('fallback')
- expect(boundary.markDirty).toHaveBeenCalledTimes(1)
+ expect(boundary.markDirty).not.toHaveBeenCalled()
+ })
+
+ test('compiled slot does not reinsert active fallback while content stays invalid', async () => {
+ const data = ref('first')
+ const Child = compile(
+ `fallback`,
+ data,
+ )
+ const Parent = compile(
+ `
+
+
+
+ first
+
+
+ second
+
+
+ `,
+ data,
+ { Child },
+ )
+ const root = document.createElement('div')
+ const app = createVaporApp(Parent)
+ app.mount(root)
+
+ const fallback = root.querySelector('span')!
+ const insertBefore = vi.spyOn(Node.prototype, 'insertBefore')
+
+ data.value = 'second'
+ await nextTick()
+
+ expect(root.innerHTML).toContain('fallback')
+ expect(insertBefore.mock.calls.some(([node]) => node === fallback)).toBe(
+ false,
+ )
+
+ insertBefore.mockRestore()
+ app.unmount()
})
- test('vdom slot dirties parent boundary once when valid content becomes empty', async () => {
+ test('vdom slot dirties parent boundary when content validity changes', async () => {
const show = ref(true)
const boundary = {
parent: null,
@@ -845,7 +1093,15 @@ describe('component: slots', () => {
default: () => (show.value ? [h('div', 'content')] : []),
})
const frag = withOwnedSlotBoundary(boundary, () =>
- vapor.vdomSlot(slotsRef, 'default', {}, instance),
+ vapor.vdomSlot(
+ slotsRef,
+ 'default',
+ {},
+ instance,
+ undefined,
+ false,
+ true,
+ ),
)
const host = document.createElement('div')
@@ -857,6 +1113,12 @@ describe('component: slots', () => {
expect(host.innerHTML).toBe('')
expect(boundary.markDirty).toHaveBeenCalledTimes(1)
+
+ show.value = true
+ await nextTick()
+
+ expect(host.innerHTML).toContain('content
')
+ expect(boundary.markDirty).toHaveBeenCalledTimes(2)
})
test('vdom slot dirties parent boundary when nested fallback validity flips', async () => {
@@ -875,7 +1137,7 @@ describe('component: slots', () => {
default: () => [],
})
const renderInnerFallback = () => {
- const child = new SlotFragment()
+ const child = new SlotFragment(true)
renderEffect(() => {
child.updateSlot(
showInnerFallback.value
@@ -886,7 +1148,15 @@ describe('component: slots', () => {
return child
}
const frag = withOwnedSlotBoundary(boundary, () =>
- vapor.vdomSlot(slotsRef, 'default', {}, instance, renderInnerFallback),
+ vapor.vdomSlot(
+ slotsRef,
+ 'default',
+ {},
+ instance,
+ renderInnerFallback,
+ false,
+ true,
+ ),
)
const host = document.createElement('div')
@@ -981,9 +1251,17 @@ describe('component: slots', () => {
)
})
- test('plain slot without fallback does not enter fallback boundary', () => {
+ test('plain slot without fallback uses dynamic fragment', () => {
+ let slotBlock!: Block
let observedBoundary: SlotBoundaryContext | null | undefined
- const Comp = defineVaporComponent(() => createSlot())
+ const Comp = defineVaporComponent(() => {
+ return (slotBlock = createSlot(
+ 'default',
+ null,
+ undefined,
+ VaporSlotFlags.SLOT_ROOT,
+ ))
+ })
define(() =>
createComponent(Comp, null, {
@@ -994,9 +1272,24 @@ describe('component: slots', () => {
}),
).render()
+ expect(slotBlock).toBeInstanceOf(DynamicFragment)
+ expect(slotBlock).not.toBeInstanceOf(SlotFragment)
expect(observedBoundary).toBe(null)
})
+ test('slot with fallback uses slot fragment', () => {
+ let slotBlock!: Block
+ const Comp = defineVaporComponent(() => {
+ return (slotBlock = createSlot('default', null, () =>
+ template('fallback')(),
+ ))
+ })
+
+ define(() => createComponent(Comp, null, {})).render()
+
+ expect(slotBlock).toBeInstanceOf(SlotFragment)
+ })
+
test('slot props should be isolated per fragment in v-for', async () => {
const items = ref([0, 1, 2])
@@ -1340,6 +1633,8 @@ describe('component: slots', () => {
() => {
return document.createTextNode('content')
},
+ undefined,
+ keyedSlotRootIfShape,
)
},
})
@@ -1350,7 +1645,7 @@ describe('component: slots', () => {
toggle.value = false
await nextTick()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
toggle.value = true
await nextTick()
@@ -1377,13 +1672,15 @@ describe('component: slots', () => {
() => {
return document.createTextNode('content')
},
+ undefined,
+ keyedSlotRootIfShape,
)
},
})
},
}).render()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
toggle.value = true
await nextTick()
@@ -1391,7 +1688,7 @@ describe('component: slots', () => {
toggle.value = false
await nextTick()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
})
test('dynamic slot work with v-if', async () => {
@@ -1510,13 +1807,15 @@ describe('component: slots', () => {
() => {
return document.createTextNode('content')
},
+ undefined,
+ keyedSlotRootIfShape,
)
},
})
},
}).render()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
toggle.value = true
await nextTick()
@@ -1524,7 +1823,7 @@ describe('component: slots', () => {
toggle.value = false
await nextTick()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
})
test('render fallback with nested v-if', async () => {
@@ -1551,19 +1850,23 @@ describe('component: slots', () => {
() => {
return document.createTextNode('content')
},
+ undefined,
+ keyedSlotRootIfShape,
)
},
+ undefined,
+ keyedSlotRootIfShape,
)
},
})
},
}).render()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
outerShow.value = true
await nextTick()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
innerShow.value = true
await nextTick()
@@ -1571,15 +1874,15 @@ describe('component: slots', () => {
innerShow.value = false
await nextTick()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
outerShow.value = false
await nextTick()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
outerShow.value = true
await nextTick()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
innerShow.value = true
await nextTick()
@@ -1610,6 +1913,8 @@ describe('component: slots', () => {
)
return n4
},
+ undefined,
+ slotRootForFlags,
)
return n2
},
@@ -1621,11 +1926,11 @@ describe('component: slots', () => {
items.value.pop()
await nextTick()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
items.value.pop()
await nextTick()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
items.value.push(2)
await nextTick()
@@ -1656,6 +1961,8 @@ describe('component: slots', () => {
)
return n4
},
+ undefined,
+ slotRootForFlags,
)
return n2
},
@@ -1663,7 +1970,7 @@ describe('component: slots', () => {
},
}).render()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
items.value.push(1)
await nextTick()
@@ -1671,11 +1978,11 @@ describe('component: slots', () => {
items.value.pop()
await nextTick()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
items.value.pop()
await nextTick()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
items.value.push(2)
await nextTick()
@@ -1709,16 +2016,19 @@ describe('component: slots', () => {
)
return n5
},
+ undefined,
+ keyedSlotRootIfShape,
)
},
item => item.text,
+ slotRootForFlags,
)
},
})
},
}).render()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
items.value[0].show = true
await nextTick()
@@ -1726,7 +2036,7 @@ describe('component: slots', () => {
items.value[0].show = false
await nextTick()
- expect(html()).toBe('fallback')
+ expect(html()).toBe('fallback')
})
test('should not render fallback for a single empty item in v-for', async () => {
@@ -1759,9 +2069,12 @@ describe('component: slots', () => {
)
return n5
},
+ undefined,
+ keyedSlotRootIfShape,
)
},
item => item.text,
+ slotRootForFlags,
)
},
})
@@ -2428,6 +2741,8 @@ describe('component: slots', () => {
})
return n5
},
+ undefined,
+ keyedSlotRootIfShape,
)
return n0
},
@@ -2485,6 +2800,8 @@ describe('component: slots', () => {
const n4 = template('if content
')()
return n4
},
+ undefined,
+ keyedSlotRootIfShape,
)
return n2
})
@@ -2503,7 +2820,7 @@ describe('component: slots', () => {
},
}).render()
- expect(html()).toBe('child fallback')
+ expect(html()).toBe('child fallback')
show.value = true
await nextTick()
@@ -2535,6 +2852,8 @@ describe('component: slots', () => {
)
return n4
},
+ undefined,
+ slotRootForFlags,
)
return n2
})
@@ -2553,7 +2872,7 @@ describe('component: slots', () => {
},
}).render()
- expect(html()).toBe('child fallback')
+ expect(html()).toBe('child fallback')
items.value.push(1)
await nextTick()
@@ -2561,7 +2880,8 @@ describe('component: slots', () => {
items.value.pop()
await nextTick()
- expect(html()).toBe('child fallback')
+ await nextTick()
+ expect(html()).toBe('child fallback')
})
test('consecutive slots with insertion state', async () => {
@@ -3106,7 +3426,7 @@ describe('component: slots', () => {
expect(fallbackSpy).toHaveBeenCalledTimes(2)
})
- test('moving active vdom fallback keeps slot carrier order after teleport move', async () => {
+ test('moving active vdom fallback keeps slot anchor order after teleport move', async () => {
const targetA = document.createElement('div')
targetA.id = 'component-slots-fallback-target-a'
const targetB = document.createElement('div')
@@ -3280,11 +3600,11 @@ describe('component: slots', () => {
const root = document.createElement('div')
createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
- expect(root.innerHTML).toBe('fallback
')
+ expect(root.innerHTML).toBe('fallback
')
useFallback.value = false
await nextTick()
- expect(root.innerHTML).toBe('')
+ expect(root.innerHTML).toBe('')
})
test('vdom fallback toggles between local and inherited fallback for non-slot-fragment content', async () => {
@@ -3328,15 +3648,15 @@ describe('component: slots', () => {
const root = document.createElement('div')
createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
- expect(root.innerHTML).toBe('local fallback
')
+ expect(root.innerHTML).toBe('local fallback
')
useFallback.value = false
await nextTick()
- expect(root.innerHTML).toBe('outer fallback
')
+ expect(root.innerHTML).toBe('outer fallback
')
useFallback.value = true
await nextTick()
- expect(root.innerHTML).toBe('local fallback
')
+ expect(root.innerHTML).toBe('local fallback
')
})
test('nested interop vapor slot fallback should satisfy enclosing vapor slot validity after content becomes invalid', async () => {
@@ -3361,6 +3681,8 @@ describe('component: slots', () => {
createIf(
() => showContent.value,
() => template('content')(),
+ undefined,
+ slotRootIfShape,
),
},
true,
@@ -3388,9 +3710,7 @@ describe('component: slots', () => {
showContent.value = false
await nextTick()
- expect(root.innerHTML).toBe(
- 'local fallback
',
- )
+ expect(root.innerHTML).toBe('local fallback
')
})
test('vdom fallback addition activates inherited vapor fallback', async () => {
@@ -3426,7 +3746,7 @@ describe('component: slots', () => {
useFallback.value = true
await nextTick()
- expect(root.innerHTML).toBe('fallback
')
+ expect(root.innerHTML).toBe('fallback
')
})
test('vdom fallback addition should not remount valid forwarded vapor content', async () => {
@@ -3487,6 +3807,8 @@ describe('component: slots', () => {
return createIf(
() => showContent.value,
() => template('content')(),
+ undefined,
+ keyedSlotRootIfShape,
)
},
})
@@ -3527,7 +3849,7 @@ describe('component: slots', () => {
showContent.value = false
await nextTick()
- expect(root.innerHTML).toBe('fallback
')
+ expect(root.innerHTML).toBe('fallback
')
expect(mountSpy).toHaveBeenCalledTimes(1)
})
@@ -3612,6 +3934,8 @@ describe('component: slots', () => {
createIf(
() => showInner.value,
() => template('inner')(),
+ undefined,
+ slotRootIfShape,
),
},
true,
@@ -3637,7 +3961,7 @@ describe('component: slots', () => {
showInner.value = false
await nextTick()
expect(root.innerHTML).toBe(
- 'stableouter fallback
',
+ 'stableouter fallback
',
)
})
@@ -3679,6 +4003,8 @@ describe('component: slots', () => {
createIf(
() => showInner.value,
() => template('inner')(),
+ undefined,
+ slotRootIfShape,
),
},
true,
@@ -3706,7 +4032,7 @@ describe('component: slots', () => {
showInner.value = false
await nextTick()
expect(root.innerHTML).toBe(
- 'stableouter fallback
',
+ 'stableouter fallback
',
)
})
diff --git a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts
index 2dab77912b1..4882ce7ad24 100644
--- a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts
+++ b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts
@@ -14,7 +14,7 @@ import {
vModelText,
withDirectives,
} from 'vue'
-import { VaporBlockShape } from '@vue/shared'
+import { VaporBlockShape, VaporVForFlags } from '@vue/shared'
import type { LooseRawProps, VaporComponent } from '../../src/component'
import { ifFlags, makeRender } from '../_utils'
import { VaporKeepAlive } from '../../src/components/KeepAlive'
@@ -431,6 +431,7 @@ describe('VaporKeepAlive', () => {
return n0
},
item => item,
+ VaporVForFlags.SLOT_ROOT,
),
})
@@ -472,9 +473,7 @@ describe('VaporKeepAlive', () => {
toggle.value = true
await nextTick()
- expect(html()).toBe(
- 'fallback
',
- )
+ expect(html()).toBe('fallback
')
itemsA.value = [3]
await nextTick()
diff --git a/packages/runtime-vapor/__tests__/dom/prop.spec.ts b/packages/runtime-vapor/__tests__/dom/prop.spec.ts
index 65f285ba0cb..485c3a3a4f4 100644
--- a/packages/runtime-vapor/__tests__/dom/prop.spec.ts
+++ b/packages/runtime-vapor/__tests__/dom/prop.spec.ts
@@ -1,4 +1,4 @@
-import { NOOP, toDisplayString } from '@vue/shared'
+import { NOOP, VaporDynamicComponentFlags, toDisplayString } from '@vue/shared'
import {
setDynamicProp as _setDynamicProp,
setAttr,
@@ -756,7 +756,12 @@ describe('patchProp', () => {
const value = ref('foo')
const { html } = define({
setup() {
- const n1 = createDynamicComponent(() => Comp, null, null, true)
+ const n1 = createDynamicComponent(
+ () => Comp,
+ null,
+ null,
+ VaporDynamicComponentFlags.SINGLE_ROOT,
+ )
renderEffect(() => setBlockText(n1, toDisplayString(value)))
return n1
},
@@ -769,7 +774,12 @@ describe('patchProp', () => {
const value = ref('foo')
const { html } = define({
setup() {
- const n1 = createDynamicComponent(() => 'button', null, null, true)
+ const n1 = createDynamicComponent(
+ () => 'button',
+ null,
+ null,
+ VaporDynamicComponentFlags.SINGLE_ROOT,
+ )
renderEffect(() => setBlockText(n1, toDisplayString(value)))
return n1
},
@@ -848,7 +858,12 @@ describe('patchProp', () => {
const value = ref('foo
')
const { html } = define({
setup() {
- const n1 = createDynamicComponent(() => Comp, null, null, true)
+ const n1 = createDynamicComponent(
+ () => Comp,
+ null,
+ null,
+ VaporDynamicComponentFlags.SINGLE_ROOT,
+ )
renderEffect(() => setBlockHtml(n1, value.value))
return n1
},
@@ -861,7 +876,12 @@ describe('patchProp', () => {
const value = ref('foo
')
const { html } = define({
setup() {
- const n1 = createDynamicComponent(() => 'button', null, null, true)
+ const n1 = createDynamicComponent(
+ () => 'button',
+ null,
+ null,
+ VaporDynamicComponentFlags.SINGLE_ROOT,
+ )
renderEffect(() => setBlockHtml(n1, value.value))
return n1
},
diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts
index 43753b48970..21779df9733 100644
--- a/packages/runtime-vapor/__tests__/hydration.spec.ts
+++ b/packages/runtime-vapor/__tests__/hydration.spec.ts
@@ -3523,7 +3523,17 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
- baz
+ baz
+ "
+ `,
+ )
+
+ data.items[0].show = true
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ bar
"
`,
)
@@ -3696,9 +3706,9 @@ describe('Vapor Mode hydration', () => {
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
- "
- foo
-
"
+ "
+ foo
+
"
`,
)
@@ -3706,9 +3716,9 @@ describe('Vapor Mode hydration', () => {
await nextTick()
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
- "
- foo1
-
"
+ "
+ foo1
+
"
`,
)
})
@@ -10518,6 +10528,42 @@ describe('VDOM interop', () => {
)
})
+ test('hydrate compiled VDOM slot content can switch to vapor fallback', async () => {
+ const data = ref({ show: true })
+ const { container } = await testWithVDOMApp(
+ `
+
+
+ content
+
+ `,
+ {
+ VaporChild: {
+ code: `fallback`,
+ vapor: true,
+ },
+ },
+ data,
+ )
+
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+ "
+ content
+ "
+ `)
+
+ data.value.show = false
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toBe('fallback')
+
+ data.value.show = true
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toBe('content
')
+ })
+
test('hydrate VDOM slot content can mount after hydrating as empty', async () => {
const data = reactive({
show: false,
@@ -10791,7 +10837,7 @@ describe('VDOM interop', () => {
`
"
-
+
"
`,
@@ -10803,7 +10849,7 @@ describe('VDOM interop', () => {
`
"
-
+
"
`,
@@ -11023,7 +11069,7 @@ describe('VDOM interop', () => {
`
"
- foo
bar
+ foo
bar
tail
"
`,
@@ -11068,7 +11114,7 @@ describe('VDOM interop', () => {
"
foo
-
+
"
`,
)
@@ -11082,7 +11128,7 @@ describe('VDOM interop', () => {
"