Skip to content

Commit aa9245d

Browse files
committed
feat(compiler): render <slot/> as block fragments
1 parent fc47029 commit aa9245d

File tree

12 files changed

+268
-191
lines changed

12 files changed

+268
-191
lines changed

packages/compiler-core/__tests__/transform.spec.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -274,16 +274,13 @@ describe('compiler: transform', () => {
274274

275275
test('single <slot/>', () => {
276276
const ast = transformWithCodegen(`<slot/>`)
277-
expect(ast.codegenNode).toMatchObject(
278-
createBlockMatcher([
279-
`_${FRAGMENT}`,
280-
`null`,
281-
{
282-
type: NodeTypes.JS_CALL_EXPRESSION,
283-
callee: `_${RENDER_SLOT}`
284-
}
285-
])
286-
)
277+
expect(ast.codegenNode).toMatchObject({
278+
codegenNode: {
279+
type: NodeTypes.JS_CALL_EXPRESSION,
280+
callee: `_${RENDER_SLOT}`,
281+
arguments: ['$slots.default']
282+
}
283+
})
287284
})
288285

289286
test('single element', () => {

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,21 @@ return function render() {
112112
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, renderSlot: _renderSlot } = _Vue
113113
114114
return (_openBlock(), _createBlock(_Fragment, null, _renderList(items, (item) => {
115-
return (_openBlock(), _createBlock(_Fragment, null, _renderSlot($slots.default)))
115+
return _renderSlot($slots.default)
116+
}), 128 /* UNKEYED_FRAGMENT */))
117+
}
118+
}"
119+
`;
120+
121+
exports[`compiler: v-for codegen v-for on <slot/> 1`] = `
122+
"const _Vue = Vue
123+
124+
return function render() {
125+
with (this) {
126+
const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, renderSlot: _renderSlot } = _Vue
127+
128+
return (_openBlock(), _createBlock(_Fragment, null, _renderList(items, (item) => {
129+
return _renderSlot($slots.default)
116130
}), 128 /* UNKEYED_FRAGMENT */))
117131
}
118132
}"

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ exports[`compiler: v-if codegen template v-if w/ single <slot/> child 1`] = `
3737
3838
return function render() {
3939
with (this) {
40-
const { openBlock: _openBlock, renderSlot: _renderSlot, Fragment: _Fragment, createBlock: _createBlock, Empty: _Empty } = _Vue
40+
const { openBlock: _openBlock, renderSlot: _renderSlot, createBlock: _createBlock, Empty: _Empty } = _Vue
4141
4242
return (_openBlock(), ok
43-
? _createBlock(_Fragment, { key: 0 }, _renderSlot($slots.default))
43+
? _renderSlot($slots.default, { key: 0 })
4444
: _createBlock(_Empty))
4545
}
4646
}"

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

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,8 @@ describe('compiler: v-for', () => {
567567
describe('codegen', () => {
568568
function assertSharedCodegen(
569569
node: SequenceExpression,
570-
keyed: boolean = false
570+
keyed: boolean = false,
571+
customReturn: boolean = false
571572
) {
572573
expect(node).toMatchObject({
573574
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
@@ -589,19 +590,21 @@ describe('compiler: v-for', () => {
589590
{}, // to be asserted by each test
590591
{
591592
type: NodeTypes.JS_FUNCTION_EXPRESSION,
592-
returns: {
593-
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
594-
expressions: [
595-
{
596-
type: NodeTypes.JS_CALL_EXPRESSION,
597-
callee: `_${OPEN_BLOCK}`
598-
},
599-
{
600-
type: NodeTypes.JS_CALL_EXPRESSION,
601-
callee: `_${CREATE_BLOCK}`
593+
returns: customReturn
594+
? {}
595+
: {
596+
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
597+
expressions: [
598+
{
599+
type: NodeTypes.JS_CALL_EXPRESSION,
600+
callee: `_${OPEN_BLOCK}`
601+
},
602+
{
603+
type: NodeTypes.JS_CALL_EXPRESSION,
604+
callee: `_${CREATE_BLOCK}`
605+
}
606+
]
602607
}
603-
]
604-
}
605608
}
606609
]
607610
},
@@ -621,7 +624,10 @@ describe('compiler: v-for', () => {
621624
return {
622625
source: renderListArgs[0] as SimpleExpressionNode,
623626
params: (renderListArgs[1] as any).params,
624-
blockArgs: (renderListArgs[1] as any).returns.expressions[1].arguments
627+
returns: (renderListArgs[1] as any).returns,
628+
blockArgs: customReturn
629+
? null
630+
: (renderListArgs[1] as any).returns.expressions[1].arguments
625631
}
626632
}
627633

@@ -715,17 +721,33 @@ describe('compiler: v-for', () => {
715721
} = parseWithForTransform(
716722
'<template v-for="item in items"><slot/></template>'
717723
)
718-
expect(assertSharedCodegen(codegenNode)).toMatchObject({
724+
expect(
725+
assertSharedCodegen(codegenNode, false, true /* custom return */)
726+
).toMatchObject({
719727
source: { content: `items` },
720728
params: [{ content: `item` }],
721-
blockArgs: [
722-
`_${FRAGMENT}`,
723-
`null`,
724-
{
725-
type: NodeTypes.JS_CALL_EXPRESSION,
726-
callee: `_${RENDER_SLOT}`
727-
}
728-
]
729+
returns: {
730+
type: NodeTypes.JS_CALL_EXPRESSION,
731+
callee: `_${RENDER_SLOT}`
732+
}
733+
})
734+
expect(generate(root).code).toMatchSnapshot()
735+
})
736+
737+
test('v-for on <slot/>', () => {
738+
const {
739+
root,
740+
node: { codegenNode }
741+
} = parseWithForTransform('<slot v-for="item in items"></slot>')
742+
expect(
743+
assertSharedCodegen(codegenNode, false, true /* custom return */)
744+
).toMatchObject({
745+
source: { content: `items` },
746+
params: [{ content: `item` }],
747+
returns: {
748+
type: NodeTypes.JS_CALL_EXPRESSION,
749+
callee: `_${RENDER_SLOT}`
750+
}
729751
})
730752
expect(generate(root).code).toMatchSnapshot()
731753
})
@@ -794,7 +816,7 @@ describe('compiler: v-for', () => {
794816
// should optimize v-if + v-for into a single Fragment block
795817
arguments: [
796818
`_${FRAGMENT}`,
797-
`{ key: 0 }`,
819+
createObjectMatcher({ key: `[0]` }),
798820
{
799821
type: NodeTypes.JS_CALL_EXPRESSION,
800822
callee: `_${RENDER_LIST}`,

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

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,10 @@ describe('compiler: v-if', () => {
316316
assertSharedCodegen(codegenNode)
317317
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
318318
.consequent as CallExpression
319-
expect(branch1.arguments).toMatchObject([`"div"`, `{ key: 0 }`])
319+
expect(branch1.arguments).toMatchObject([
320+
`"div"`,
321+
createObjectMatcher({ key: `[0]` })
322+
])
320323
const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
321324
.alternate as CallExpression
322325
expect(branch2.arguments).toMatchObject([`_${EMPTY}`])
@@ -333,7 +336,7 @@ describe('compiler: v-if', () => {
333336
.consequent as CallExpression
334337
expect(branch1.arguments).toMatchObject([
335338
`_${FRAGMENT}`,
336-
`{ key: 0 }`,
339+
createObjectMatcher({ key: `[0]` }),
337340
[
338341
{ type: NodeTypes.ELEMENT, tag: 'div' },
339342
{ type: NodeTypes.TEXT, content: `hello` },
@@ -351,17 +354,14 @@ describe('compiler: v-if', () => {
351354
root,
352355
node: { codegenNode }
353356
} = parseWithIfTransform(`<template v-if="ok"><slot/></template>`)
354-
assertSharedCodegen(codegenNode)
357+
// assertSharedCodegen(codegenNode)
355358
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
356359
.consequent as CallExpression
357-
expect(branch1.arguments).toMatchObject([
358-
`_${FRAGMENT}`,
359-
`{ key: 0 }`,
360-
{
361-
type: NodeTypes.JS_CALL_EXPRESSION,
362-
callee: `_${RENDER_SLOT}`
363-
}
364-
])
360+
expect(branch1).toMatchObject({
361+
type: NodeTypes.JS_CALL_EXPRESSION,
362+
callee: `_${RENDER_SLOT}`,
363+
arguments: ['$slots.default', createObjectMatcher({ key: `[0]` })]
364+
})
365365
expect(generate(root).code).toMatchSnapshot()
366366
})
367367

@@ -373,10 +373,16 @@ describe('compiler: v-if', () => {
373373
assertSharedCodegen(codegenNode)
374374
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
375375
.consequent as CallExpression
376-
expect(branch1.arguments).toMatchObject([`"div"`, `{ key: 0 }`])
376+
expect(branch1.arguments).toMatchObject([
377+
`"div"`,
378+
createObjectMatcher({ key: `[0]` })
379+
])
377380
const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
378381
.alternate as CallExpression
379-
expect(branch2.arguments).toMatchObject([`"p"`, `{ key: 1 }`])
382+
expect(branch2.arguments).toMatchObject([
383+
`"p"`,
384+
createObjectMatcher({ key: `[1]` })
385+
])
380386
expect(generate(root).code).toMatchSnapshot()
381387
})
382388

@@ -388,12 +394,15 @@ describe('compiler: v-if', () => {
388394
assertSharedCodegen(codegenNode, 1)
389395
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
390396
.consequent as CallExpression
391-
expect(branch1.arguments).toMatchObject([`"div"`, `{ key: 0 }`])
397+
expect(branch1.arguments).toMatchObject([
398+
`"div"`,
399+
createObjectMatcher({ key: `[0]` })
400+
])
392401
const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
393402
.alternate as ConditionalExpression
394403
expect((branch2.consequent as CallExpression).arguments).toMatchObject([
395404
`"p"`,
396-
`{ key: 1 }`
405+
createObjectMatcher({ key: `[1]` })
397406
])
398407
expect(generate(root).code).toMatchSnapshot()
399408
})
@@ -408,16 +417,19 @@ describe('compiler: v-if', () => {
408417
assertSharedCodegen(codegenNode, 1)
409418
const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
410419
.consequent as CallExpression
411-
expect(branch1.arguments).toMatchObject([`"div"`, `{ key: 0 }`])
420+
expect(branch1.arguments).toMatchObject([
421+
`"div"`,
422+
createObjectMatcher({ key: `[0]` })
423+
])
412424
const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
413425
.alternate as ConditionalExpression
414426
expect((branch2.consequent as CallExpression).arguments).toMatchObject([
415427
`"p"`,
416-
`{ key: 1 }`
428+
createObjectMatcher({ key: `[1]` })
417429
])
418430
expect((branch2.alternate as CallExpression).arguments).toMatchObject([
419431
`_${FRAGMENT}`,
420-
`{ key: 2 }`,
432+
createObjectMatcher({ key: `[2]` }),
421433
[
422434
{
423435
type: NodeTypes.TEXT,
@@ -437,7 +449,7 @@ describe('compiler: v-if', () => {
437449
expect(branch1.arguments[1]).toMatchObject({
438450
type: NodeTypes.JS_CALL_EXPRESSION,
439451
callee: `_${MERGE_PROPS}`,
440-
arguments: [`{ key: 0 }`, { content: `obj` }]
452+
arguments: [createObjectMatcher({ key: `[0]` }), { content: `obj` }]
441453
})
442454
})
443455

@@ -470,7 +482,7 @@ describe('compiler: v-if', () => {
470482
type: NodeTypes.JS_CALL_EXPRESSION,
471483
callee: `_${MERGE_PROPS}`,
472484
arguments: [
473-
`{ key: 0 }`,
485+
createObjectMatcher({ key: `[0]` }),
474486
{ content: `obj` },
475487
createObjectMatcher({
476488
id: 'foo'
@@ -487,9 +499,11 @@ describe('compiler: v-if', () => {
487499
.consequent as CallExpression
488500
expect(branch1.callee).toBe(`_${APPLY_DIRECTIVES}`)
489501
const realBranch = branch1.arguments[0] as CallExpression
490-
expect(realBranch.arguments[1]).toBe(`{ key: 0 }`)
502+
expect(realBranch.arguments[1]).toMatchObject(
503+
createObjectMatcher({ key: `[0]` })
504+
)
491505
})
492506

493-
test('with comments', () => {})
507+
test.todo('with comments')
494508
})
495509
})

packages/compiler-core/src/transform.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { isString, isArray } from '@vue/shared'
1616
import { CompilerError, defaultOnError } from './errors'
1717
import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
18-
import { isVSlot, createBlockExpression } from './utils'
18+
import { isVSlot, createBlockExpression, isSlotOutlet } from './utils'
1919

2020
// There are two types of transforms:
2121
//
@@ -192,22 +192,20 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
192192
const { children } = root
193193
if (children.length === 1) {
194194
const child = children[0]
195-
if (child.type === NodeTypes.ELEMENT && child.codegenNode) {
196-
// only child is a <slot/> - it needs to be in a fragment block.
197-
if (child.tagType === ElementTypes.SLOT) {
198-
root.codegenNode = createBlockExpression(
199-
[helper(FRAGMENT), `null`, child.codegenNode!],
200-
context
201-
)
202-
} else {
203-
// turn root element into a block
204-
root.codegenNode = createBlockExpression(
205-
child.codegenNode!.arguments,
206-
context
207-
)
208-
}
195+
if (
196+
child.type === NodeTypes.ELEMENT &&
197+
!isSlotOutlet(child) &&
198+
child.codegenNode
199+
) {
200+
// turn root element into a block
201+
root.codegenNode = createBlockExpression(
202+
child.codegenNode!.arguments,
203+
context
204+
)
209205
} else {
210-
// IfNode, ForNode, TextNodes or transform calls without transformElement.
206+
// - single <slot/>, IfNode, ForNode: already blocks.
207+
// - single text node: always patched.
208+
// - transform calls without transformElement (only during tests)
211209
// Just generate the node as-is
212210
root.codegenNode = child
213211
}

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

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,9 @@ export const transformElement: NodeTransform = (node, context) => {
101101
if (hasDynamicTextChild) {
102102
patchFlag |= PatchFlags.TEXT
103103
}
104-
// pass directly if the only child is one of:
105-
// - text (plain / interpolation / expression)
106-
// - <slot> outlet (already an array)
107-
if (
108-
type === NodeTypes.TEXT ||
109-
hasDynamicTextChild ||
110-
(type === NodeTypes.ELEMENT &&
111-
(child as ElementNode).tagType === ElementTypes.SLOT)
112-
) {
104+
// pass directly if the only child is a text node
105+
// (plain / interpolation / expression)
106+
if (hasDynamicTextChild || type === NodeTypes.TEXT) {
113107
args.push(child)
114108
} else {
115109
args.push(node.children)
@@ -171,7 +165,7 @@ export const transformElement: NodeTransform = (node, context) => {
171165
}
172166
}
173167

174-
type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
168+
export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
175169

176170
export function buildProps(
177171
props: ElementNode['props'],

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import { NodeTransform } from '../transform'
22
import {
33
NodeTypes,
4-
ElementTypes,
54
CompoundExpressionNode,
65
createCompoundExpression,
76
CallExpression,
87
createCallExpression
98
} from '../ast'
10-
import { isSimpleIdentifier } from '../utils'
9+
import { isSimpleIdentifier, isSlotOutlet } from '../utils'
1110
import { buildProps } from './transformElement'
1211
import { createCompilerError, ErrorCodes } from '../errors'
1312
import { RENDER_SLOT } from '../runtimeConstants'
1413

1514
export const transformSlotOutlet: NodeTransform = (node, context) => {
16-
if (node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT) {
15+
if (isSlotOutlet(node)) {
1716
const { props, children, loc } = node
1817
const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots`
1918
let slot: string | CompoundExpressionNode = $slots + `.default`

0 commit comments

Comments
 (0)