feat(transition): properly handle transition & transition-group in compiler

This commit is contained in:
Evan You 2019-11-29 12:42:04 -05:00
parent 4e8d57bdfb
commit 0e3e07079a
7 changed files with 77 additions and 40 deletions
packages
compiler-core/src
compiler-dom/src
runtime-dom/src/components

@ -1,4 +1,4 @@
import { NO, makeMap, isArray } from '@vue/shared' import { NO, isArray } from '@vue/shared'
import { import {
ErrorCodes, ErrorCodes,
createCompilerError, createCompilerError,
@ -8,7 +8,8 @@ import {
import { import {
assert, assert,
advancePositionWithMutation, advancePositionWithMutation,
advancePositionWithClone advancePositionWithClone,
isCoreComponent
} from './utils' } from './utils'
import { import {
Namespace, Namespace,
@ -29,22 +30,12 @@ import {
} from './ast' } from './ast'
import { extend } from '@vue/shared' import { extend } from '@vue/shared'
// Portal and Fragment are native types, not components
const isBuiltInComponent = /*#__PURE__*/ makeMap(
`suspense,keep-alive,base-transition`,
true
)
export interface ParserOptions { export interface ParserOptions {
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
isNativeTag?: (tag: string) => boolean // e.g. loading-indicator in weex isNativeTag?: (tag: string) => boolean // e.g. loading-indicator in weex
isPreTag?: (tag: string) => boolean // e.g. <pre> where whitespace is intact isPreTag?: (tag: string) => boolean // e.g. <pre> where whitespace is intact
isCustomElement?: (tag: string) => boolean isCustomElement?: (tag: string) => boolean
// for importing platform-specific components from the runtime. isBuiltInComponent?: (tag: string) => symbol | void
// e.g. <transition> for runtime-dom
// However this is only needed if isNativeTag is not specified, since when
// isNativeTag is specified anything that is not native is a component.
isBuiltInComponent?: (tag: string) => boolean
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
getTextMode?: (tag: string, ns: Namespace) => TextModes getTextMode?: (tag: string, ns: Namespace) => TextModes
delimiters?: [string, string] // ['{{', '}}'] delimiters?: [string, string] // ['{{', '}}']
@ -483,7 +474,7 @@ function parseTag(
if (options.isNativeTag) { if (options.isNativeTag) {
if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
} else if ( } else if (
isBuiltInComponent(tag) || isCoreComponent(tag) ||
(options.isBuiltInComponent && options.isBuiltInComponent(tag)) || (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
/^[A-Z]/.test(tag) /^[A-Z]/.test(tag)
) { ) {

@ -17,7 +17,7 @@ import {
CacheExpression, CacheExpression,
createCacheExpression createCacheExpression
} from './ast' } from './ast'
import { isString, isArray } from '@vue/shared' import { isString, isArray, NOOP } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors' import { CompilerError, defaultOnError } from './errors'
import { import {
TO_STRING, TO_STRING,
@ -68,6 +68,7 @@ export type StructuralDirectiveTransform = (
export interface TransformOptions { export interface TransformOptions {
nodeTransforms?: NodeTransform[] nodeTransforms?: NodeTransform[]
directiveTransforms?: { [name: string]: DirectiveTransform } directiveTransforms?: { [name: string]: DirectiveTransform }
isBuiltInComponent?: (tag: string) => symbol | void
prefixIdentifiers?: boolean prefixIdentifiers?: boolean
hoistStatic?: boolean hoistStatic?: boolean
cacheHandlers?: boolean cacheHandlers?: boolean
@ -110,6 +111,7 @@ function createTransformContext(
cacheHandlers = false, cacheHandlers = false,
nodeTransforms = [], nodeTransforms = [],
directiveTransforms = {}, directiveTransforms = {},
isBuiltInComponent = NOOP,
onError = defaultOnError onError = defaultOnError
}: TransformOptions }: TransformOptions
): TransformContext { ): TransformContext {
@ -132,6 +134,7 @@ function createTransformContext(
cacheHandlers, cacheHandlers,
nodeTransforms, nodeTransforms,
directiveTransforms, directiveTransforms,
isBuiltInComponent,
onError, onError,
parent: null, parent: null,
currentNode: root, currentNode: root,

@ -15,7 +15,7 @@ import {
createObjectExpression, createObjectExpression,
Property Property
} from '../ast' } from '../ast'
import { PatchFlags, PatchFlagNames, isSymbol, hyphenate } from '@vue/shared' import { PatchFlags, PatchFlagNames, isSymbol } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { import {
CREATE_VNODE, CREATE_VNODE,
@ -26,11 +26,15 @@ import {
MERGE_PROPS, MERGE_PROPS,
TO_HANDLERS, TO_HANDLERS,
PORTAL, PORTAL,
SUSPENSE, KEEP_ALIVE
KEEP_ALIVE,
BASE_TRANSITION
} from '../runtimeHelpers' } from '../runtimeHelpers'
import { getInnerRange, isVSlot, toValidAssetId, findProp } from '../utils' import {
getInnerRange,
isVSlot,
toValidAssetId,
findProp,
isCoreComponent
} from '../utils'
import { buildSlots } from './vSlot' import { buildSlots } from './vSlot'
import { isStaticNode } from './hoistStatic' import { isStaticNode } from './hoistStatic'
@ -38,9 +42,6 @@ import { isStaticNode } from './hoistStatic'
// import, which should be used instead of a resolveDirective call. // import, which should be used instead of a resolveDirective call.
const directiveImportMap = new WeakMap<DirectiveNode, symbol>() const directiveImportMap = new WeakMap<DirectiveNode, symbol>()
const isBuiltInType = (tag: string, expected: string): boolean =>
tag === expected || tag === hyphenate(expected)
// generate a JavaScript AST for this element's codegen // generate a JavaScript AST for this element's codegen
export const transformElement: NodeTransform = (node, context) => { export const transformElement: NodeTransform = (node, context) => {
if ( if (
@ -57,10 +58,8 @@ export const transformElement: NodeTransform = (node, context) => {
// processed and merged. // processed and merged.
return function postTransformElement() { return function postTransformElement() {
const { tag, tagType, props } = node const { tag, tagType, props } = node
const isPortal = isBuiltInType(tag, 'Portal') const builtInComponentSymbol =
const isSuspense = isBuiltInType(tag, 'Suspense') isCoreComponent(tag) || context.isBuiltInComponent(tag)
const isKeepAlive = isBuiltInType(tag, 'KeepAlive')
const isBaseTransition = isBuiltInType(tag, 'BaseTransition')
const isComponent = tagType === ElementTypes.COMPONENT const isComponent = tagType === ElementTypes.COMPONENT
let hasProps = props.length > 0 let hasProps = props.length > 0
@ -96,14 +95,8 @@ export const transformElement: NodeTransform = (node, context) => {
let nodeType let nodeType
if (dynamicComponent) { if (dynamicComponent) {
nodeType = dynamicComponent nodeType = dynamicComponent
} else if (isPortal) { } else if (builtInComponentSymbol) {
nodeType = context.helper(PORTAL) nodeType = context.helper(builtInComponentSymbol)
} else if (isSuspense) {
nodeType = context.helper(SUSPENSE)
} else if (isKeepAlive) {
nodeType = context.helper(KEEP_ALIVE)
} else if (isBaseTransition) {
nodeType = context.helper(BASE_TRANSITION)
} else if (isComponent) { } else if (isComponent) {
// user component w/ resolve // user component w/ resolve
context.helper(RESOLVE_COMPONENT) context.helper(RESOLVE_COMPONENT)
@ -142,7 +135,11 @@ export const transformElement: NodeTransform = (node, context) => {
// Portal is not a real component has dedicated handling in the renderer // Portal is not a real component has dedicated handling in the renderer
// KeepAlive should not track its own deps so that it can be used inside // KeepAlive should not track its own deps so that it can be used inside
// Transition // Transition
if (isComponent && !isPortal && !isKeepAlive) { if (
isComponent &&
builtInComponentSymbol !== PORTAL &&
builtInComponentSymbol !== KEEP_ALIVE
) {
const { slots, hasDynamicSlots } = buildSlots(node, context) const { slots, hasDynamicSlots } = buildSlots(node, context)
args.push(slots) args.push(slots)
if (hasDynamicSlots) { if (hasDynamicSlots) {

@ -27,8 +27,31 @@ import {
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, MERGE_PROPS, RENDER_SLOT } from './runtimeHelpers' import {
import { isString, isFunction, isObject } from '@vue/shared' OPEN_BLOCK,
MERGE_PROPS,
RENDER_SLOT,
PORTAL,
SUSPENSE,
KEEP_ALIVE,
BASE_TRANSITION
} from './runtimeHelpers'
import { isString, isFunction, isObject, hyphenate } from '@vue/shared'
export const isBuiltInType = (tag: string, expected: string): boolean =>
tag === expected || tag === hyphenate(expected)
export function isCoreComponent(tag: string): symbol | void {
if (isBuiltInType(tag, 'Portal')) {
return PORTAL
} else if (isBuiltInType(tag, 'Suspense')) {
return SUSPENSE
} else if (isBuiltInType(tag, 'KeepAlive')) {
return KEEP_ALIVE
} else if (isBuiltInType(tag, 'BaseTransition')) {
return BASE_TRANSITION
}
}
// 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

@ -1,4 +1,9 @@
import { baseCompile, CompilerOptions, CodegenResult } from '@vue/compiler-core' import {
baseCompile,
CompilerOptions,
CodegenResult,
isBuiltInType
} from '@vue/compiler-core'
import { parserOptionsMinimal } from './parserOptionsMinimal' import { parserOptionsMinimal } from './parserOptionsMinimal'
import { parserOptionsStandard } from './parserOptionsStandard' import { parserOptionsStandard } from './parserOptionsStandard'
import { transformStyle } from './transforms/transformStyle' import { transformStyle } from './transforms/transformStyle'
@ -8,6 +13,7 @@ import { transformVText } from './transforms/vText'
import { transformModel } from './transforms/vModel' import { transformModel } from './transforms/vModel'
import { transformOn } from './transforms/vOn' import { transformOn } from './transforms/vOn'
import { transformShow } from './transforms/vShow' import { transformShow } from './transforms/vShow'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
export function compile( export function compile(
template: string, template: string,
@ -25,6 +31,13 @@ export function compile(
on: transformOn, on: transformOn,
show: transformShow, show: transformShow,
...(options.directiveTransforms || {}) ...(options.directiveTransforms || {})
},
isBuiltInComponent: tag => {
if (isBuiltInType(tag, `Transition`)) {
return TRANSITION
} else if (isBuiltInType(tag, `TransitionGroup`)) {
return TRANSITION_GROUP
}
} }
}) })
} }

@ -11,6 +11,9 @@ export const V_ON_WITH_KEYS = Symbol(__DEV__ ? `vOnKeysGuard` : ``)
export const V_SHOW = Symbol(__DEV__ ? `vShow` : ``) export const V_SHOW = Symbol(__DEV__ ? `vShow` : ``)
export const TRANSITION = Symbol(__DEV__ ? `Transition` : ``)
export const TRANSITION_GROUP = Symbol(__DEV__ ? `TransitionGroup` : ``)
registerRuntimeHelpers({ registerRuntimeHelpers({
[V_MODEL_RADIO]: `vModelRadio`, [V_MODEL_RADIO]: `vModelRadio`,
[V_MODEL_CHECKBOX]: `vModelCheckbox`, [V_MODEL_CHECKBOX]: `vModelCheckbox`,
@ -19,5 +22,7 @@ registerRuntimeHelpers({
[V_MODEL_DYNAMIC]: `vModelDynamic`, [V_MODEL_DYNAMIC]: `vModelDynamic`,
[V_ON_WITH_MODIFIERS]: `withModifiers`, [V_ON_WITH_MODIFIERS]: `withModifiers`,
[V_ON_WITH_KEYS]: `withKeys`, [V_ON_WITH_KEYS]: `withKeys`,
[V_SHOW]: `vShow` [V_SHOW]: `vShow`,
[TRANSITION]: `Transition`,
[TRANSITION_GROUP]: `TransitionGroup`
}) })

@ -95,6 +95,11 @@ export const TransitionGroup = {
prevChildren = children prevChildren = children
children = slots.default ? slots.default() : [] children = slots.default ? slots.default() : []
// handle fragment children case, e.g. v-for
if (children.length === 1 && children[0].type === Fragment) {
children = children[0].children as VNode[]
}
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
if (child.key != null) { if (child.key != null) {