import { ElementNode, ObjectExpression, createObjectExpression, NodeTypes, createObjectProperty, createSimpleExpression, createFunctionExpression, DirectiveNode, ElementTypes, ExpressionNode, Property, TemplateChildNode, SourceLocation, createConditionalExpression, ConditionalExpression, SimpleExpressionNode, FunctionExpression, CallExpression, createCallExpression, createArrayExpression, SlotsExpression } from '../ast' import { TransformContext, NodeTransform } from '../transform' import { createCompilerError, ErrorCodes } from '../errors' import { findDir, isTemplateNode, assert, isVSlot, hasScopeRef, isStaticExp } from '../utils' import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers' import { parseForExpression, createForLoopParams } from './vFor' import { SlotFlags, slotFlagsText } from '@vue/shared' const defaultFallback = createSimpleExpression(`undefined`, false) // A NodeTransform that: // 1. Tracks scope identifiers for scoped slots so that they don't get prefixed // by transformExpression. This is only applied in non-browser builds with // { prefixIdentifiers: true }. // 2. Track v-slot depths so that we know a slot is inside another slot. // Note the exit callback is executed before buildSlots() on the same node, // so only nested slots see positive numbers. export const trackSlotScopes: NodeTransform = (node, context) => { if ( node.type === NodeTypes.ELEMENT && (node.tagType === ElementTypes.COMPONENT || node.tagType === ElementTypes.TEMPLATE) ) { // We are only checking non-empty v-slot here // since we only care about slots that introduce scope variables. const vSlot = findDir(node, 'slot') if (vSlot) { const slotProps = vSlot.exp if (!__BROWSER__ && context.prefixIdentifiers) { slotProps && context.addIdentifiers(slotProps) } context.scopes.vSlot++ return () => { if (!__BROWSER__ && context.prefixIdentifiers) { slotProps && context.removeIdentifiers(slotProps) } context.scopes.vSlot-- } } } } // A NodeTransform that tracks scope identifiers for scoped slots with v-for. // This transform is only applied in non-browser builds with { prefixIdentifiers: true } export const trackVForSlotScopes: NodeTransform = (node, context) => { let vFor if ( isTemplateNode(node) && node.props.some(isVSlot) && (vFor = findDir(node, 'for')) ) { const result = (vFor.parseResult = parseForExpression( vFor.exp as SimpleExpressionNode, context )) if (result) { const { value, key, index } = result const { addIdentifiers, removeIdentifiers } = context value && addIdentifiers(value) key && addIdentifiers(key) index && addIdentifiers(index) return () => { value && removeIdentifiers(value) key && removeIdentifiers(key) index && removeIdentifiers(index) } } } } export type SlotFnBuilder = ( slotProps: ExpressionNode | undefined, slotChildren: TemplateChildNode[], loc: SourceLocation ) => FunctionExpression const buildClientSlotFn: SlotFnBuilder = (props, children, loc) => createFunctionExpression( props, children, false /* newline */, true /* isSlot */, children.length ? children[0].loc : loc ) // Instead of being a DirectiveTransform, v-slot processing is called during // transformElement to build the slots object for a component. export function buildSlots( node: ElementNode, context: TransformContext, buildSlotFn: SlotFnBuilder = buildClientSlotFn ): { slots: SlotsExpression hasDynamicSlots: boolean } { context.helper(WITH_CTX) const { children, loc } = node const slotsProperties: Property[] = [] const dynamicSlots: (ConditionalExpression | CallExpression)[] = [] const buildDefaultSlotProperty = ( props: ExpressionNode | undefined, children: TemplateChildNode[] ) => createObjectProperty(`default`, buildSlotFn(props, children, loc)) // If the slot is inside a v-for or another v-slot, force it to be dynamic // since it likely uses a scope variable. let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0 // with `prefixIdentifiers: true`, this can be further optimized to make // it dynamic only when the slot actually uses the scope variables. if (!__BROWSER__ && !context.ssr && context.prefixIdentifiers) { hasDynamicSlots = hasScopeRef(node, context.identifiers) } // 1. Check for slot with slotProps on component itself. // <Comp v-slot="{ prop }"/> const onComponentSlot = findDir(node, 'slot', true) if (onComponentSlot) { const { arg, exp } = onComponentSlot if (arg && !isStaticExp(arg)) { hasDynamicSlots = true } slotsProperties.push( createObjectProperty( arg || createSimpleExpression('default', true), buildSlotFn(exp, children, loc) ) ) } // 2. Iterate through children and check for template slots // <template v-slot:foo="{ prop }"> let hasTemplateSlots = false 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 if ( !isTemplateNode(slotElement) || !(slotDir = findDir(slotElement, 'slot', true)) ) { // not a <template v-slot>, skip. if (slotElement.type !== NodeTypes.COMMENT) { implicitDefaultChildren.push(slotElement) } continue } if (onComponentSlot) { // already has on-component slot - this is incorrect usage. context.onError( createCompilerError(ErrorCodes.X_V_SLOT_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: string | undefined if (isStaticExp(slotName)) { staticSlotName = slotName ? slotName.content : `default` } else { hasDynamicSlots = true } const slotFunction = buildSlotFn(slotProps, slotChildren, slotLoc) // check if this slot is conditional (v-if/v-for) let vIf: DirectiveNode | undefined let vElse: DirectiveNode | undefined let vFor: DirectiveNode | undefined if ((vIf = findDir(slotElement, 'if'))) { hasDynamicSlots = true dynamicSlots.push( createConditionalExpression( vIf.exp!, buildDynamicSlot(slotName, slotFunction), defaultFallback ) ) } else if ( (vElse = findDir(slotElement, /^else(-if)?$/, true /* allowEmpty */)) ) { // find adjacent v-if let j = i let prev while (j--) { prev = children[j] if (prev.type !== NodeTypes.COMMENT) { break } } if (prev && isTemplateNode(prev) && findDir(prev, 'if')) { // remove node children.splice(i, 1) i-- __TEST__ && assert(dynamicSlots.length > 0) // attach this slot to previous conditional let conditional = dynamicSlots[ dynamicSlots.length - 1 ] as ConditionalExpression while ( conditional.alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION ) { conditional = conditional.alternate } conditional.alternate = vElse.exp ? createConditionalExpression( vElse.exp, buildDynamicSlot(slotName, slotFunction), defaultFallback ) : buildDynamicSlot(slotName, slotFunction) } else { context.onError( createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc) ) } } else if ((vFor = findDir(slotElement, 'for'))) { hasDynamicSlots = true const parseResult = vFor.parseResult || parseForExpression(vFor.exp as SimpleExpressionNode, context) if (parseResult) { // Render the dynamic slots as an array and add it to the createSlot() // args. The runtime knows how to handle it appropriately. dynamicSlots.push( createCallExpression(context.helper(RENDER_LIST), [ parseResult.source, createFunctionExpression( createForLoopParams(parseResult), buildDynamicSlot(slotName, slotFunction), true /* force newline */ ) ]) ) } else { context.onError( createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, vFor.loc) ) } } else { // check duplicate static names if (staticSlotName) { if (seenSlotNames.has(staticSlotName)) { context.onError( createCompilerError( ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES, dirLoc ) ) continue } seenSlotNames.add(staticSlotName) if (staticSlotName === 'default') { hasNamedDefaultSlot = true } } slotsProperties.push(createObjectProperty(slotName, slotFunction)) } } if (!onComponentSlot) { if (!hasTemplateSlots) { // implicit default slot (on component) slotsProperties.push(buildDefaultSlotProperty(undefined, children)) } 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( buildDefaultSlotProperty(undefined, implicitDefaultChildren) ) } } } const slotFlag = hasDynamicSlots ? SlotFlags.DYNAMIC : hasForwardedSlots(node.children) ? SlotFlags.FORWARDED : SlotFlags.STABLE let slots = createObjectExpression( slotsProperties.concat( createObjectProperty( `_`, // 2 = compiled but dynamic = can skip normalization, but must run diff // 1 = compiled and static = can skip normalization AND diff as optimized createSimpleExpression( slotFlag + (__DEV__ ? ` /* ${slotFlagsText[slotFlag]} */` : ``), false ) ) ), loc ) as SlotsExpression if (dynamicSlots.length) { slots = createCallExpression(context.helper(CREATE_SLOTS), [ slots, createArrayExpression(dynamicSlots) ]) as SlotsExpression } return { slots, hasDynamicSlots } } function buildDynamicSlot( name: ExpressionNode, fn: FunctionExpression ): ObjectExpression { return createObjectExpression([ createObjectProperty(`name`, name), createObjectProperty(`fn`, fn) ]) } function hasForwardedSlots(children: TemplateChildNode[]): boolean { for (let i = 0; i < children.length; i++) { const child = children[i] switch (child.type) { case NodeTypes.ELEMENT: if ( child.tagType === ElementTypes.SLOT || (child.tagType === ElementTypes.ELEMENT && hasForwardedSlots(child.children)) ) { return true } break case NodeTypes.IF: if (hasForwardedSlots(child.branches)) return true break case NodeTypes.IF_BRANCH: case NodeTypes.FOR: if (hasForwardedSlots(child.children)) return true break default: break } } return false }