]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler): transform component slots
authorEvan You <yyx990803@gmail.com>
Sat, 28 Sep 2019 02:25:32 +0000 (22:25 -0400)
committerEvan You <yyx990803@gmail.com>
Sat, 28 Sep 2019 02:25:32 +0000 (22:25 -0400)
packages/compiler-core/__tests__/transforms/vSlot.spec.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/errors.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/vSlot.ts

index 4d11e9e049ad6830344bdfebb62deccf87f0adce..d7a36196323dea17c7caba351be5d376ea5f0b25 100644 (file)
@@ -3,7 +3,8 @@ import {
   parse,
   transform,
   ElementNode,
-  NodeTypes
+  NodeTypes,
+  generate
 } from '../../src'
 import { transformElement } from '../../src/transforms/transformElement'
 import { transformOn } from '../../src/transforms/vOn'
@@ -320,4 +321,10 @@ describe('compiler: transform slots', () => {
       ]
     })
   })
+
+  test('generate slot', () => {
+    const ast = parseWithSlots(`<Comp><div/></Comp>`)
+    const { code } = generate(ast)
+    console.log(code)
+  })
 })
index 4bfb4beaf4dee6cb017766d505e6dec4c55818d1..d86b325ac3ffe2b0028dc72e2b7573e8edf3e70c 100644 (file)
@@ -27,7 +27,8 @@ export const enum NodeTypes {
   JS_CALL_EXPRESSION,
   JS_OBJECT_EXPRESSION,
   JS_PROPERTY,
-  JS_ARRAY_EXPRESSION
+  JS_ARRAY_EXPRESSION,
+  JS_SLOT_FUNCTION
 }
 
 export const enum ElementTypes {
@@ -157,6 +158,7 @@ export type JSChildNode =
   | ObjectExpression
   | ArrayExpression
   | ExpressionNode
+  | SlotFunctionExpression
 
 export interface CallExpression extends Node {
   type: NodeTypes.JS_CALL_EXPRESSION
@@ -180,6 +182,12 @@ export interface ArrayExpression extends Node {
   elements: Array<string | JSChildNode>
 }
 
+export interface SlotFunctionExpression extends Node {
+  type: NodeTypes.JS_SLOT_FUNCTION
+  params: ExpressionNode | undefined
+  returns: ChildNode[]
+}
+
 export function createArrayExpression(
   elements: ArrayExpression['elements'],
   loc: SourceLocation
@@ -264,3 +272,16 @@ export function createCallExpression(
     arguments: args
   }
 }
+
+export function createFunctionExpression(
+  params: ExpressionNode | undefined,
+  returns: ChildNode[],
+  loc: SourceLocation
+): SlotFunctionExpression {
+  return {
+    type: NodeTypes.JS_SLOT_FUNCTION,
+    params,
+    returns,
+    loc
+  }
+}
index 9405587f9ef56af03f5fa05a3f258378c8138a6a..a2700bbf9fa7a22e96168e5c5a811bdc20f0c438 100644 (file)
@@ -18,7 +18,8 @@ import {
   InterpolationNode,
   CompoundExpressionNode,
   SimpleExpressionNode,
-  ElementTypes
+  ElementTypes,
+  SlotFunctionExpression
 } from './ast'
 import { SourceMapGenerator, RawSourceMap } from 'source-map'
 import {
@@ -364,10 +365,17 @@ function genNode(node: CodegenNode, context: CodegenContext) {
     case NodeTypes.JS_ARRAY_EXPRESSION:
       genArrayExpression(node, context)
       break
+    case NodeTypes.JS_SLOT_FUNCTION:
+      genSlotFunction(node, context)
+      break
     default:
-      /* istanbul ignore next */
-      __DEV__ &&
+      /* istanbul ignore if */
+      if (__DEV__) {
         assert(false, `unhandled codegen node type: ${(node as any).type}`)
+        // make sure we exhaust all possible types
+        const exhaustiveCheck: never = node
+        return exhaustiveCheck
+      }
   }
 }
 
@@ -568,3 +576,14 @@ function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
 function genArrayExpression(node: ArrayExpression, context: CodegenContext) {
   genNodeListAsArray(node.elements, context)
 }
+
+function genSlotFunction(
+  node: SlotFunctionExpression,
+  context: CodegenContext
+) {
+  context.push(`(`, node)
+  if (node.params) genNode(node.params, context)
+  context.push(`) => `)
+  // pre-normalized slots should always return arrays
+  genNodeListAsArray(node.returns, context)
+}
index a3de21fc634cfac0fd5e6eff086804cf207ea0a8..8efb901bc66b8a9e473bc41e154ecb68f24d6530 100644 (file)
@@ -69,6 +69,10 @@ export const enum ErrorCodes {
   X_V_BIND_NO_EXPRESSION,
   X_V_ON_NO_EXPRESSION,
   X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
+  X_NAMED_SLOT_ON_COMPONENT,
+  X_MIXED_SLOT_USAGE,
+  X_DUPLICATE_SLOT_NAMES,
+  X_EXTRANEOUS_NON_SLOT_CHILDREN,
 
   // generic errors
   X_PREFIX_ID_NOT_SUPPORTED,
@@ -133,14 +137,24 @@ export const errorMessages: { [code: number]: string } = {
     'Note that dynamic directive argument connot contain spaces.',
 
   // transform errors
-  [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_FOR_NO_EXPRESSION]: `v-for has no expression`,
-  [ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression`,
-  [ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression`,
-  [ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression`,
-  [ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `unexpected custom directive on <slot> outlet`,
-
+  [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_FOR_NO_EXPRESSION]: `v-for has no expression.`,
+  [ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression.`,
+  [ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
+  [ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression.`,
+  [ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`,
+  [ErrorCodes.X_NAMED_SLOT_ON_COMPONENT]:
+    `Named v-slot on component. ` +
+    `Named slots should use <template v-slot> syntax nested inside the component.`,
+  [ErrorCodes.X_MIXED_SLOT_USAGE]:
+    `Mixed v-slot usage on both the component and nested <template>.` +
+    `The default slot should also use <template> syntax when there are other ` +
+    `named slots to avoid scope ambiguity.`,
+  [ErrorCodes.X_DUPLICATE_SLOT_NAMES]: `Duplicate slot names found. `,
+  [ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN]:
+    `Extraneous children found when component has explicit slots. ` +
+    `These children will be ignored.`,
   // generic errors
   [ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
   [ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`
index 1037854036c6315a9077792108cfe7aef90ae5e5..afbd64b248d5c0c1e5de963505ca54f8a57d6f2a 100644 (file)
@@ -141,6 +141,12 @@ export function buildProps(
       // directives
       isStatic = false
       const { name, arg, exp, loc } = prop
+
+      // skip v-slot - it is handled by its dedicated transform.
+      if (name === 'slot') {
+        continue
+      }
+
       // special case for v-bind and v-on with no argument
       const isBind = name === 'bind'
       if (!arg && (isBind || name === 'on')) {
index 74cac5cae3ba8c7f9cd09674fb1a733816f5e6a2..670371799e0e48b3e9ae12a49d73a6a7a1c3fa32 100644 (file)
@@ -6,22 +6,127 @@ import {
   createCompoundExpression,
   createCallExpression,
   CompoundExpressionNode,
-  CallExpression
+  CallExpression,
+  createObjectProperty,
+  createSimpleExpression,
+  createFunctionExpression,
+  DirectiveNode,
+  ElementTypes,
+  ExpressionNode,
+  Property,
+  ChildNode,
+  SourceLocation
 } from '../ast'
 import { TransformContext } from '../transform'
 import { buildProps } from './transformElement'
 import { createCompilerError, ErrorCodes } from '../errors'
 import { isSimpleIdentifier } from '../utils'
 import { RENDER_SLOT } from '../runtimeConstants'
+import { isString } from '@vue/shared'
+
+const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
+  p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
 
 export function buildSlots(
-  { loc, children }: ElementNode,
+  { props, children, loc }: ElementNode,
   context: TransformContext
 ): ObjectExpression {
-  const slots = createObjectExpression([], loc)
-  // TODO
+  const slots: Property[] = []
+
+  // 1. Check for default slot with slotProps on component itself.
+  //    <Comp v-slot="{ prop }"/>
+  const explicitDefaultSlot = props.find(isVSlot)
+  if (explicitDefaultSlot) {
+    const { arg, exp, loc } = explicitDefaultSlot
+    if (arg) {
+      context.onError(
+        createCompilerError(ErrorCodes.X_NAMED_SLOT_ON_COMPONENT, loc)
+      )
+    }
+    slots.push(buildSlot(`default`, exp, children, loc))
+  }
 
-  return slots
+  // 2. Iterate through children and check for template slots
+  //    <template v-slot:foo="{ prop }">
+  let hasTemplateSlots = false
+  const seenSlotNames = new Set<string>()
+  const nonSlotChildren: ChildNode[] = []
+  for (let i = 0; i < children.length; i++) {
+    const child = children[i]
+    if (
+      child.type === NodeTypes.ELEMENT &&
+      child.tagType === ElementTypes.TEMPLATE
+    ) {
+      const { props, children, loc: nodeLoc } = child
+      const slotDir = props.find(isVSlot)
+      if (slotDir) {
+        hasTemplateSlots = true
+        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)
+          )
+          break
+        } else {
+          // check duplicate slot names
+          if (
+            !slotName ||
+            (slotName.type === NodeTypes.SIMPLE_EXPRESSION && slotName.isStatic)
+          ) {
+            const name = slotName ? slotName.content : `default`
+            if (seenSlotNames.has(name)) {
+              context.onError(
+                createCompilerError(ErrorCodes.X_DUPLICATE_SLOT_NAMES, dirLoc)
+              )
+              continue
+            }
+            seenSlotNames.add(name)
+          }
+          slots.push(
+            buildSlot(slotName || `default`, slotProps, children, nodeLoc)
+          )
+        }
+      } else {
+        nonSlotChildren.push(child)
+      }
+    } else {
+      nonSlotChildren.push(child)
+    }
+  }
+
+  if (hasTemplateSlots && nonSlotChildren.length) {
+    context.onError(
+      createCompilerError(
+        ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN,
+        nonSlotChildren[0].loc
+      )
+    )
+  }
+
+  if (!explicitDefaultSlot && !hasTemplateSlots) {
+    // implicit default slot.
+    slots.push(buildSlot(`default`, undefined, children, loc))
+  }
+
+  return createObjectExpression(slots, loc)
+}
+
+function buildSlot(
+  name: string | ExpressionNode,
+  slotProps: ExpressionNode | undefined,
+  children: ChildNode[],
+  loc: SourceLocation
+): Property {
+  return createObjectProperty(
+    isString(name) ? createSimpleExpression(name, true, loc) : name,
+    createFunctionExpression(
+      slotProps,
+      children,
+      children.length ? children[0].loc : loc
+    ),
+    loc
+  )
 }
 
 export function buildSlotOutlet(node: ElementNode, context: TransformContext) {
@@ -84,7 +189,10 @@ export function buildSlotOutlet(node: ElementNode, context: TransformContext) {
     )
     if (directives.length) {
       context.onError(
-        createCompilerError(ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET)
+        createCompilerError(
+          ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
+          directives[0].loc
+        )
       )
     }
     slotArgs.push(propsExpression)