]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler): transform slot outlets
authorEvan You <yyx990803@gmail.com>
Sat, 28 Sep 2019 00:29:20 +0000 (20:29 -0400)
committerEvan You <yyx990803@gmail.com>
Sat, 28 Sep 2019 00:29:20 +0000 (20:29 -0400)
packages/compiler-core/__tests__/transforms/vSlot.spec.ts [new file with mode: 0644]
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/errors.ts
packages/compiler-core/src/runtimeConstants.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/vSlot.ts
packages/runtime-core/src/componentSlots.ts
packages/runtime-core/src/helpers/renderSlot.ts [new file with mode: 0644]
packages/runtime-core/src/index.ts

diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
new file mode 100644 (file)
index 0000000..4d11e9e
--- /dev/null
@@ -0,0 +1,323 @@
+import {
+  CompilerOptions,
+  parse,
+  transform,
+  ElementNode,
+  NodeTypes
+} from '../../src'
+import { transformElement } from '../../src/transforms/transformElement'
+import { transformOn } from '../../src/transforms/vOn'
+import { transformBind } from '../../src/transforms/vBind'
+import { transformExpression } from '../../src/transforms/transformExpression'
+import { RENDER_SLOT } from '../../src/runtimeConstants'
+
+function parseWithSlots(template: string, options: CompilerOptions = {}) {
+  const ast = parse(template)
+  transform(ast, {
+    nodeTransforms: [
+      ...(options.prefixIdentifiers ? [transformExpression] : []),
+      // slot transform is part of transformElement
+      transformElement
+    ],
+    directiveTransforms: {
+      on: transformOn,
+      bind: transformBind
+    },
+    ...options
+  })
+  return ast
+}
+
+describe('compiler: transform slots', () => {
+  test('default slot outlet', () => {
+    const ast = parseWithSlots(`<slot/>`)
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${RENDER_SLOT}`,
+      arguments: [`$slots.default`]
+    })
+  })
+
+  test('statically named slot outlet', () => {
+    const ast = parseWithSlots(`<slot name="foo" />`)
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${RENDER_SLOT}`,
+      arguments: [`$slots.foo`]
+    })
+  })
+
+  test('statically named slot outlet w/ name that needs quotes', () => {
+    const ast = parseWithSlots(`<slot name="foo-bar" />`)
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${RENDER_SLOT}`,
+      arguments: [`$slots["foo-bar"]`]
+    })
+  })
+
+  test('dynamically named slot outlet', () => {
+    const ast = parseWithSlots(`<slot :name="foo" />`)
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${RENDER_SLOT}`,
+      arguments: [
+        {
+          type: NodeTypes.COMPOUND_EXPRESSION,
+          children: [
+            `$slots[`,
+            {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: `foo`,
+              isStatic: false
+            },
+            `]`
+          ]
+        }
+      ]
+    })
+  })
+
+  test('dynamically named slot outlet w/ prefixIdentifiers: true', () => {
+    const ast = parseWithSlots(`<slot :name="foo + bar" />`, {
+      prefixIdentifiers: true
+    })
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: RENDER_SLOT,
+      arguments: [
+        {
+          type: NodeTypes.COMPOUND_EXPRESSION,
+          children: [
+            `_ctx.$slots[`,
+            {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: `_ctx.foo`,
+              isStatic: false
+            },
+            ` + `,
+            {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: `_ctx.bar`,
+              isStatic: false
+            },
+            `]`
+          ]
+        }
+      ]
+    })
+  })
+
+  test('default slot outlet with props', () => {
+    const ast = parseWithSlots(`<slot foo="bar" :baz="qux" />`)
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${RENDER_SLOT}`,
+      arguments: [
+        `$slots.default`,
+        {
+          type: NodeTypes.JS_OBJECT_EXPRESSION,
+          properties: [
+            {
+              key: {
+                content: `foo`,
+                isStatic: true
+              },
+              value: {
+                content: `bar`,
+                isStatic: true
+              }
+            },
+            {
+              key: {
+                content: `baz`,
+                isStatic: true
+              },
+              value: {
+                content: `qux`,
+                isStatic: false
+              }
+            }
+          ]
+        }
+      ]
+    })
+  })
+
+  test('statically named slot outlet with props', () => {
+    const ast = parseWithSlots(`<slot name="foo" foo="bar" :baz="qux" />`)
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${RENDER_SLOT}`,
+      arguments: [
+        `$slots.foo`,
+        {
+          type: NodeTypes.JS_OBJECT_EXPRESSION,
+          // props should not include name
+          properties: [
+            {
+              key: {
+                content: `foo`,
+                isStatic: true
+              },
+              value: {
+                content: `bar`,
+                isStatic: true
+              }
+            },
+            {
+              key: {
+                content: `baz`,
+                isStatic: true
+              },
+              value: {
+                content: `qux`,
+                isStatic: false
+              }
+            }
+          ]
+        }
+      ]
+    })
+  })
+
+  test('dynamically named slot outlet with props', () => {
+    const ast = parseWithSlots(`<slot :name="foo" foo="bar" :baz="qux" />`)
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${RENDER_SLOT}`,
+      arguments: [
+        {
+          type: NodeTypes.COMPOUND_EXPRESSION,
+          children: [`$slots[`, { content: `foo` }, `]`]
+        },
+        {
+          type: NodeTypes.JS_OBJECT_EXPRESSION,
+          // props should not include name
+          properties: [
+            {
+              key: {
+                content: `foo`,
+                isStatic: true
+              },
+              value: {
+                content: `bar`,
+                isStatic: true
+              }
+            },
+            {
+              key: {
+                content: `baz`,
+                isStatic: true
+              },
+              value: {
+                content: `qux`,
+                isStatic: false
+              }
+            }
+          ]
+        }
+      ]
+    })
+  })
+
+  test('default slot outlet with fallback', () => {
+    const ast = parseWithSlots(`<slot><div/></slot>`)
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${RENDER_SLOT}`,
+      arguments: [
+        `$slots.default`,
+        `{}`,
+        [
+          {
+            type: NodeTypes.ELEMENT,
+            tag: `div`
+          }
+        ]
+      ]
+    })
+  })
+
+  test('named slot outlet with fallback', () => {
+    const ast = parseWithSlots(`<slot name="foo"><div/></slot>`)
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${RENDER_SLOT}`,
+      arguments: [
+        `$slots.foo`,
+        `{}`,
+        [
+          {
+            type: NodeTypes.ELEMENT,
+            tag: `div`
+          }
+        ]
+      ]
+    })
+  })
+
+  test('default slot outlet with props & fallback', () => {
+    const ast = parseWithSlots(`<slot :foo="bar"><div/></slot>`)
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${RENDER_SLOT}`,
+      arguments: [
+        `$slots.default`,
+        {
+          type: NodeTypes.JS_OBJECT_EXPRESSION,
+          properties: [
+            {
+              key: {
+                content: `foo`,
+                isStatic: true
+              },
+              value: {
+                content: `bar`,
+                isStatic: false
+              }
+            }
+          ]
+        },
+        [
+          {
+            type: NodeTypes.ELEMENT,
+            tag: `div`
+          }
+        ]
+      ]
+    })
+  })
+
+  test('named slot outlet with props & fallback', () => {
+    const ast = parseWithSlots(`<slot name="foo" :foo="bar"><div/></slot>`)
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${RENDER_SLOT}`,
+      arguments: [
+        `$slots.foo`,
+        {
+          type: NodeTypes.JS_OBJECT_EXPRESSION,
+          properties: [
+            {
+              key: {
+                content: `foo`,
+                isStatic: true
+              },
+              value: {
+                content: `bar`,
+                isStatic: false
+              }
+            }
+          ]
+        },
+        [
+          {
+            type: NodeTypes.ELEMENT,
+            tag: `div`
+          }
+        ]
+      ]
+    })
+  })
+})
index 90817339e51e966951327d5c976e8a252cf802e0..4bfb4beaf4dee6cb017766d505e6dec4c55818d1 100644 (file)
@@ -160,8 +160,8 @@ export type JSChildNode =
 
 export interface CallExpression extends Node {
   type: NodeTypes.JS_CALL_EXPRESSION
-  callee: string // can only be imported runtime helpers, so no source location
-  arguments: Array<string | JSChildNode | ChildNode[]>
+  callee: string | ExpressionNode
+  arguments: (string | JSChildNode | ChildNode[])[]
 }
 
 export interface ObjectExpression extends Node {
@@ -253,7 +253,7 @@ export function createCompoundExpression(
 }
 
 export function createCallExpression(
-  callee: string,
+  callee: string | ExpressionNode,
   args: CallExpression['arguments'],
   loc: SourceLocation
 ): CallExpression {
index 6dc09119fb5c0debc50d32a56dde11b2cf74346c..9405587f9ef56af03f5fa05a3f258378c8138a6a 100644 (file)
@@ -17,7 +17,8 @@ import {
   Position,
   InterpolationNode,
   CompoundExpressionNode,
-  SimpleExpressionNode
+  SimpleExpressionNode,
+  ElementTypes
 } from './ast'
 import { SourceMapGenerator, RawSourceMap } from 'source-map'
 import {
@@ -262,7 +263,10 @@ function genHoists(hoists: JSChildNode[], context: CodegenContext) {
 
 // This will generate a single vnode call if:
 // - The target position explicitly allows a single node (root, if, for)
-// - The list has length === 1, AND The only child is a text, expression or comment.
+// - The list has length === 1, AND The only child is a:
+//   - text
+//   - expression
+//   - <slot> outlet, which always produces an array
 function genChildren(
   children: ChildNode[],
   context: CodegenContext,
@@ -272,12 +276,14 @@ function genChildren(
     return context.push(`null`)
   }
   const child = children[0]
+  const type = child.type
   if (
     children.length === 1 &&
     (allowSingle ||
-      child.type === NodeTypes.TEXT ||
-      child.type === NodeTypes.INTERPOLATION ||
-      child.type === NodeTypes.COMMENT)
+      type === NodeTypes.TEXT ||
+      type === NodeTypes.INTERPOLATION ||
+      (type === NodeTypes.ELEMENT &&
+        (child as ElementNode).tagType === ElementTypes.SLOT))
   ) {
     genNode(child, context)
   } else {
@@ -523,7 +529,12 @@ function genCallExpression(
   context: CodegenContext,
   multilines = node.arguments.length > 2
 ) {
-  context.push(node.callee + `(`, node, true)
+  if (isString(node.callee)) {
+    context.push(node.callee + `(`, node, true)
+  } else {
+    genNode(node.callee, context)
+    context.push(`(`)
+  }
   multilines && context.indent()
   genNodeList(node.arguments, context, multilines)
   multilines && context.deindent()
index 28a982c75b50b56a237b3f6e8dea88a7e03370cf..a3de21fc634cfac0fd5e6eff086804cf207ea0a8 100644 (file)
@@ -68,6 +68,7 @@ export const enum ErrorCodes {
   X_FOR_MALFORMED_EXPRESSION,
   X_V_BIND_NO_EXPRESSION,
   X_V_ON_NO_EXPRESSION,
+  X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
 
   // generic errors
   X_PREFIX_ID_NOT_SUPPORTED,
@@ -138,6 +139,7 @@ export const errorMessages: { [code: number]: string } = {
   [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`,
 
   // generic errors
   [ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
index 6510963f7ffdab34e02541845e3b63e71234c2e6..79e3d09b1e86efc888bd7e2a70efc6aa23bdad59 100644 (file)
@@ -10,6 +10,7 @@ export const RESOLVE_COMPONENT = `resolveComponent`
 export const RESOLVE_DIRECTIVE = `resolveDirective`
 export const APPLY_DIRECTIVES = `applyDirectives`
 export const RENDER_LIST = `renderList`
+export const RENDER_SLOT = `renderSlot`
 export const TO_STRING = `toString`
 export const MERGE_PROPS = `mergeProps`
 export const TO_HANDLERS = `toHandlers`
index 9024534236f2c3128e3a2a985e3aaad18f38588c..1037854036c6315a9077792108cfe7aef90ae5e5 100644 (file)
@@ -13,7 +13,8 @@ import {
   createObjectProperty,
   createSimpleExpression,
   createObjectExpression,
-  Property
+  Property,
+  SourceLocation
 } from '../ast'
 import { isArray } from '@vue/shared'
 import { createCompilerError, ErrorCodes } from '../errors'
@@ -26,6 +27,7 @@ import {
   TO_HANDLERS
 } from '../runtimeConstants'
 import { getInnerRange } from '../utils'
+import { buildSlotOutlet, buildSlots } from './vSlot'
 
 const toValidId = (str: string): string => str.replace(/[^\w]/g, '')
 
@@ -56,7 +58,7 @@ export const transformElement: NodeTransform = (node, context) => {
       ]
       // props
       if (hasProps) {
-        const { props, directives } = buildProps(node, context)
+        const { props, directives } = buildProps(node.props, node.loc, context)
         args.push(props)
         runtimeDirectives = directives
       }
@@ -94,8 +96,7 @@ export const transformElement: NodeTransform = (node, context) => {
         node.codegenNode = vnode
       }
     } else if (node.tagType === ElementTypes.SLOT) {
-      // <slot [name="xxx"]/>
-      // TODO
+      buildSlotOutlet(node, context)
     }
     // node.tagType can also be TEMPLATE, in which case nothing needs to be done
   }
@@ -103,8 +104,9 @@ export const transformElement: NodeTransform = (node, context) => {
 
 type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
 
-function buildProps(
-  { loc: elementLoc, props }: ElementNode,
+export function buildProps(
+  props: ElementNode['props'],
+  elementLoc: SourceLocation,
   context: TransformContext
 ): {
   props: PropsExpression
@@ -311,13 +313,3 @@ function createDirectiveArgs(
   }
   return createArrayExpression(dirArgs, dir.loc)
 }
-
-function buildSlots(
-  { loc, children }: ElementNode,
-  context: TransformContext
-): ObjectExpression {
-  const slots = createObjectExpression([], loc)
-  // TODO
-
-  return slots
-}
index 70b786d12ed055a08b57f5cf47f717bf6a266301..74cac5cae3ba8c7f9cd09674fb1a733816f5e6a2 100644 (file)
@@ -1 +1,105 @@
-// TODO
+import {
+  ElementNode,
+  ObjectExpression,
+  createObjectExpression,
+  NodeTypes,
+  createCompoundExpression,
+  createCallExpression,
+  CompoundExpressionNode,
+  CallExpression
+} from '../ast'
+import { TransformContext } from '../transform'
+import { buildProps } from './transformElement'
+import { createCompilerError, ErrorCodes } from '../errors'
+import { isSimpleIdentifier } from '../utils'
+import { RENDER_SLOT } from '../runtimeConstants'
+
+export function buildSlots(
+  { loc, children }: ElementNode,
+  context: TransformContext
+): ObjectExpression {
+  const slots = createObjectExpression([], loc)
+  // TODO
+
+  return slots
+}
+
+export function buildSlotOutlet(node: ElementNode, context: TransformContext) {
+  const { props, children, loc } = node
+  const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots`
+  let slot: string | CompoundExpressionNode = $slots + `.default`
+
+  // check for <slot name="xxx" OR :name="xxx" />
+  let nameIndex: number = -1
+  for (let i = 0; i < props.length; i++) {
+    const prop = props[i]
+    if (prop.type === NodeTypes.ATTRIBUTE) {
+      if (prop.name === `name` && prop.value) {
+        // static name="xxx"
+        const name = prop.value.content
+        const accessor = isSimpleIdentifier(name)
+          ? `.${name}`
+          : `[${JSON.stringify(name)}]`
+        slot = `${$slots}${accessor}`
+        nameIndex = i
+        break
+      }
+    } else if (prop.name === `bind`) {
+      const { arg, exp } = prop
+      if (
+        arg &&
+        exp &&
+        arg.type === NodeTypes.SIMPLE_EXPRESSION &&
+        arg.isStatic &&
+        arg.content === `name`
+      ) {
+        // dynamic :name="xxx"
+        slot = createCompoundExpression(
+          [
+            $slots + `[`,
+            ...(exp.type === NodeTypes.SIMPLE_EXPRESSION
+              ? [exp]
+              : exp.children),
+            `]`
+          ],
+          loc
+        )
+        nameIndex = i
+        break
+      }
+    }
+  }
+
+  const slotArgs: CallExpression['arguments'] = [slot]
+  const propsWithoutName =
+    nameIndex > -1
+      ? props.slice(0, nameIndex).concat(props.slice(nameIndex + 1))
+      : props
+  const hasProps = propsWithoutName.length
+  if (hasProps) {
+    const { props: propsExpression, directives } = buildProps(
+      propsWithoutName,
+      loc,
+      context
+    )
+    if (directives.length) {
+      context.onError(
+        createCompilerError(ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET)
+      )
+    }
+    slotArgs.push(propsExpression)
+  }
+
+  if (children.length) {
+    if (!hasProps) {
+      slotArgs.push(`{}`)
+    }
+    slotArgs.push(children)
+  }
+
+  node.codegenNode = createCallExpression(
+    context.helper(RENDER_SLOT),
+    slotArgs,
+    loc
+  )
+}
index ba4cb215237b81e0407edd8c83d5cff07e20e719..fb972c1b5e4e5169c00b212fab4a425e66b06cbb 100644 (file)
@@ -1,10 +1,16 @@
 import { ComponentInternalInstance, currentInstance } from './component'
-import { VNode, NormalizedChildren, normalizeVNode, VNodeChild } from './vnode'
+import {
+  VNode,
+  NormalizedChildren,
+  normalizeVNode,
+  VNodeChild,
+  VNodeChildren
+} from './vnode'
 import { isArray, isFunction } from '@vue/shared'
 import { ShapeFlags } from './shapeFlags'
 import { warn } from './warning'
 
-export type Slot = (...args: any[]) => VNode[]
+export type Slot = (...args: any[]) => VNodeChildren
 export type Slots = Readonly<{
   [name: string]: Slot
 }>
diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts
new file mode 100644 (file)
index 0000000..3cb2c25
--- /dev/null
@@ -0,0 +1,12 @@
+import { Slot } from '../componentSlots'
+import { VNodeChildren } from '../vnode'
+
+export function renderSlot(
+  slot: Slot | undefined,
+  props: any = {},
+  // this is not a user-facing function, so the fallback is always generated by
+  // the compiler.
+  fallback?: string | VNodeChildren
+): string | VNodeChildren | null {
+  return slot ? slot() : fallback || null
+}
index 9082b85906965c93412d8034e829f9400b89c6b8..6dbbbb85d29149ea60f787920e18f3db463cf127 100644 (file)
@@ -42,6 +42,7 @@ export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
 export { renderList } from './helpers/renderList'
 export { toString } from './helpers/toString'
 export { toHandlers } from './helpers/toHandlers'
+export { renderSlot } from './helpers/renderSlot'
 export { capitalize, camelize } from '@vue/shared'
 
 // Internal, for integration with runtime compiler