feat(compiler): handle conditional v-slot

This commit is contained in:
Evan You 2019-10-02 17:18:11 -04:00
parent e90b83600a
commit 3d14265102
9 changed files with 187 additions and 62 deletions

View File

@ -233,7 +233,7 @@ describe('compiler: v-if', () => {
}) })
expect(onError.mock.calls[0]).toMatchObject([ expect(onError.mock.calls[0]).toMatchObject([
{ {
code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF, code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
loc: node1.loc loc: node1.loc
} }
]) ])
@ -245,7 +245,7 @@ describe('compiler: v-if', () => {
) )
expect(onError.mock.calls[1]).toMatchObject([ expect(onError.mock.calls[1]).toMatchObject([
{ {
code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF, code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
loc: node2.loc loc: node2.loc
} }
]) ])
@ -257,7 +257,7 @@ describe('compiler: v-if', () => {
) )
expect(onError.mock.calls[2]).toMatchObject([ expect(onError.mock.calls[2]).toMatchObject([
{ {
code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF, code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
loc: node3.loc loc: node3.loc
} }
]) ])

View File

@ -63,7 +63,6 @@ export const enum ErrorCodes {
// transform errors // transform errors
X_IF_NO_EXPRESSION, X_IF_NO_EXPRESSION,
X_ELSE_IF_NO_ADJACENT_IF,
X_ELSE_NO_ADJACENT_IF, X_ELSE_NO_ADJACENT_IF,
X_FOR_NO_EXPRESSION, X_FOR_NO_EXPRESSION,
X_FOR_MALFORMED_EXPRESSION, X_FOR_MALFORMED_EXPRESSION,
@ -140,8 +139,7 @@ export const errorMessages: { [code: number]: string } = {
// transform errors // transform errors
[ErrorCodes.X_IF_NO_EXPRESSION]: `v-if/v-else-if is missing expression.`, [ErrorCodes.X_IF_NO_EXPRESSION]: `v-if/v-else-if is missing expression.`,
[ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF]: `v-else-if has no adjacent v-if.`, [ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else/v-else-if has no adjacent v-if.`,
[ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else has no adjacent v-if.`,
[ErrorCodes.X_FOR_NO_EXPRESSION]: `v-for is missing expression.`, [ErrorCodes.X_FOR_NO_EXPRESSION]: `v-for is missing expression.`,
[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.`,

View File

@ -16,6 +16,7 @@ import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors' import { CompilerError, defaultOnError } from './errors'
import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants' import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
import { createBlockExpression } from './utils' import { createBlockExpression } from './utils'
import { isVSlot } from './transforms/vSlot'
// There are two types of transforms: // There are two types of transforms:
// //
@ -311,6 +312,11 @@ export function createStructuralDirectiveTransform(
return (node, context) => { return (node, context) => {
if (node.type === NodeTypes.ELEMENT) { if (node.type === NodeTypes.ELEMENT) {
const { props } = node const { props } = node
// structural directive transforms are not concerned with slots
// as they are handled separately in vSlot.ts
if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
return
}
const exitFns = [] const exitFns = []
for (let i = 0; i < props.length; i++) { for (let i = 0; i < props.length; i++) {
const prop = props[i] const prop = props[i]

View File

@ -87,9 +87,9 @@ export const transformElement: NodeTransform = (node, context) => {
args.push(`null`) args.push(`null`)
} }
if (isComponent) { if (isComponent) {
const { slots, hasDynamicSlotName } = buildSlots(node, context) const { slots, hasDynamicSlots } = buildSlots(node, context)
args.push(slots) args.push(slots)
if (hasDynamicSlotName) { if (hasDynamicSlots) {
patchFlag |= PatchFlags.DYNAMIC_SLOTS patchFlag |= PatchFlags.DYNAMIC_SLOTS
} }
} else if (node.children.length === 1) { } else if (node.children.length === 1) {

View File

@ -47,7 +47,7 @@ export const transformFor = createStructuralDirectiveTransform(
// create the loop render function expression now, and add the // create the loop render function expression now, and add the
// iterator on exit after all children have been traversed // iterator on exit after all children have been traversed
const renderExp = createCallExpression(helper(RENDER_LIST), [source]) const renderExp = createCallExpression(helper(RENDER_LIST), [source])
const keyProp = findProp(node.props, `key`) const keyProp = findProp(node, `key`)
const fragmentFlag = keyProp const fragmentFlag = keyProp
? PatchFlags.KEYED_FRAGMENT ? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT : PatchFlags.UNKEYED_FRAGMENT

View File

@ -114,12 +114,7 @@ export const transformIf = createStructuralDirectiveTransform(
} }
} else { } else {
context.onError( context.onError(
createCompilerError( createCompilerError(ErrorCodes.X_ELSE_NO_ADJACENT_IF, node.loc)
dir.name === 'else'
? ErrorCodes.X_ELSE_NO_ADJACENT_IF
: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
node.loc
)
) )
} }
break break

View File

@ -11,15 +11,24 @@ import {
ExpressionNode, ExpressionNode,
Property, Property,
TemplateChildNode, TemplateChildNode,
SourceLocation SourceLocation,
createConditionalExpression,
ConditionalExpression,
JSChildNode,
SimpleExpressionNode
} from '../ast' } from '../ast'
import { TransformContext, NodeTransform } from '../transform' import { TransformContext, NodeTransform } from '../transform'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { isString } from '@vue/shared' import { mergeExpressions, findNonEmptyDir } from '../utils'
export const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode => export const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
p.type === NodeTypes.DIRECTIVE && p.name === 'slot' p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
const defaultFallback = createSimpleExpression(`undefined`, false)
// A NodeTransform that tracks scope identifiers for scoped slots so that they // A NodeTransform that tracks scope identifiers for scoped slots so that they
// don't get prefixed by transformExpression. This transform is only applied // don't get prefixed by transformExpression. This transform is only applied
// in non-browser builds with { prefixIdentifiers: true } // in non-browser builds with { prefixIdentifiers: true }
@ -46,10 +55,10 @@ export function buildSlots(
context: TransformContext context: TransformContext
): { ): {
slots: ObjectExpression slots: ObjectExpression
hasDynamicSlotName: boolean hasDynamicSlots: boolean
} { } {
const slots: Property[] = [] const slots: Property[] = []
let hasDynamicSlotName = false let hasDynamicSlots = false
// 1. Check for default slot with slotProps on component itself. // 1. Check for default slot with slotProps on component itself.
// <Comp v-slot="{ prop }"/> // <Comp v-slot="{ prop }"/>
@ -61,7 +70,7 @@ export function buildSlots(
createCompilerError(ErrorCodes.X_NAMED_SLOT_ON_COMPONENT, loc) createCompilerError(ErrorCodes.X_NAMED_SLOT_ON_COMPONENT, loc)
) )
} }
slots.push(buildSlot(`default`, exp, children, loc)) slots.push(buildDefaultSlot(exp, children, loc))
} }
// 2. Iterate through children and check for template slots // 2. Iterate through children and check for template slots
@ -70,45 +79,127 @@ export function buildSlots(
let extraneousChild: TemplateChildNode | undefined = undefined let extraneousChild: TemplateChildNode | undefined = undefined
const seenSlotNames = new Set<string>() const seenSlotNames = new Set<string>()
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const slotElement = children[i]
let slotDir let slotDir
if ( if (
child.type === NodeTypes.ELEMENT && slotElement.type !== NodeTypes.ELEMENT ||
child.tagType === ElementTypes.TEMPLATE && slotElement.tagType !== ElementTypes.TEMPLATE ||
(slotDir = child.props.find(isVSlot)) !(slotDir = slotElement.props.find(isVSlot))
) { ) {
hasTemplateSlots = true // not a <template v-slot>, skip.
const { children, loc: nodeLoc } = child extraneousChild = extraneousChild || slotElement
const { arg: slotName, exp: slotProps, loc: dirLoc } = slotDir continue
if (explicitDefaultSlot) { }
// already has on-component default slot - this is incorrect usage.
context.onError( if (explicitDefaultSlot) {
createCompilerError(ErrorCodes.X_MIXED_SLOT_USAGE, dirLoc) // already has on-component default slot - this is incorrect usage.
context.onError(
createCompilerError(ErrorCodes.X_MIXED_SLOT_USAGE, slotDir.loc)
)
break
}
hasTemplateSlots = true
const { children: slotChildren, loc: slotLoc } = slotElement
const {
arg: slotName = createSimpleExpression(`default`, true),
exp: slotProps,
loc: dirLoc
} = slotDir
// check if name is dynamic.
let staticSlotName
if (isStaticExp(slotName)) {
staticSlotName = slotName ? slotName.content : `default`
} else {
hasDynamicSlots = true
}
const slotFunction = createFunctionExpression(
slotProps,
slotChildren,
false,
slotChildren.length ? slotChildren[0].loc : slotLoc
)
// check if this slot is conditional (v-if/else/else-if)
let vIf
let vElse
if ((vIf = findNonEmptyDir(slotElement, 'if'))) {
hasDynamicSlots = true
slots.push(
createObjectProperty(
slotName,
createConditionalExpression(vIf.exp!, slotFunction, defaultFallback)
) )
break )
} else { } else if ((vElse = findNonEmptyDir(slotElement, /^else(-if)?$/))) {
if ( hasDynamicSlots = true
!slotName || // find adjacent v-if slot
(slotName.type === NodeTypes.SIMPLE_EXPRESSION && slotName.isStatic) let vIfBase
) { let i = slots.length
// check duplicate slot names while (i--) {
const name = slotName ? slotName.content : `default` if (slots[i].value.type === NodeTypes.JS_CONDITIONAL_EXPRESSION) {
if (seenSlotNames.has(name)) { vIfBase = slots[i]
context.onError( break
createCompilerError(ErrorCodes.X_DUPLICATE_SLOT_NAMES, dirLoc)
)
continue
}
seenSlotNames.add(name)
} else {
hasDynamicSlotName = true
} }
slots.push( }
buildSlot(slotName || `default`, slotProps, children, nodeLoc) if (vIfBase) {
// check if the v-else and the base v-if has the same slot name
if (
isStaticExp(vIfBase.key) &&
vIfBase.key.content === staticSlotName
) {
let conditional = vIfBase.value as ConditionalExpression
while (
conditional.alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
) {
conditional = conditional.alternate
}
conditional.alternate = vElse.exp
? createConditionalExpression(
vElse.exp,
slotFunction,
defaultFallback
)
: slotFunction
} else {
// not the same slot name. generate a separate property.
slots.push(
createObjectProperty(
slotName,
createConditionalExpression(
// negate baseVIf
mergeExpressions(
`!(`,
(vIfBase.value as ConditionalExpression).test,
`)`,
...(vElse.exp ? [` && (`, vElse.exp, `)`] : [])
),
slotFunction,
defaultFallback
)
)
)
}
} else {
context.onError(
createCompilerError(ErrorCodes.X_ELSE_NO_ADJACENT_IF, vElse.loc)
) )
} }
} else if (!extraneousChild) { } else {
extraneousChild = child // check duplicate static names
if (staticSlotName) {
if (seenSlotNames.has(staticSlotName)) {
context.onError(
createCompilerError(ErrorCodes.X_DUPLICATE_SLOT_NAMES, dirLoc)
)
continue
}
seenSlotNames.add(staticSlotName)
}
slots.push(createObjectProperty(slotName, slotFunction))
} }
} }
@ -123,23 +214,22 @@ export function buildSlots(
if (!explicitDefaultSlot && !hasTemplateSlots) { if (!explicitDefaultSlot && !hasTemplateSlots) {
// implicit default slot. // implicit default slot.
slots.push(buildSlot(`default`, undefined, children, loc)) slots.push(buildDefaultSlot(undefined, children, loc))
} }
return { return {
slots: createObjectExpression(slots, loc), slots: createObjectExpression(slots, loc),
hasDynamicSlotName hasDynamicSlots
} }
} }
function buildSlot( function buildDefaultSlot(
name: string | ExpressionNode,
slotProps: ExpressionNode | undefined, slotProps: ExpressionNode | undefined,
children: TemplateChildNode[], children: TemplateChildNode[],
loc: SourceLocation loc: SourceLocation
): Property { ): Property {
return createObjectProperty( return createObjectProperty(
isString(name) ? createSimpleExpression(name, true, loc) : name, createSimpleExpression(`default`, true),
createFunctionExpression( createFunctionExpression(
slotProps, slotProps,
children, children,

View File

@ -6,12 +6,17 @@ import {
CallExpression, CallExpression,
SequenceExpression, SequenceExpression,
createSequenceExpression, createSequenceExpression,
createCallExpression createCallExpression,
ExpressionNode,
CompoundExpressionNode,
createCompoundExpression,
DirectiveNode
} from './ast' } from './ast'
import { parse } from 'acorn' import { parse } from 'acorn'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
import { TransformContext } from './transform' import { TransformContext } from './transform'
import { OPEN_BLOCK, CREATE_BLOCK } from './runtimeConstants' import { OPEN_BLOCK, CREATE_BLOCK } from './runtimeConstants'
import { isString } from '@vue/shared'
// cache node requires // cache node requires
// lazy require dependencies so that they don't end up in rollup's dep graph // lazy require dependencies so that they don't end up in rollup's dep graph
@ -106,12 +111,28 @@ export function assert(condition: boolean, msg?: string) {
} }
} }
export function findNonEmptyDir(
node: ElementNode,
name: string | RegExp
): DirectiveNode | undefined {
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (
p.type === NodeTypes.DIRECTIVE &&
p.exp &&
(isString(name) ? p.name === name : name.test(p.name))
) {
return p
}
}
}
export function findProp( export function findProp(
props: ElementNode['props'], node: ElementNode,
name: string name: string
): ElementNode['props'][0] | undefined { ): ElementNode['props'][0] | undefined {
for (let i = 0; i < props.length; i++) { for (let i = 0; i < node.props.length; i++) {
const p = props[i] const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) { if (p.type === NodeTypes.ATTRIBUTE) {
if (p.name === name && p.value && !p.value.isEmpty) { if (p.name === name && p.value && !p.value.isEmpty) {
return p return p
@ -137,3 +158,18 @@ export function createBlockExpression(
createCallExpression(context.helper(CREATE_BLOCK), args) createCallExpression(context.helper(CREATE_BLOCK), args)
]) ])
} }
export function mergeExpressions(
...args: (string | ExpressionNode)[]
): CompoundExpressionNode {
const children: CompoundExpressionNode['children'] = []
for (let i = 0; i < args.length; i++) {
const exp = args[i]
if (isString(exp) || exp.type === NodeTypes.SIMPLE_EXPRESSION) {
children.push(exp)
} else {
children.push(...exp.children)
}
}
return createCompoundExpression(children)
}

View File

@ -51,7 +51,7 @@ export function resolveSlots(
let value = (children as RawSlots)[key] let value = (children as RawSlots)[key]
if (isFunction(value)) { if (isFunction(value)) {
;(slots as any)[key] = normalizeSlot(key, value) ;(slots as any)[key] = normalizeSlot(key, value)
} else { } else if (value != null) {
if (__DEV__) { if (__DEV__) {
warn( warn(
`Non-function value encountered for slot "${key}". ` + `Non-function value encountered for slot "${key}". ` +