fix(compiler-core/slots): should support on-component named slots

This commit is contained in:
Evan You 2020-04-01 20:44:53 -04:00
parent 20f4965b45
commit a022b63605
4 changed files with 121 additions and 56 deletions

View File

@ -113,20 +113,6 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: transform component slots named slots 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
one: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
two: _withCtx(({ bar }) => [_toDisplayString(_ctx.foo), _toDisplayString(bar)]),
_: 1
}))
}"
`;
exports[`compiler: transform component slots named slots w/ implicit default slot 1`] = ` exports[`compiler: transform component slots named slots w/ implicit default slot 1`] = `
"const _Vue = Vue "const _Vue = Vue
@ -171,6 +157,32 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: transform component slots on component dynamically named slot 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
[_ctx.named]: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
_: 1
}))
}"
`;
exports[`compiler: transform component slots on component named slot 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
named: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
_: 1
}))
}"
`;
exports[`compiler: transform component slots on-component default slot 1`] = ` exports[`compiler: transform component slots on-component default slot 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue "const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
@ -183,3 +195,17 @@ return function render(_ctx, _cache) {
})) }))
}" }"
`; `;
exports[`compiler: transform component slots template named slots 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, {
one: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]),
two: _withCtx(({ bar }) => [_toDisplayString(_ctx.foo), _toDisplayString(bar)]),
_: 1
}))
}"
`;

View File

@ -130,7 +130,40 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
}) })
test('named slots', () => { test('on component named slot', () => {
const { root, slots } = parseWithSlots(
`<Comp v-slot:named="{ foo }">{{ foo }}{{ bar }}</Comp>`,
{ prefixIdentifiers: true }
)
expect(slots).toMatchObject(
createSlotMatcher({
named: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`{ `, { content: `foo` }, ` }`]
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.bar`
}
}
]
}
})
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('template named slots', () => {
const { root, slots } = parseWithSlots( const { root, slots } = parseWithSlots(
`<Comp> `<Comp>
<template v-slot:one="{ foo }"> <template v-slot:one="{ foo }">
@ -191,6 +224,39 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
}) })
test('on component dynamically named slot', () => {
const { root, slots } = parseWithSlots(
`<Comp v-slot:[named]="{ foo }">{{ foo }}{{ bar }}</Comp>`,
{ prefixIdentifiers: true }
)
expect(slots).toMatchObject(
createSlotMatcher({
'[_ctx.named]': {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`{ `, { content: `foo` }, ` }`]
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.bar`
}
}
]
}
})
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('named slots w/ implicit default slot', () => { test('named slots w/ implicit default slot', () => {
const { root, slots } = parseWithSlots( const { root, slots } = parseWithSlots(
`<Comp> `<Comp>
@ -736,28 +802,5 @@ describe('compiler: transform component slots', () => {
} }
}) })
}) })
test('error on named slot on component', () => {
const onError = jest.fn()
const source = `<Comp v-slot:foo>foo</Comp>`
parseWithSlots(source, { onError })
const index = source.indexOf('v-slot')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT,
loc: {
source: `v-slot:foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 10,
line: 1,
column: index + 11
}
}
})
})
}) })
}) })

View File

@ -76,7 +76,6 @@ export const enum ErrorCodes {
X_V_BIND_NO_EXPRESSION, X_V_BIND_NO_EXPRESSION,
X_V_ON_NO_EXPRESSION, X_V_ON_NO_EXPRESSION,
X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
X_V_SLOT_NAMED_SLOT_ON_COMPONENT,
X_V_SLOT_MIXED_SLOT_USAGE, X_V_SLOT_MIXED_SLOT_USAGE,
X_V_SLOT_DUPLICATE_SLOT_NAMES, X_V_SLOT_DUPLICATE_SLOT_NAMES,
X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN, X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
@ -163,13 +162,10 @@ export const errorMessages: { [code: number]: string } = {
[ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`, [ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
[ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression.`, [ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression.`,
[ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`, [ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`,
[ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT]:
`Named v-slot on component. ` +
`Named slots should use <template v-slot> syntax nested inside the component.`,
[ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE]: [ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE]:
`Mixed v-slot usage on both the component and nested <template>.` + `Mixed v-slot usage on both the component and nested <template>.` +
`The default slot should also use <template> syntax when there are other ` + `When there are multiple named slots, all slots should use <template> ` +
`named slots to avoid scope ambiguity.`, `syntax to avoid scope ambiguity.`,
[ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES]: `Duplicate slot names found. `, [ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES]: `Duplicate slot names found. `,
[ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN]: [ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN]:
`Extraneous children found when component already has explicitly named ` + `Extraneous children found when component already has explicitly named ` +

View File

@ -139,17 +139,17 @@ export function buildSlots(
hasDynamicSlots = hasScopeRef(node, context.identifiers) hasDynamicSlots = hasScopeRef(node, context.identifiers)
} }
// 1. Check for default slot with slotProps on component itself. // 1. Check for slot with slotProps on component itself.
// <Comp v-slot="{ prop }"/> // <Comp v-slot="{ prop }"/>
const onComponentDefaultSlot = findDir(node, 'slot', true) const onComponentSlot = findDir(node, 'slot', true)
if (onComponentDefaultSlot) { if (onComponentSlot) {
const { arg, exp, loc } = onComponentDefaultSlot const { arg, exp } = onComponentSlot
if (arg) { slotsProperties.push(
context.onError( createObjectProperty(
createCompilerError(ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT, loc) arg || createSimpleExpression('default', true),
buildSlotFn(exp, children, loc)
)
) )
}
slotsProperties.push(buildDefaultSlotProperty(exp, children))
} }
// 2. Iterate through children and check for template slots // 2. Iterate through children and check for template slots
@ -174,8 +174,8 @@ export function buildSlots(
continue continue
} }
if (onComponentDefaultSlot) { if (onComponentSlot) {
// already has on-component default slot - this is incorrect usage. // already has on-component slot - this is incorrect usage.
context.onError( context.onError(
createCompilerError(ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, slotDir.loc) createCompilerError(ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, slotDir.loc)
) )
@ -294,7 +294,7 @@ export function buildSlots(
} }
} }
if (!onComponentDefaultSlot) { if (!onComponentSlot) {
if (!hasTemplateSlots) { if (!hasTemplateSlots) {
// implicit default slot (on component) // implicit default slot (on component)
slotsProperties.push(buildDefaultSlotProperty(undefined, children)) slotsProperties.push(buildDefaultSlotProperty(undefined, children))