Skip to content

Commit 2ac4b72

Browse files
committed
fix(compiler/v-slot): handle implicit default slot mixed with named slots
1 parent bb6a346 commit 2ac4b72

File tree

4 files changed

+110
-40
lines changed

4 files changed

+110
-40
lines changed

packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap

+35-14
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,6 @@ return function render() {
1515
}"
1616
`;
1717

18-
exports[`compiler: transform component slots explicit default slot 1`] = `
19-
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
20-
21-
return function render() {
22-
const _ctx = this
23-
const _component_Comp = resolveComponent(\\"Comp\\")
24-
25-
return (openBlock(), createBlock(_component_Comp, null, {
26-
default: ({ foo }) => [toString(foo), toString(_ctx.bar)],
27-
_compiled: true
28-
}))
29-
}"
30-
`;
31-
3218
exports[`compiler: transform component slots implicit default slot 1`] = `
3319
"const { createVNode, resolveComponent, createBlock, openBlock } = Vue
3420
@@ -146,6 +132,27 @@ return function render() {
146132
}"
147133
`;
148134

135+
exports[`compiler: transform component slots named slots w/ implicit default slot 1`] = `
136+
"const _Vue = Vue
137+
138+
return function render() {
139+
with (this) {
140+
const { createVNode: _createVNode, resolveComponent: _resolveComponent, createBlock: _createBlock, openBlock: _openBlock } = _Vue
141+
142+
const _component_Comp = _resolveComponent(\\"Comp\\")
143+
144+
return (_openBlock(), _createBlock(_component_Comp, null, {
145+
one: () => [\\"foo\\"],
146+
default: () => [
147+
\\"bar\\",
148+
_createVNode(\\"span\\")
149+
],
150+
_compiled: true
151+
}))
152+
}
153+
}"
154+
`;
155+
149156
exports[`compiler: transform component slots nested slots scoping 1`] = `
150157
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
151158
@@ -169,3 +176,17 @@ return function render() {
169176
}))
170177
}"
171178
`;
179+
180+
exports[`compiler: transform component slots on-component default slot 1`] = `
181+
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
182+
183+
return function render() {
184+
const _ctx = this
185+
const _component_Comp = resolveComponent(\\"Comp\\")
186+
187+
return (openBlock(), createBlock(_component_Comp, null, {
188+
default: ({ foo }) => [toString(foo), toString(_ctx.bar)],
189+
_compiled: true
190+
}))
191+
}"
192+
`;

packages/compiler-core/__tests__/transforms/vSlot.spec.ts

+40-3
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe('compiler: transform component slots', () => {
9595
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
9696
})
9797

98-
test('explicit default slot', () => {
98+
test('on-component default slot', () => {
9999
const { root, slots } = parseWithSlots(
100100
`<Comp v-slot="{ foo }">{{ foo }}{{ bar }}</Comp>`,
101101
{ prefixIdentifiers: true }
@@ -189,6 +189,43 @@ describe('compiler: transform component slots', () => {
189189
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
190190
})
191191

192+
test('named slots w/ implicit default slot', () => {
193+
const { root, slots } = parseWithSlots(
194+
`<Comp>
195+
<template #one>foo</template>bar<span/>
196+
</Comp>`
197+
)
198+
expect(slots).toMatchObject(
199+
createSlotMatcher({
200+
one: {
201+
type: NodeTypes.JS_FUNCTION_EXPRESSION,
202+
params: undefined,
203+
returns: [
204+
{
205+
type: NodeTypes.TEXT,
206+
content: `foo`
207+
}
208+
]
209+
},
210+
default: {
211+
type: NodeTypes.JS_FUNCTION_EXPRESSION,
212+
params: undefined,
213+
returns: [
214+
{
215+
type: NodeTypes.TEXT,
216+
content: `bar`
217+
},
218+
{
219+
type: NodeTypes.ELEMENT,
220+
tag: `span`
221+
}
222+
]
223+
}
224+
})
225+
)
226+
expect(generate(root).code).toMatchSnapshot()
227+
})
228+
192229
test('dynamically named slots', () => {
193230
const { root, slots } = parseWithSlots(
194231
`<Comp>
@@ -608,13 +645,13 @@ describe('compiler: transform component slots', () => {
608645
})
609646

