import { createStructuralDirectiveTransform, TransformContext, traverseNode } from '../transform' import { NodeTypes, ElementTypes, ElementNode, DirectiveNode, IfBranchNode, SimpleExpressionNode, createCallExpression, createConditionalExpression, createSimpleExpression, createObjectProperty, createObjectExpression, IfConditionalExpression, BlockCodegenNode, IfNode, createVNodeCall } from '../ast' import { createCompilerError, ErrorCodes } from '../errors' import { processExpression } from './transformExpression' import { validateBrowserExpression } from '../validateExpression' import { CREATE_BLOCK, FRAGMENT, CREATE_COMMENT, OPEN_BLOCK, TELEPORT } from '../runtimeHelpers' import { injectProp } from '../utils' import { PatchFlags, PatchFlagNames } from '@vue/shared' export const transformIf = createStructuralDirectiveTransform( /^(if|else|else-if)$/, (node, dir, context) => { return processIf(node, dir, context, (ifNode, branch, isRoot) => { // #1587: We need to dynamically increment the key based on the current // node's sibling nodes, since chained v-if/else branches are // rendered at the same depth const siblings = context.parent!.children let i = siblings.indexOf(ifNode) let key = 0 while (i-- >= 0) { const sibling = siblings[i] if (sibling && sibling.type === NodeTypes.IF) { key += sibling.branches.length } } // Exit callback. Complete the codegenNode when all children have been // transformed. return () => { if (isRoot) { ifNode.codegenNode = createCodegenNodeForBranch( branch, key, context ) as IfConditionalExpression } else { // attach this branch's codegen node to the v-if root. let parentCondition = ifNode.codegenNode! while ( parentCondition.alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION ) { parentCondition = parentCondition.alternate } parentCondition.alternate = createCodegenNodeForBranch( branch, key + ifNode.branches.length - 1, context ) } } }) } ) // target-agnostic transform used for both Client and SSR export function processIf( node: ElementNode, dir: DirectiveNode, context: TransformContext, processCodegen?: ( node: IfNode, branch: IfBranchNode, isRoot: boolean ) => (() => void) | undefined ) { if ( dir.name !== 'else' && (!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim()) ) { const loc = dir.exp ? dir.exp.loc : node.loc context.onError( createCompilerError(ErrorCodes.X_V_IF_NO_EXPRESSION, dir.loc) ) dir.exp = createSimpleExpression(`true`, false, loc) } if (!__BROWSER__ && context.prefixIdentifiers && dir.exp) { // dir.exp can only be simple expression because vIf transform is applied // before expression transform. dir.exp = processExpression(dir.exp as SimpleExpressionNode, context) } if (__DEV__ && __BROWSER__ && dir.exp) { validateBrowserExpression(dir.exp as SimpleExpressionNode, context) } if (dir.name === 'if') { const branch = createIfBranch(node, dir) const ifNode: IfNode = { type: NodeTypes.IF, loc: node.loc, branches: [branch] } context.replaceNode(ifNode) if (processCodegen) { return processCodegen(ifNode, branch, true) } } else { // locate the adjacent v-if const siblings = context.parent!.children const comments = [] let i = siblings.indexOf(node) while (i-- >= -1) { const sibling = siblings[i] if (__DEV__ && sibling && sibling.type === NodeTypes.COMMENT) { context.removeNode(sibling) comments.unshift(sibling) continue } if (sibling && sibling.type === NodeTypes.IF) { // move the node to the if node's branches context.removeNode() const branch = createIfBranch(node, dir) if (__DEV__ && comments.length) { branch.children = [...comments, ...branch.children] } sibling.branches.push(branch) const onExit = processCodegen && processCodegen(sibling, branch, false) // since the branch was removed, it will not be traversed. // make sure to traverse here. traverseNode(branch, context) // call on exit if (onExit) onExit() // make sure to reset currentNode after traversal to indicate this // node has been removed. context.currentNode = null } else { context.onError( createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc) ) } break } } } function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode { return { type: NodeTypes.IF_BRANCH, loc: node.loc, condition: dir.name === 'else' ? undefined : dir.exp, children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node] } } function createCodegenNodeForBranch( branch: IfBranchNode, index: number, context: TransformContext ): IfConditionalExpression | BlockCodegenNode { if (branch.condition) { return createConditionalExpression( branch.condition, createChildrenCodegenNode(branch, index, context), // make sure to pass in asBlock: true so that the comment node call // closes the current block. createCallExpression(context.helper(CREATE_COMMENT), [ __DEV__ ? '"v-if"' : '""', 'true' ]) ) as IfConditionalExpression } else { return createChildrenCodegenNode(branch, index, context) } } function createChildrenCodegenNode( branch: IfBranchNode, index: number, context: TransformContext ): BlockCodegenNode { const { helper } = context const keyProperty = createObjectProperty( `key`, createSimpleExpression(index + '', false) ) const { children } = branch const firstChild = children[0] const needFragmentWrapper = children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT if (needFragmentWrapper) { if (children.length === 1 && firstChild.type === NodeTypes.FOR) { // optimize away nested fragments when child is a ForNode const vnodeCall = firstChild.codegenNode! injectProp(vnodeCall, keyProperty, context) return vnodeCall } else { return createVNodeCall( context, helper(FRAGMENT), createObjectExpression([keyProperty]), children, `${PatchFlags.STABLE_FRAGMENT} /* ${ PatchFlagNames[PatchFlags.STABLE_FRAGMENT] } */`, undefined, undefined, true, false, branch.loc ) } } else { const vnodeCall = (firstChild as ElementNode) .codegenNode as BlockCodegenNode // Change createVNode to createBlock. if ( vnodeCall.type === NodeTypes.VNODE_CALL && // component vnodes are always tracked and its children are // compiled into slots so no need to make it a block ((firstChild as ElementNode).tagType !== ElementTypes.COMPONENT || // teleport has component type but isn't always tracked vnodeCall.tag === TELEPORT) ) { vnodeCall.isBlock = true helper(OPEN_BLOCK) helper(CREATE_BLOCK) } // inject branch key injectProp(vnodeCall, keyProperty, context) return vnodeCall } }