feat(compiler): transform slot outlets

This commit is contained in:
Evan You 2019-09-27 20:29:20 -04:00
parent d900c13efb
commit ee66ce78b7
10 changed files with 480 additions and 28 deletions

View File

@ -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`
}
]
]
})
})
})

View File

@ -160,8 +160,8 @@ export type JSChildNode =
export interface CallExpression extends Node { export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION type: NodeTypes.JS_CALL_EXPRESSION
callee: string // can only be imported runtime helpers, so no source location callee: string | ExpressionNode
arguments: Array<string | JSChildNode | ChildNode[]> arguments: (string | JSChildNode | ChildNode[])[]
} }
export interface ObjectExpression extends Node { export interface ObjectExpression extends Node {
@ -253,7 +253,7 @@ export function createCompoundExpression(
} }
export function createCallExpression( export function createCallExpression(
callee: string, callee: string | ExpressionNode,
args: CallExpression['arguments'], args: CallExpression['arguments'],
loc: SourceLocation loc: SourceLocation
): CallExpression { ): CallExpression {

View File

@ -17,7 +17,8 @@ import {
Position, Position,
InterpolationNode, InterpolationNode,
CompoundExpressionNode, CompoundExpressionNode,
SimpleExpressionNode SimpleExpressionNode,
ElementTypes
} from './ast' } from './ast'
import { SourceMapGenerator, RawSourceMap } from 'source-map' import { SourceMapGenerator, RawSourceMap } from 'source-map'
import { import {
@ -262,7 +263,10 @@ function genHoists(hoists: JSChildNode[], context: CodegenContext) {
// This will generate a single vnode call if: // This will generate a single vnode call if:
// - The target position explicitly allows a single node (root, if, for) // - 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( function genChildren(
children: ChildNode[], children: ChildNode[],
context: CodegenContext, context: CodegenContext,
@ -272,12 +276,14 @@ function genChildren(
return context.push(`null`) return context.push(`null`)
} }
const child = children[0] const child = children[0]
const type = child.type
if ( if (
children.length === 1 && children.length === 1 &&
(allowSingle || (allowSingle ||
child.type === NodeTypes.TEXT || type === NodeTypes.TEXT ||
child.type === NodeTypes.INTERPOLATION || type === NodeTypes.INTERPOLATION ||
child.type === NodeTypes.COMMENT) (type === NodeTypes.ELEMENT &&
(child as ElementNode).tagType === ElementTypes.SLOT))
) { ) {
genNode(child, context) genNode(child, context)
} else { } else {
@ -523,7 +529,12 @@ function genCallExpression(
context: CodegenContext, context: CodegenContext,
multilines = node.arguments.length > 2 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() multilines && context.indent()
genNodeList(node.arguments, context, multilines) genNodeList(node.arguments, context, multilines)
multilines && context.deindent() multilines && context.deindent()

View File

@ -68,6 +68,7 @@ export const enum ErrorCodes {
X_FOR_MALFORMED_EXPRESSION, X_FOR_MALFORMED_EXPRESSION,
X_V_BIND_NO_EXPRESSION, X_V_BIND_NO_EXPRESSION,
X_V_ON_NO_EXPRESSION, X_V_ON_NO_EXPRESSION,
X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
// generic errors // generic errors
X_PREFIX_ID_NOT_SUPPORTED, 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_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression`,
[ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing 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_V_ON_NO_EXPRESSION]: `v-on is missing expression`,
[ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `unexpected custom directive on <slot> outlet`,
// generic errors // generic errors
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`, [ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,

View File

@ -10,6 +10,7 @@ export const RESOLVE_COMPONENT = `resolveComponent`
export const RESOLVE_DIRECTIVE = `resolveDirective` export const RESOLVE_DIRECTIVE = `resolveDirective`
export const APPLY_DIRECTIVES = `applyDirectives` export const APPLY_DIRECTIVES = `applyDirectives`
export const RENDER_LIST = `renderList` export const RENDER_LIST = `renderList`
export const RENDER_SLOT = `renderSlot`
export const TO_STRING = `toString` export const TO_STRING = `toString`
export const MERGE_PROPS = `mergeProps` export const MERGE_PROPS = `mergeProps`
export const TO_HANDLERS = `toHandlers` export const TO_HANDLERS = `toHandlers`

View File

@ -13,7 +13,8 @@ import {
createObjectProperty, createObjectProperty,
createSimpleExpression, createSimpleExpression,
createObjectExpression, createObjectExpression,
Property Property,
SourceLocation
} from '../ast' } from '../ast'
import { isArray } from '@vue/shared' import { isArray } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
@ -26,6 +27,7 @@ import {
TO_HANDLERS TO_HANDLERS
} from '../runtimeConstants' } from '../runtimeConstants'
import { getInnerRange } from '../utils' import { getInnerRange } from '../utils'
import { buildSlotOutlet, buildSlots } from './vSlot'
const toValidId = (str: string): string => str.replace(/[^\w]/g, '') const toValidId = (str: string): string => str.replace(/[^\w]/g, '')
@ -56,7 +58,7 @@ export const transformElement: NodeTransform = (node, context) => {
] ]
// props // props
if (hasProps) { if (hasProps) {
const { props, directives } = buildProps(node, context) const { props, directives } = buildProps(node.props, node.loc, context)
args.push(props) args.push(props)
runtimeDirectives = directives runtimeDirectives = directives
} }
@ -94,8 +96,7 @@ export const transformElement: NodeTransform = (node, context) => {
node.codegenNode = vnode node.codegenNode = vnode
} }
} else if (node.tagType === ElementTypes.SLOT) { } else if (node.tagType === ElementTypes.SLOT) {
// <slot [name="xxx"]/> buildSlotOutlet(node, context)
// TODO
} }
// node.tagType can also be TEMPLATE, in which case nothing needs to be done // 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 type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
function buildProps( export function buildProps(
{ loc: elementLoc, props }: ElementNode, props: ElementNode['props'],
elementLoc: SourceLocation,
context: TransformContext context: TransformContext
): { ): {
props: PropsExpression props: PropsExpression
@ -311,13 +313,3 @@ function createDirectiveArgs(
} }
return createArrayExpression(dirArgs, dir.loc) return createArrayExpression(dirArgs, dir.loc)
} }
function buildSlots(
{ loc, children }: ElementNode,
context: TransformContext
): ObjectExpression {
const slots = createObjectExpression([], loc)
// TODO
return slots
}

View 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
)
}

View File

@ -1,10 +1,16 @@
import { ComponentInternalInstance, currentInstance } from './component' 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 { isArray, isFunction } from '@vue/shared'
import { ShapeFlags } from './shapeFlags' import { ShapeFlags } from './shapeFlags'
import { warn } from './warning' import { warn } from './warning'
export type Slot = (...args: any[]) => VNode[] export type Slot = (...args: any[]) => VNodeChildren
export type Slots = Readonly<{ export type Slots = Readonly<{
[name: string]: Slot [name: string]: Slot
}> }>

View File

@ -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
}

View File

@ -42,6 +42,7 @@ export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
export { renderList } from './helpers/renderList' export { renderList } from './helpers/renderList'
export { toString } from './helpers/toString' export { toString } from './helpers/toString'
export { toHandlers } from './helpers/toHandlers' export { toHandlers } from './helpers/toHandlers'
export { renderSlot } from './helpers/renderSlot'
export { capitalize, camelize } from '@vue/shared' export { capitalize, camelize } from '@vue/shared'
// Internal, for integration with runtime compiler // Internal, for integration with runtime compiler