ExpressionNode,
Property,
TemplateChildNode,
- SourceLocation
+ SourceLocation,
+ createConditionalExpression,
+ ConditionalExpression,
+ JSChildNode,
+ SimpleExpressionNode
} from '../ast'
import { TransformContext, NodeTransform } from '../transform'
import { createCompilerError, ErrorCodes } from '../errors'
-import { isString } from '@vue/shared'
+import { mergeExpressions, findNonEmptyDir } from '../utils'
export const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
+const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
+ p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
+
+const defaultFallback = createSimpleExpression(`undefined`, false)
+
// A NodeTransform that tracks scope identifiers for scoped slots so that they
// don't get prefixed by transformExpression. This transform is only applied
// in non-browser builds with { prefixIdentifiers: true }
context: TransformContext
): {
slots: ObjectExpression
- hasDynamicSlotName: boolean
+ hasDynamicSlots: boolean
} {
const slots: Property[] = []
- let hasDynamicSlotName = false
+ let hasDynamicSlots = false
// 1. Check for default slot with slotProps on component itself.
// <Comp v-slot="{ prop }"/>
createCompilerError(ErrorCodes.X_NAMED_SLOT_ON_COMPONENT, loc)
)
}
- slots.push(buildSlot(`default`, exp, children, loc))
+ slots.push(buildDefaultSlot(exp, children, loc))
}
// 2. Iterate through children and check for template slots
let extraneousChild: TemplateChildNode | undefined = undefined
const seenSlotNames = new Set<string>()
for (let i = 0; i < children.length; i++) {
- const child = children[i]
+ const slotElement = children[i]
let slotDir
+
if (
- child.type === NodeTypes.ELEMENT &&
- child.tagType === ElementTypes.TEMPLATE &&
- (slotDir = child.props.find(isVSlot))
+ slotElement.type !== NodeTypes.ELEMENT ||
+ slotElement.tagType !== ElementTypes.TEMPLATE ||
+ !(slotDir = slotElement.props.find(isVSlot))
) {
- hasTemplateSlots = true
- const { children, loc: nodeLoc } = child
- const { arg: slotName, exp: slotProps, loc: dirLoc } = slotDir
- if (explicitDefaultSlot) {
- // already has on-component default slot - this is incorrect usage.
- context.onError(
- createCompilerError(ErrorCodes.X_MIXED_SLOT_USAGE, dirLoc)
+ // not a <template v-slot>, skip.
+ extraneousChild = extraneousChild || slotElement
+ continue
+ }
+
+ if (explicitDefaultSlot) {
+ // already has on-component default slot - this is incorrect usage.
+ context.onError(
+ createCompilerError(ErrorCodes.X_MIXED_SLOT_USAGE, slotDir.loc)
+ )
+ break
+ }
+
+ hasTemplateSlots = true
+ const { children: slotChildren, loc: slotLoc } = slotElement
+ const {
+ arg: slotName = createSimpleExpression(`default`, true),
+ exp: slotProps,
+ loc: dirLoc
+ } = slotDir
+
+ // check if name is dynamic.
+ let staticSlotName
+ if (isStaticExp(slotName)) {
+ staticSlotName = slotName ? slotName.content : `default`
+ } else {
+ hasDynamicSlots = true
+ }
+
+ const slotFunction = createFunctionExpression(
+ slotProps,
+ slotChildren,
+ false,
+ slotChildren.length ? slotChildren[0].loc : slotLoc
+ )
+
+ // check if this slot is conditional (v-if/else/else-if)
+ let vIf
+ let vElse
+ if ((vIf = findNonEmptyDir(slotElement, 'if'))) {
+ hasDynamicSlots = true
+ slots.push(
+ createObjectProperty(
+ slotName,
+ createConditionalExpression(vIf.exp!, slotFunction, defaultFallback)
)
- break
- } else {
+ )
+ } else if ((vElse = findNonEmptyDir(slotElement, /^else(-if)?$/))) {
+ hasDynamicSlots = true
+ // find adjacent v-if slot
+ let vIfBase
+ let i = slots.length
+ while (i--) {
+ if (slots[i].value.type === NodeTypes.JS_CONDITIONAL_EXPRESSION) {
+ vIfBase = slots[i]
+ break
+ }
+ }
+ if (vIfBase) {
+ // check if the v-else and the base v-if has the same slot name
if (
- !slotName ||
- (slotName.type === NodeTypes.SIMPLE_EXPRESSION && slotName.isStatic)
+ isStaticExp(vIfBase.key) &&
+ vIfBase.key.content === staticSlotName
) {
- // check duplicate slot names
- const name = slotName ? slotName.content : `default`
- if (seenSlotNames.has(name)) {
- context.onError(
- createCompilerError(ErrorCodes.X_DUPLICATE_SLOT_NAMES, dirLoc)
- )
- continue
+ let conditional = vIfBase.value as ConditionalExpression
+ while (
+ conditional.alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
+ ) {
+ conditional = conditional.alternate
}
- seenSlotNames.add(name)
+ conditional.alternate = vElse.exp
+ ? createConditionalExpression(
+ vElse.exp,
+ slotFunction,
+ defaultFallback
+ )
+ : slotFunction
} else {
- hasDynamicSlotName = true
+ // not the same slot name. generate a separate property.
+ slots.push(
+ createObjectProperty(
+ slotName,
+ createConditionalExpression(
+ // negate baseVIf
+ mergeExpressions(
+ `!(`,
+ (vIfBase.value as ConditionalExpression).test,
+ `)`,
+ ...(vElse.exp ? [` && (`, vElse.exp, `)`] : [])
+ ),
+ slotFunction,
+ defaultFallback
+ )
+ )
+ )
}
- slots.push(
- buildSlot(slotName || `default`, slotProps, children, nodeLoc)
+ } else {
+ context.onError(
+ createCompilerError(ErrorCodes.X_ELSE_NO_ADJACENT_IF, vElse.loc)
)
}
- } else if (!extraneousChild) {
- extraneousChild = child
+ } else {
+ // check duplicate static names
+ if (staticSlotName) {
+ if (seenSlotNames.has(staticSlotName)) {
+ context.onError(
+ createCompilerError(ErrorCodes.X_DUPLICATE_SLOT_NAMES, dirLoc)
+ )
+ continue
+ }
+ seenSlotNames.add(staticSlotName)
+ }
+ slots.push(createObjectProperty(slotName, slotFunction))
}
}
if (!explicitDefaultSlot && !hasTemplateSlots) {
// implicit default slot.
- slots.push(buildSlot(`default`, undefined, children, loc))
+ slots.push(buildDefaultSlot(undefined, children, loc))
}
return {
slots: createObjectExpression(slots, loc),
- hasDynamicSlotName
+ hasDynamicSlots
}
}
-function buildSlot(
- name: string | ExpressionNode,
+function buildDefaultSlot(
slotProps: ExpressionNode | undefined,
children: TemplateChildNode[],
loc: SourceLocation
): Property {
return createObjectProperty(
- isString(name) ? createSimpleExpression(name, true, loc) : name,
+ createSimpleExpression(`default`, true),
createFunctionExpression(
slotProps,
children,
CallExpression,
SequenceExpression,
createSequenceExpression,
- createCallExpression
+ createCallExpression,
+ ExpressionNode,
+ CompoundExpressionNode,
+ createCompoundExpression,
+ DirectiveNode
} from './ast'
import { parse } from 'acorn'
import { walk } from 'estree-walker'
import { TransformContext } from './transform'
import { OPEN_BLOCK, CREATE_BLOCK } from './runtimeConstants'
+import { isString } from '@vue/shared'
// cache node requires
// lazy require dependencies so that they don't end up in rollup's dep graph
}
}
+export function findNonEmptyDir(
+ node: ElementNode,
+ name: string | RegExp
+): DirectiveNode | undefined {
+ for (let i = 0; i < node.props.length; i++) {
+ const p = node.props[i]
+ if (
+ p.type === NodeTypes.DIRECTIVE &&
+ p.exp &&
+ (isString(name) ? p.name === name : name.test(p.name))
+ ) {
+ return p
+ }
+ }
+}
+
export function findProp(
- props: ElementNode['props'],
+ node: ElementNode,
name: string
): ElementNode['props'][0] | undefined {
- for (let i = 0; i < props.length; i++) {
- const p = props[i]
+ for (let i = 0; i < node.props.length; i++) {
+ const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) {
if (p.name === name && p.value && !p.value.isEmpty) {
return p
createCallExpression(context.helper(CREATE_BLOCK), args)
])
}
+
+export function mergeExpressions(
+ ...args: (string | ExpressionNode)[]
+): CompoundExpressionNode {
+ const children: CompoundExpressionNode['children'] = []
+ for (let i = 0; i < args.length; i++) {
+ const exp = args[i]
+ if (isString(exp) || exp.type === NodeTypes.SIMPLE_EXPRESSION) {
+ children.push(exp)
+ } else {
+ children.push(...exp.children)
+ }
+ }
+ return createCompoundExpression(children)
+}