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 {
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 {

View 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
) {
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()

View 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.`,

View 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`

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

View File

@ -1 +1,105 @@
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 { 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
}>

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 { 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