610647
describe('errors', () => {
611-
test('error on extraneous children w/ named slots', () => {
648+
test('error on extraneous children w/ named default slot', () => {
612649
const onError = jest.fn()
613650
const source = `<Comp><template #default>foo</template>bar</Comp>`
614651
parseWithSlots(source, { onError })
615652
const index = source.indexOf('bar')
616653
expect(onError.mock.calls[0][0]).toMatchObject({
617-
code: ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
654+
code: ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
618655
loc: {
619656
source: `bar`,
620657
start: {

packages/compiler-core/src/errors.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export const enum ErrorCodes {
7676
X_V_SLOT_NAMED_SLOT_ON_COMPONENT,
7777
X_V_SLOT_MIXED_SLOT_USAGE,
7878
X_V_SLOT_DUPLICATE_SLOT_NAMES,
79-
X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
79+
X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
8080
X_V_SLOT_MISPLACED,
8181
X_V_MODEL_NO_EXPRESSION,
8282
X_V_MODEL_MALFORMED_EXPRESSION,
@@ -168,9 +168,9 @@ export const errorMessages: { [code: number]: string } = {
168168
`The default slot should also use <template> syntax when there are other ` +
169169
`named slots to avoid scope ambiguity.`,
170170
[ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES]: `Duplicate slot names found. `,
171-
[ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN]:
172-
`Extraneous children found when component has explicit slots. ` +
173-
`These children will be ignored.`,
171+
[ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN]:
172+
`Extraneous children found when component already has explicitly named ` +
173+
`default slot. These children will be ignored.`,
174174
[ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
175175
[ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
176176
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,

packages/compiler-core/src/transforms/vSlot.ts

+31-19
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@ export function buildSlots(
117117

118118
// 1. Check for default slot with slotProps on component itself.
119119
// <Comp v-slot="{ prop }"/>
120-
const explicitDefaultSlot = findDir(node, 'slot', true)
121-
if (explicitDefaultSlot) {
122-
const { arg, exp, loc } = explicitDefaultSlot
120+
const onComponentDefaultSlot = findDir(node, 'slot', true)
121+
if (onComponentDefaultSlot) {
122+
const { arg, exp, loc } = onComponentDefaultSlot
123123
if (arg) {
124124
context.onError(
125125
createCompilerError(ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT, loc)
@@ -131,8 +131,10 @@ export function buildSlots(
131131
// 2. Iterate through children and check for template slots
132132
// <template v-slot:foo="{ prop }">
133133
let hasTemplateSlots = false
134-
let extraneousChild: TemplateChildNode | undefined = undefined
134+
let hasNamedDefaultSlot = false
135+
const implicitDefaultChildren: TemplateChildNode[] = []
135136
const seenSlotNames = new Set<string>()
137+
136138
for (let i = 0; i < children.length; i++) {
137139
const slotElement = children[i]
138140
let slotDir
@@ -142,13 +144,13 @@ export function buildSlots(
142144
!(slotDir = findDir(slotElement, 'slot', true))
143145
) {
144146
// not a <template v-slot>, skip.
145-
if (slotElement.type !== NodeTypes.COMMENT && !extraneousChild) {
146-
extraneousChild = slotElement
147+
if (slotElement.type !== NodeTypes.COMMENT) {
148+
implicitDefaultChildren.push(slotElement)
147149
}
148150
continue
149151
}
150152

151-
if (explicitDefaultSlot) {
153+
if (onComponentDefaultSlot) {
152154
// already has on-component default slot - this is incorrect usage.
153155
context.onError(
154156
createCompilerError(ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, slotDir.loc)
@@ -267,23 +269,33 @@ export function buildSlots(
267269
continue
268270
}
269271
seenSlotNames.add(staticSlotName)
272+
if (staticSlotName === 'default') {
273+
hasNamedDefaultSlot = true
274+
}
270275
}
271276
slotsProperties.push(createObjectProperty(slotName, slotFunction))
272277
}
273278
}
274279

275-
if (hasTemplateSlots && extraneousChild) {
276-
context.onError(
277-
createCompilerError(
278-
ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
279-
extraneousChild.loc
280-
)
281-
)
282-
}
283-
284-
if (!explicitDefaultSlot && !hasTemplateSlots) {
285-
// implicit default slot.
286-
slotsProperties.push(buildDefaultSlot(undefined, children, loc))
280+
if (!onComponentDefaultSlot) {
281+
if (!hasTemplateSlots) {
282+
// implicit default slot (on component)
283+
slotsProperties.push(buildDefaultSlot(undefined, children, loc))
284+
} else if (implicitDefaultChildren.length) {
285+
// implicit default slot (mixed with named slots)
286+
if (hasNamedDefaultSlot) {
287+
context.onError(
288+
createCompilerError(
289+
ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
290+
implicitDefaultChildren[0].loc
291+
)
292+
)
293+
} else {
294+
slotsProperties.push(
295+
buildDefaultSlot(undefined, implicitDefaultChildren, loc)
296+
)
297+
}
298+
}
287299
}
288300

289301
let slots: ObjectExpression | CallExpression = createObjectExpression(

0 commit comments

Comments
 (0)