Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions packages/compiler-core/__tests__/vForSlotIssue.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, test } from 'vitest'
import { baseParse as parse } from '../src/parser'
import { transform } from '../src/transform'
import { transformFor } from '../src/transforms/vFor'
import { transformIf } from '../src/transforms/vIf'
import { transformElement } from '../src/transforms/transformElement'
import { transformSlotOutlet } from '../src/transforms/transformSlotOutlet'
import { trackSlotScopes, trackVForSlotScopes } from '../src/transforms/vSlot'
import { type ElementNode, NodeTypes } from '../src/ast'

function parseTemplate(template: string) {
const ast = parse(template)
transform(ast, {
nodeTransforms: [
transformIf,
transformFor,
trackVForSlotScopes,
transformSlotOutlet,
transformElement,
trackSlotScopes,
],
})
return ast
}

describe('v-for + v-slot issue', () => {
test('template v-for without v-slot should create FOR node', () => {
const ast = parseTemplate(`<Foo><template v-for="i in 3" /></Foo>`)
const comp = ast.children[0] as ElementNode
expect(comp.type).toBe(NodeTypes.ELEMENT)
// The first child should be a FOR node
expect(comp.children[0].type).toBe(NodeTypes.FOR)
})

test('template v-for with v-slot should also create FOR node', () => {
const ast = parseTemplate(`<Foo><template v-for="i in 3" v-slot /></Foo>`)
const comp = ast.children[0] as ElementNode
expect(comp.type).toBe(NodeTypes.ELEMENT)

// EXPECTED: FOR node containing ELEMENT with slot directive
// ACTUAL (BUG): ELEMENT node with both for and slot directives
const firstChild = comp.children[0]

// This is the expected behavior according to the issue
expect(firstChild.type).toBe(NodeTypes.FOR)
})
})
8 changes: 7 additions & 1 deletion packages/compiler-core/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,13 @@ export function createStructuralDirectiveTransform(
const { props } = node
// structural directive transforms are not concerned with slots
// as they are handled separately in vSlot.ts
if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
// v-for is an exception: it should still create a FOR node even
// when combined with v-slot, to ensure the AST structure is correct
if (
node.tagType === ElementTypes.TEMPLATE &&
props.some(isVSlot) &&
!matches('for')
) {
return
}
const exitFns = []
Expand Down
15 changes: 14 additions & 1 deletion packages/compiler-core/src/transforms/vFor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
injectProp,
isSlotOutlet,
isTemplateNode,
isVSlot,
} from '../utils'
import {
FRAGMENT,
Expand All @@ -54,6 +55,14 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
(node, dir, context) => {
const { helper, removeHelper } = context
return processFor(node, dir, context, forNode => {
// When <template v-for v-slot> is used, skip codegen here since
// it will be handled by buildSlots in vSlot.ts
// The AST structure is still correct (FOR node with template child)
const hasVSlot = isTemplateNode(node) && node.props.some(isVSlot)
if (hasVSlot) {
return
}

// create the loop render function expression now, and add the
// iterator on exit after all children have been traversed
const renderExp = createCallExpression(helper(RENDER_LIST), [
Expand Down Expand Up @@ -280,6 +289,10 @@ export function processFor(
const { addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult

// When <template v-for v-slot> is used, keep the template element
// (with v-slot) as the child, so that buildSlots can find and process it
const hasVSlot = isTemplateNode(node) && node.props.some(isVSlot)

const forNode: ForNode = {
type: NodeTypes.FOR,
loc: dir.loc,
Expand All @@ -288,7 +301,7 @@ export function processFor(
keyAlias: key,
objectIndexAlias: index,
parseResult,
children: isTemplateNode(node) ? node.children : [node],
children: isTemplateNode(node) && !hasVSlot ? node.children : [node],
}

context.replaceNode(forNode)
Expand Down
58 changes: 58 additions & 0 deletions packages/compiler-core/src/transforms/vSlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type ElementNode,
ElementTypes,
type ExpressionNode,
type ForNode,
type FunctionExpression,
NodeTypes,
type ObjectExpression,
Expand Down Expand Up @@ -174,6 +175,63 @@ export function buildSlots(
const slotElement = children[i]
let slotDir

// Handle FOR node containing a template with v-slot
// This happens when <template v-for v-slot> is used
if (
slotElement.type === NodeTypes.FOR &&
slotElement.children.length === 1 &&
isTemplateNode(slotElement.children[0]) &&
(slotDir = findDir(slotElement.children[0], 'slot', true))
) {
if (onComponentSlot) {
context.onError(
createCompilerError(
ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE,
slotDir.loc,
),
)
break
}

hasTemplateSlots = true
const forNode = slotElement as ForNode
const innerSlotElement = forNode.children[0] as ElementNode
const { children: slotChildren, loc: slotLoc } = innerSlotElement
const {
arg: slotName = createSimpleExpression(`default`, true),
exp: slotProps,
} = slotDir

// check if name is dynamic.
if (!isStaticExp(slotName)) {
hasDynamicSlots = true
}

// Pass undefined for vFor param as the v-for context is already handled
// by the FOR node - the slot function doesn't need the directive
const slotFunction = buildSlotFn(
slotProps,
undefined,
slotChildren,
slotLoc,
)

// v-for slots are always dynamic
hasDynamicSlots = true
// Use the parseResult from the FOR node
dynamicSlots.push(
createCallExpression(context.helper(RENDER_LIST), [
forNode.source,
createFunctionExpression(
createForLoopParams(forNode.parseResult),
buildDynamicSlot(slotName, slotFunction),
true /* force newline */,
),
]),
)
continue
}

if (
!isTemplateNode(slotElement) ||
!(slotDir = findDir(slotElement, 'slot', true))
Expand Down