diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
new file mode 100644
index 00000000..4d11e9e0
--- /dev/null
+++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
@@ -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(``)
+ 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(``)
+ 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(``)
+ 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(``)
+ 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(``, {
+ 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(``)
+ 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(``)
+ 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(``)
+ 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(``)
+ 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(``)
+ 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(``)
+ 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(``)
+ 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`
+ }
+ ]
+ ]
+ })
+ })
+})
diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts
index 90817339..4bfb4bea 100644
--- a/packages/compiler-core/src/ast.ts
+++ b/packages/compiler-core/src/ast.ts
@@ -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
+ 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 {
diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts
index 6dc09119..9405587f 100644
--- a/packages/compiler-core/src/codegen.ts
+++ b/packages/compiler-core/src/codegen.ts
@@ -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
+// - 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()
diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts
index 28a982c7..a3de21fc 100644
--- a/packages/compiler-core/src/errors.ts
+++ b/packages/compiler-core/src/errors.ts
@@ -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 outlet`,
// generic errors
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
diff --git a/packages/compiler-core/src/runtimeConstants.ts b/packages/compiler-core/src/runtimeConstants.ts
index 6510963f..79e3d09b 100644
--- a/packages/compiler-core/src/runtimeConstants.ts
+++ b/packages/compiler-core/src/runtimeConstants.ts
@@ -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`
diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts
index 90245342..10378540 100644
--- a/packages/compiler-core/src/transforms/transformElement.ts
+++ b/packages/compiler-core/src/transforms/transformElement.ts
@@ -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) {
- //
- // 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
-}
diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts
index 70b786d1..74cac5ca 100644
--- a/packages/compiler-core/src/transforms/vSlot.ts
+++ b/packages/compiler-core/src/transforms/vSlot.ts
@@ -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
+ 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
+ )
+}
diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts
index ba4cb215..fb972c1b 100644
--- a/packages/runtime-core/src/componentSlots.ts
+++ b/packages/runtime-core/src/componentSlots.ts
@@ -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
index 00000000..3cb2c25e
--- /dev/null
+++ b/packages/runtime-core/src/helpers/renderSlot.ts
@@ -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
+}
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index 9082b859..6dbbbb85 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -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