From ee66ce78b7f29e901ca7d25f0bd0ef889ef7b070 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 27 Sep 2019 20:29:20 -0400 Subject: [PATCH] feat(compiler): transform slot outlets --- .../__tests__/transforms/vSlot.spec.ts | 323 ++++++++++++++++++ packages/compiler-core/src/ast.ts | 6 +- packages/compiler-core/src/codegen.ts | 23 +- packages/compiler-core/src/errors.ts | 2 + .../compiler-core/src/runtimeConstants.ts | 1 + .../src/transforms/transformElement.ts | 24 +- .../compiler-core/src/transforms/vSlot.ts | 106 +++++- packages/runtime-core/src/componentSlots.ts | 10 +- .../runtime-core/src/helpers/renderSlot.ts | 12 + packages/runtime-core/src/index.ts | 1 + 10 files changed, 480 insertions(+), 28 deletions(-) create mode 100644 packages/compiler-core/__tests__/transforms/vSlot.spec.ts create mode 100644 packages/runtime-core/src/helpers/renderSlot.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 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