fix(compiler/v-slot): handle implicit default slot mixed with named slots

This commit is contained in:
Evan You 2020-01-06 15:31:21 -05:00
parent bb6a346996
commit 2ac4b723e0
4 changed files with 110 additions and 40 deletions

View File

@ -15,20 +15,6 @@ return function render() {
}"
`;
exports[`compiler: transform component slots explicit default slot 1`] = `
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, {
default: ({ foo }) => [toString(foo), toString(_ctx.bar)],
_compiled: true
}))
}"
`;
exports[`compiler: transform component slots implicit default slot 1`] = `
"const { createVNode, resolveComponent, createBlock, openBlock } = Vue
@ -146,6 +132,27 @@ return function render() {
}"
`;
exports[`compiler: transform component slots named slots w/ implicit default slot 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { createVNode: _createVNode, resolveComponent: _resolveComponent, createBlock: _createBlock, openBlock: _openBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
one: () => [\\"foo\\"],
default: () => [
\\"bar\\",
_createVNode(\\"span\\")
],
_compiled: true
}))
}
}"
`;
exports[`compiler: transform component slots nested slots scoping 1`] = `
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
@ -169,3 +176,17 @@ return function render() {
}))
}"
`;
exports[`compiler: transform component slots on-component default slot 1`] = `
"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, {
default: ({ foo }) => [toString(foo), toString(_ctx.bar)],
_compiled: true
}))
}"
`;

View File

@ -95,7 +95,7 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('explicit default slot', () => {
test('on-component default slot', () => {
const { root, slots } = parseWithSlots(
`<Comp v-slot="{ foo }">{{ foo }}{{ bar }}</Comp>`,
{ prefixIdentifiers: true }
@ -189,6 +189,43 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('named slots w/ implicit default slot', () => {
const { root, slots } = parseWithSlots(
`<Comp>
<template #one>foo</template>bar<span/>
</Comp>`
)
expect(slots).toMatchObject(
createSlotMatcher({
one: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: undefined,
returns: [
{
type: NodeTypes.TEXT,
content: `foo`
}
]
},
default: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: undefined,
returns: [
{
type: NodeTypes.TEXT,
content: `bar`
},
{
type: NodeTypes.ELEMENT,
tag: `span`
}
]
}
})
)
expect(generate(root).code).toMatchSnapshot()
})
test('dynamically named slots', () => {
const { root, slots } = parseWithSlots(
`<Comp>
@ -608,13 +645,13 @@ describe('compiler: transform component slots', () => {
})
describe('errors', () => {
test('error on extraneous children w/ named slots', () => {
test('error on extraneous children w/ named default slot', () => {
const onError = jest.fn()
const source = `<Comp><template #default>foo</template>bar</Comp>`
parseWithSlots(source, { onError })
const index = source.indexOf('bar')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
code: ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
loc: {
source: `bar`,
start: {

View File

@ -76,7 +76,7 @@ export const enum ErrorCodes {
X_V_SLOT_NAMED_SLOT_ON_COMPONENT,
X_V_SLOT_MIXED_SLOT_USAGE,
X_V_SLOT_DUPLICATE_SLOT_NAMES,
X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
X_V_SLOT_MISPLACED,
X_V_MODEL_NO_EXPRESSION,
X_V_MODEL_MALFORMED_EXPRESSION,
@ -168,9 +168,9 @@ export const errorMessages: { [code: number]: string } = {
`The default slot should also use <template> syntax when there are other ` +
`named slots to avoid scope ambiguity.`,
[ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES]: `Duplicate slot names found. `,
[ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN]:
`Extraneous children found when component has explicit slots. ` +
`These children will be ignored.`,
[ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN]:
`Extraneous children found when component already has explicitly named ` +
`default slot. These children will be ignored.`,
[ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
[ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,

View File

@ -117,9 +117,9 @@ export function buildSlots(
// 1. Check for default slot with slotProps on component itself.
// <Comp v-slot="{ prop }"/>
const explicitDefaultSlot = findDir(node, 'slot', true)
if (explicitDefaultSlot) {
const { arg, exp, loc } = explicitDefaultSlot
const onComponentDefaultSlot = findDir(node, 'slot', true)
if (onComponentDefaultSlot) {
const { arg, exp, loc } = onComponentDefaultSlot
if (arg) {
context.onError(
createCompilerError(ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT, loc)
@ -131,8 +131,10 @@ export function buildSlots(
// 2. Iterate through children and check for template slots
// <template v-slot:foo="{ prop }">
let hasTemplateSlots = false
let extraneousChild: TemplateChildNode | undefined = undefined
let hasNamedDefaultSlot = false
const implicitDefaultChildren: TemplateChildNode[] = []
const seenSlotNames = new Set<string>()
for (let i = 0; i < children.length; i++) {
const slotElement = children[i]
let slotDir
@ -142,13 +144,13 @@ export function buildSlots(
!(slotDir = findDir(slotElement, 'slot', true))
) {
// not a <template v-slot>, skip.
if (slotElement.type !== NodeTypes.COMMENT && !extraneousChild) {
extraneousChild = slotElement
if (slotElement.type !== NodeTypes.COMMENT) {
implicitDefaultChildren.push(slotElement)
}
continue
}
if (explicitDefaultSlot) {
if (onComponentDefaultSlot) {
// already has on-component default slot - this is incorrect usage.
context.onError(
createCompilerError(ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, slotDir.loc)
@ -267,23 +269,33 @@ export function buildSlots(
continue
}
seenSlotNames.add(staticSlotName)
if (staticSlotName === 'default') {
hasNamedDefaultSlot = true
}
}
slotsProperties.push(createObjectProperty(slotName, slotFunction))
}
}
if (hasTemplateSlots && extraneousChild) {
context.onError(
createCompilerError(
ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
extraneousChild.loc
)
)
}
if (!explicitDefaultSlot && !hasTemplateSlots) {
// implicit default slot.
slotsProperties.push(buildDefaultSlot(undefined, children, loc))
if (!onComponentDefaultSlot) {
if (!hasTemplateSlots) {
// implicit default slot (on component)
slotsProperties.push(buildDefaultSlot(undefined, children, loc))
} else if (implicitDefaultChildren.length) {
// implicit default slot (mixed with named slots)
if (hasNamedDefaultSlot) {
context.onError(
createCompilerError(
ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
implicitDefaultChildren[0].loc
)
)
} else {
slotsProperties.push(
buildDefaultSlot(undefined, implicitDefaultChildren, loc)
)
}
}
}
let slots: ObjectExpression | CallExpression = createObjectExpression(