feat(compiler): render <slot/> as block fragments
This commit is contained in:
@@ -15,7 +15,7 @@ import {
|
||||
import { isString, isArray } from '@vue/shared'
|
||||
import { CompilerError, defaultOnError } from './errors'
|
||||
import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
|
||||
import { isVSlot, createBlockExpression } from './utils'
|
||||
import { isVSlot, createBlockExpression, isSlotOutlet } from './utils'
|
||||
|
||||
// There are two types of transforms:
|
||||
//
|
||||
@@ -192,22 +192,20 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
|
||||
const { children } = root
|
||||
if (children.length === 1) {
|
||||
const child = children[0]
|
||||
if (child.type === NodeTypes.ELEMENT && child.codegenNode) {
|
||||
// only child is a <slot/> - it needs to be in a fragment block.
|
||||
if (child.tagType === ElementTypes.SLOT) {
|
||||
root.codegenNode = createBlockExpression(
|
||||
[helper(FRAGMENT), `null`, child.codegenNode!],
|
||||
context
|
||||
)
|
||||
} else {
|
||||
// turn root element into a block
|
||||
root.codegenNode = createBlockExpression(
|
||||
child.codegenNode!.arguments,
|
||||
context
|
||||
)
|
||||
}
|
||||
if (
|
||||
child.type === NodeTypes.ELEMENT &&
|
||||
!isSlotOutlet(child) &&
|
||||
child.codegenNode
|
||||
) {
|
||||
// turn root element into a block
|
||||
root.codegenNode = createBlockExpression(
|
||||
child.codegenNode!.arguments,
|
||||
context
|
||||
)
|
||||
} else {
|
||||
// IfNode, ForNode, TextNodes or transform calls without transformElement.
|
||||
// - single <slot/>, IfNode, ForNode: already blocks.
|
||||
// - single text node: always patched.
|
||||
// - transform calls without transformElement (only during tests)
|
||||
// Just generate the node as-is
|
||||
root.codegenNode = child
|
||||
}
|
||||
|
||||
@@ -101,15 +101,9 @@ export const transformElement: NodeTransform = (node, context) => {
|
||||
if (hasDynamicTextChild) {
|
||||
patchFlag |= PatchFlags.TEXT
|
||||
}
|
||||
// pass directly if the only child is one of:
|
||||
// - text (plain / interpolation / expression)
|
||||
// - <slot> outlet (already an array)
|
||||
if (
|
||||
type === NodeTypes.TEXT ||
|
||||
hasDynamicTextChild ||
|
||||
(type === NodeTypes.ELEMENT &&
|
||||
(child as ElementNode).tagType === ElementTypes.SLOT)
|
||||
) {
|
||||
// pass directly if the only child is a text node
|
||||
// (plain / interpolation / expression)
|
||||
if (hasDynamicTextChild || type === NodeTypes.TEXT) {
|
||||
args.push(child)
|
||||
} else {
|
||||
args.push(node.children)
|
||||
@@ -171,7 +165,7 @@ export const transformElement: NodeTransform = (node, context) => {
|
||||
}
|
||||
}
|
||||
|
||||
type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
|
||||
export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
|
||||
|
||||
export function buildProps(
|
||||
props: ElementNode['props'],
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { NodeTransform } from '../transform'
|
||||
import {
|
||||
NodeTypes,
|
||||
ElementTypes,
|
||||
CompoundExpressionNode,
|
||||
createCompoundExpression,
|
||||
CallExpression,
|
||||
createCallExpression
|
||||
} from '../ast'
|
||||
import { isSimpleIdentifier } from '../utils'
|
||||
import { isSimpleIdentifier, isSlotOutlet } from '../utils'
|
||||
import { buildProps } from './transformElement'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import { RENDER_SLOT } from '../runtimeConstants'
|
||||
|
||||
export const transformSlotOutlet: NodeTransform = (node, context) => {
|
||||
if (node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT) {
|
||||
if (isSlotOutlet(node)) {
|
||||
const { props, children, loc } = node
|
||||
const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots`
|
||||
let slot: string | CompoundExpressionNode = $slots + `.default`
|
||||
|
||||
@@ -12,14 +12,18 @@ import {
|
||||
createCallExpression,
|
||||
createFunctionExpression,
|
||||
ElementTypes,
|
||||
ObjectExpression,
|
||||
createObjectExpression,
|
||||
createObjectProperty,
|
||||
TemplateChildNode,
|
||||
CallExpression
|
||||
createObjectProperty
|
||||
} from '../ast'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import { getInnerRange, findProp, createBlockExpression } from '../utils'
|
||||
import {
|
||||
getInnerRange,
|
||||
findProp,
|
||||
createBlockExpression,
|
||||
isTemplateNode,
|
||||
isSlotOutlet,
|
||||
injectProp
|
||||
} from '../utils'
|
||||
import {
|
||||
RENDER_LIST,
|
||||
OPEN_BLOCK,
|
||||
@@ -28,6 +32,7 @@ import {
|
||||
} from '../runtimeConstants'
|
||||
import { processExpression } from './transformExpression'
|
||||
import { PatchFlags, PatchFlagNames } from '@vue/shared'
|
||||
import { PropsExpression } from './transformElement'
|
||||
|
||||
export const transformFor = createStructuralDirectiveTransform(
|
||||
'for',
|
||||
@@ -91,40 +96,52 @@ export const transformFor = createStructuralDirectiveTransform(
|
||||
|
||||
// finish the codegen now that all children have been traversed
|
||||
let childBlock
|
||||
if (node.tagType === ElementTypes.TEMPLATE) {
|
||||
const isTemplate = isTemplateNode(node)
|
||||
const slotOutlet = isSlotOutlet(node)
|
||||
? node
|
||||
: isTemplate &&
|
||||
node.children.length === 1 &&
|
||||
isSlotOutlet(node.children[0])
|
||||
? node.children[0]
|
||||
: null
|
||||
const keyProperty = keyProp
|
||||
? createObjectProperty(
|
||||
`key`,
|
||||
keyProp.type === NodeTypes.ATTRIBUTE
|
||||
? createSimpleExpression(keyProp.value!.content, true)
|
||||
: keyProp.exp!
|
||||
)
|
||||
: null
|
||||
if (slotOutlet) {
|
||||
// <slot v-for="..."> or <template v-for="..."><slot/></template>
|
||||
childBlock = slotOutlet.codegenNode!
|
||||
if (isTemplate && keyProperty) {
|
||||
// <template v-for="..." :key="..."><slot/></template>
|
||||
// we need to inject the key to the renderSlot() call.
|
||||
const existingProps = childBlock.arguments[1] as
|
||||
| PropsExpression
|
||||
| undefined
|
||||
| 'null'
|
||||
childBlock.arguments[1] = injectProp(
|
||||
existingProps,
|
||||
keyProperty,
|
||||
context
|
||||
)
|
||||
}
|
||||
} else if (isTemplate) {
|
||||
// <template v-for="...">
|
||||
// should genereate a fragment block for each loop
|
||||
let childBlockProps: string | ObjectExpression = `null`
|
||||
if (keyProp) {
|
||||
childBlockProps = createObjectExpression([
|
||||
createObjectProperty(
|
||||
`key`,
|
||||
keyProp.type === NodeTypes.ATTRIBUTE
|
||||
? createSimpleExpression(keyProp.value!.content, true)
|
||||
: keyProp.exp!
|
||||
)
|
||||
])
|
||||
}
|
||||
let childBlockChildren: TemplateChildNode[] | CallExpression =
|
||||
node.children
|
||||
// if the only child is a <slot/>, use it directly as fragment
|
||||
// children since it already returns an array.
|
||||
if (childBlockChildren.length === 1) {
|
||||
const child = childBlockChildren[0]
|
||||
if (
|
||||
child.type === NodeTypes.ELEMENT &&
|
||||
child.tagType === ElementTypes.SLOT
|
||||
) {
|
||||
childBlockChildren = child.codegenNode!
|
||||
}
|
||||
}
|
||||
childBlock = createBlockExpression(
|
||||
[helper(FRAGMENT), childBlockProps, childBlockChildren],
|
||||
[
|
||||
helper(FRAGMENT),
|
||||
keyProperty ? createObjectExpression([keyProperty]) : `null`,
|
||||
node.children
|
||||
],
|
||||
context
|
||||
)
|
||||
} else {
|
||||
// Normal element v-for. Directly use the child's codegenNode arguments,
|
||||
// but replace createVNode() with createBlock()
|
||||
// Normal element v-for. Directly use the child's codegenNode
|
||||
// arguments, but replace createVNode() with createBlock()
|
||||
childBlock = createBlockExpression(
|
||||
node.codegenNode!.arguments,
|
||||
context
|
||||
|
||||
@@ -16,11 +16,8 @@ import {
|
||||
ConditionalExpression,
|
||||
CallExpression,
|
||||
createSimpleExpression,
|
||||
JSChildNode,
|
||||
ObjectExpression,
|
||||
createObjectProperty,
|
||||
Property,
|
||||
ExpressionNode
|
||||
createObjectExpression
|
||||
} from '../ast'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import { processExpression } from './transformExpression'
|
||||
@@ -30,9 +27,10 @@ import {
|
||||
EMPTY,
|
||||
FRAGMENT,
|
||||
APPLY_DIRECTIVES,
|
||||
MERGE_PROPS
|
||||
CREATE_VNODE
|
||||
} from '../runtimeConstants'
|
||||
import { isString } from '@vue/shared'
|
||||
import { injectProp } from '../utils'
|
||||
import { PropsExpression } from './transformElement'
|
||||
|
||||
export const transformIf = createStructuralDirectiveTransform(
|
||||
/^(if|else|else-if)$/,
|
||||
@@ -153,81 +151,52 @@ function createCodegenNodeForBranch(
|
||||
function createChildrenCodegenNode(
|
||||
branch: IfBranchNode,
|
||||
index: number,
|
||||
{ helper }: TransformContext
|
||||
context: TransformContext
|
||||
): CallExpression {
|
||||
const keyExp = `{ key: ${index} }`
|
||||
const { helper } = context
|
||||
const keyProperty = createObjectProperty(
|
||||
`key`,
|
||||
createSimpleExpression(index + '', false)
|
||||
)
|
||||
const { children } = branch
|
||||
const child = children[0]
|
||||
const needFragmentWrapper =
|
||||
children.length !== 1 ||
|
||||
child.type !== NodeTypes.ELEMENT ||
|
||||
child.tagType === ElementTypes.SLOT
|
||||
children.length !== 1 || child.type !== NodeTypes.ELEMENT
|
||||
if (needFragmentWrapper) {
|
||||
const blockArgs: CallExpression['arguments'] = [
|
||||
helper(FRAGMENT),
|
||||
keyExp,
|
||||
createObjectExpression([keyProperty]),
|
||||
children
|
||||
]
|
||||
if (children.length === 1) {
|
||||
if (children.length === 1 && child.type === NodeTypes.FOR) {
|
||||
// optimize away nested fragments when child is a ForNode
|
||||
if (child.type === NodeTypes.FOR) {
|
||||
const forBlockArgs = (child.codegenNode
|
||||
.expressions[1] as CallExpression).arguments
|
||||
// directly use the for block's children and patchFlag
|
||||
blockArgs[2] = forBlockArgs[2]
|
||||
blockArgs[3] = forBlockArgs[3]
|
||||
} else if (
|
||||
child.type === NodeTypes.ELEMENT &&
|
||||
child.tagType === ElementTypes.SLOT
|
||||
) {
|
||||
// <template v-if="..."><slot/></template>
|
||||
// since slot always returns array, use it directly as the fragment children.
|
||||
blockArgs[2] = child.codegenNode!
|
||||
}
|
||||
const forBlockArgs = (child.codegenNode.expressions[1] as CallExpression)
|
||||
.arguments
|
||||
// directly use the for block's children and patchFlag
|
||||
blockArgs[2] = forBlockArgs[2]
|
||||
blockArgs[3] = forBlockArgs[3]
|
||||
}
|
||||
return createCallExpression(helper(CREATE_BLOCK), blockArgs)
|
||||
} else {
|
||||
const childCodegen = (child as ElementNode).codegenNode!
|
||||
let vnodeCall = childCodegen
|
||||
// Element with custom directives. Locate the actual createVNode() call.
|
||||
if (vnodeCall.callee.includes(APPLY_DIRECTIVES)) {
|
||||
vnodeCall = vnodeCall.arguments[0] as CallExpression
|
||||
}
|
||||
// change child to a block
|
||||
vnodeCall.callee = helper(CREATE_BLOCK)
|
||||
// branch key
|
||||
const existingProps = vnodeCall.arguments[1]
|
||||
if (!existingProps || existingProps === `null`) {
|
||||
vnodeCall.arguments[1] = keyExp
|
||||
} else {
|
||||
// inject branch key if not already have a key
|
||||
const props = existingProps as
|
||||
| CallExpression
|
||||
| ObjectExpression
|
||||
| ExpressionNode
|
||||
if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
|
||||
// merged props... add ours
|
||||
// only inject key to object literal if it's the first argument so that
|
||||
// if doesn't override user provided keys
|
||||
const first = props.arguments[0] as string | JSChildNode
|
||||
if (!isString(first) && first.type === NodeTypes.JS_OBJECT_EXPRESSION) {
|
||||
first.properties.unshift(createKeyProperty(index))
|
||||
} else {
|
||||
props.arguments.unshift(keyExp)
|
||||
}
|
||||
} else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
|
||||
props.properties.unshift(createKeyProperty(index))
|
||||
} else {
|
||||
// single v-bind with expression
|
||||
vnodeCall.arguments[1] = createCallExpression(helper(MERGE_PROPS), [
|
||||
keyExp,
|
||||
props
|
||||
])
|
||||
}
|
||||
// Change createVNode to createBlock.
|
||||
// It's possible to have renderSlot() here as well - which already produces
|
||||
// a block, so no need to change the callee. renderSlot() also accepts props
|
||||
// as the 2nd argument, so the key injection logic below works for it too.
|
||||
if (vnodeCall.callee.includes(CREATE_VNODE)) {
|
||||
vnodeCall.callee = helper(CREATE_BLOCK)
|
||||
}
|
||||
// inject branch key
|
||||
const existingProps = vnodeCall.arguments[1] as
|
||||
| PropsExpression
|
||||
| undefined
|
||||
| 'null'
|
||||
vnodeCall.arguments[1] = injectProp(existingProps, keyProperty, context)
|
||||
return childCodegen
|
||||
}
|
||||
}
|
||||
|
||||
function createKeyProperty(index: number): Property {
|
||||
return createObjectProperty(`key`, createSimpleExpression(index + '', false))
|
||||
}
|
||||
|
||||
@@ -10,13 +10,18 @@ import {
|
||||
DirectiveNode,
|
||||
ElementTypes,
|
||||
TemplateChildNode,
|
||||
RootNode
|
||||
RootNode,
|
||||
ObjectExpression,
|
||||
Property,
|
||||
JSChildNode,
|
||||
createObjectExpression
|
||||
} from './ast'
|
||||
import { parse } from 'acorn'
|
||||
import { walk } from 'estree-walker'
|
||||
import { TransformContext } from './transform'
|
||||
import { OPEN_BLOCK, CREATE_BLOCK } from './runtimeConstants'
|
||||
import { OPEN_BLOCK, CREATE_BLOCK, MERGE_PROPS } from './runtimeConstants'
|
||||
import { isString } from '@vue/shared'
|
||||
import { PropsExpression } from './transforms/transformElement'
|
||||
|
||||
// cache node requires
|
||||
// lazy require dependencies so that they don't end up in rollup's dep graph
|
||||
@@ -165,5 +170,40 @@ export const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
|
||||
|
||||
export const isTemplateNode = (
|
||||
node: RootNode | TemplateChildNode
|
||||
): node is ElementNode =>
|
||||
): node is ElementNode & { tagType: ElementTypes.TEMPLATE } =>
|
||||
node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE
|
||||
|
||||
export const isSlotOutlet = (
|
||||
node: RootNode | TemplateChildNode
|
||||
): node is ElementNode & { tagType: ElementTypes.SLOT } =>
|
||||
node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT
|
||||
|
||||
export function injectProp(
|
||||
props: PropsExpression | undefined | 'null',
|
||||
prop: Property,
|
||||
context: TransformContext
|
||||
): ObjectExpression | CallExpression {
|
||||
if (props == null || props === `null`) {
|
||||
return createObjectExpression([prop])
|
||||
} else if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
|
||||
// merged props... add ours
|
||||
// only inject key to object literal if it's the first argument so that
|
||||
// if doesn't override user provided keys
|
||||
const first = props.arguments[0] as string | JSChildNode
|
||||
if (!isString(first) && first.type === NodeTypes.JS_OBJECT_EXPRESSION) {
|
||||
first.properties.unshift(prop)
|
||||
} else {
|
||||
props.arguments.unshift(createObjectExpression([prop]))
|
||||
}
|
||||
return props
|
||||
} else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
|
||||
props.properties.unshift(prop)
|
||||
return props
|
||||
} else {
|
||||
// single v-bind with expression, return a merged replacement
|
||||
return createCallExpression(context.helper(MERGE_PROPS), [
|
||||
createObjectExpression([prop]),
|
||||
props
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user