]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler): handle conditional v-slot
authorEvan You <yyx990803@gmail.com>
Wed, 2 Oct 2019 21:18:11 +0000 (17:18 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 2 Oct 2019 21:18:11 +0000 (17:18 -0400)
packages/compiler-core/__tests__/transforms/vIf.spec.ts
packages/compiler-core/src/errors.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/vFor.ts
packages/compiler-core/src/transforms/vIf.ts
packages/compiler-core/src/transforms/vSlot.ts
packages/compiler-core/src/utils.ts
packages/runtime-core/src/componentSlots.ts

index 075451526d785dbad2a031f70ecb80058cbbb73a..1010e914139935aee38c09842f345142658ebeed 100644 (file)
@@ -233,7 +233,7 @@ describe('compiler: v-if', () => {
       })
       expect(onError.mock.calls[0]).toMatchObject([
         {
-          code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
+          code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
           loc: node1.loc
         }
       ])
@@ -245,7 +245,7 @@ describe('compiler: v-if', () => {
       )
       expect(onError.mock.calls[1]).toMatchObject([
         {
-          code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
+          code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
           loc: node2.loc
         }
       ])
@@ -257,7 +257,7 @@ describe('compiler: v-if', () => {
       )
       expect(onError.mock.calls[2]).toMatchObject([
         {
-          code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
+          code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
           loc: node3.loc
         }
       ])
index 4ec54b74419ed10a64b3888666f746aaa2effd81..e64183dd3941035a60a66f219aaff6aed5a16c45 100644 (file)
@@ -63,7 +63,6 @@ export const enum ErrorCodes {
 
   // transform errors
   X_IF_NO_EXPRESSION,
-  X_ELSE_IF_NO_ADJACENT_IF,
   X_ELSE_NO_ADJACENT_IF,
   X_FOR_NO_EXPRESSION,
   X_FOR_MALFORMED_EXPRESSION,
@@ -140,8 +139,7 @@ export const errorMessages: { [code: number]: string } = {
 
   // transform errors
   [ErrorCodes.X_IF_NO_EXPRESSION]: `v-if/v-else-if is missing expression.`,
-  [ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF]: `v-else-if has no adjacent v-if.`,
-  [ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else has no adjacent v-if.`,
+  [ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else/v-else-if has no adjacent v-if.`,
   [ErrorCodes.X_FOR_NO_EXPRESSION]: `v-for is missing expression.`,
   [ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression.`,
   [ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
index 7931f019b3e68554a00275c6d87b4c1653aed818..d5585dd8a294f7e4100c05ec001aa13de9636fbc 100644 (file)
@@ -16,6 +16,7 @@ import { isString, isArray } from '@vue/shared'
 import { CompilerError, defaultOnError } from './errors'
 import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
 import { createBlockExpression } from './utils'
+import { isVSlot } from './transforms/vSlot'
 
 // There are two types of transforms:
 //
@@ -311,6 +312,11 @@ export function createStructuralDirectiveTransform(
   return (node, context) => {
     if (node.type === NodeTypes.ELEMENT) {
       const { props } = node
+      // structural directive transforms are not concerned with slots
+      // as they are handled separately in vSlot.ts
+      if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
+        return
+      }
       const exitFns = []
       for (let i = 0; i < props.length; i++) {
         const prop = props[i]
index 121e0fbc368d3c4316fceeee84e18dcadb02bcea..0f1c051dc3c84203c4a21c3d9b45f0a7fb398f4b 100644 (file)
@@ -87,9 +87,9 @@ export const transformElement: NodeTransform = (node, context) => {
             args.push(`null`)
           }
           if (isComponent) {
-            const { slots, hasDynamicSlotName } = buildSlots(node, context)
+            const { slots, hasDynamicSlots } = buildSlots(node, context)
             args.push(slots)
-            if (hasDynamicSlotName) {
+            if (hasDynamicSlots) {
               patchFlag |= PatchFlags.DYNAMIC_SLOTS
             }
           } else if (node.children.length === 1) {
index 2a8a6ff24a4f4a3b8eddaf9f4d6a2fc2222928f5..8aec71ec6dcbb08df1019ddf28d4366cc2c0602a 100644 (file)
@@ -47,7 +47,7 @@ export const transformFor = createStructuralDirectiveTransform(
         // create the loop render function expression now, and add the
         // iterator on exit after all children have been traversed
         const renderExp = createCallExpression(helper(RENDER_LIST), [source])
-        const keyProp = findProp(node.props, `key`)
+        const keyProp = findProp(node, `key`)
         const fragmentFlag = keyProp
           ? PatchFlags.KEYED_FRAGMENT
           : PatchFlags.UNKEYED_FRAGMENT
index 24760e472bcfcd48d22bd672f425239e7c8377d0..23276d37e21288e116686808b5d313c39eccf7d1 100644 (file)
@@ -114,12 +114,7 @@ export const transformIf = createStructuralDirectiveTransform(
           }
         } else {
           context.onError(
-            createCompilerError(
-              dir.name === 'else'
-                ? ErrorCodes.X_ELSE_NO_ADJACENT_IF
-                : ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
-              node.loc
-            )
+            createCompilerError(ErrorCodes.X_ELSE_NO_ADJACENT_IF, node.loc)
           )
         }
         break
index 989ccce77d10d223103acb287349fecefd11a668..4f6e0683a7af640630b0e5a29c429e0b20979cb0 100644 (file)
@@ -11,15 +11,24 @@ import {
   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 }
@@ -46,10 +55,10 @@ export function buildSlots(
   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 }"/>
@@ -61,7 +70,7 @@ export function buildSlots(
         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
@@ -70,45 +79,127 @@ export function buildSlots(
   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))
     }
   }
 
@@ -123,23 +214,22 @@ export function buildSlots(
 
   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,
index 23eabba02e89f882ccc28799c3bbc2f47061a073..252c4101665e36ee571ddc8338dfca1dd2bf3839 100644 (file)
@@ -6,12 +6,17 @@ import {
   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
@@ -106,12 +111,28 @@ export function assert(condition: boolean, msg?: string) {
   }
 }
 
+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
@@ -137,3 +158,18 @@ export function createBlockExpression(
     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)
+}
index fb972c1b5e4e5169c00b212fab4a425e66b06cbb..67858ee33961e537894476bdbc230da24fcc7bde 100644 (file)
@@ -51,7 +51,7 @@ export function resolveSlots(
         let value = (children as RawSlots)[key]
         if (isFunction(value)) {
           ;(slots as any)[key] = normalizeSlot(key, value)
-        } else {
+        } else if (value != null) {
           if (__DEV__) {
             warn(
               `Non-function value encountered for slot "${key}". ` +