From 052a621762c5f7c420464747ebbbed27c7350593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=9C=C9=B4=D0=B2=CA=8F=D1=82=E1=B4=87?= Date: Tue, 20 Oct 2020 05:15:53 +0800 Subject: [PATCH] feat(compile-core): handle falsy dynamic args for v-on and v-bind (#2393) fix #2388 --- .../__tests__/transforms/vBind.spec.ts | 6 +++-- .../__tests__/transforms/vOn.spec.ts | 18 ++++++------- packages/compiler-core/src/runtimeHelpers.ts | 2 ++ .../compiler-core/src/transforms/vBind.ts | 8 ++++++ packages/compiler-core/src/transforms/vOn.ts | 26 +++++++++++-------- .../__tests__/transforms/vOn.spec.ts | 24 ++++++++--------- .../compiler-ssr/__tests__/ssrElement.spec.ts | 8 +++--- packages/runtime-core/src/apiLifecycle.ts | 14 +++++----- packages/runtime-core/src/componentEmits.ts | 22 ++++++++-------- .../runtime-core/src/helpers/toHandlers.ts | 4 +-- packages/runtime-core/src/index.ts | 7 ++++- packages/runtime-core/src/renderer.ts | 1 + packages/runtime-core/src/vnode.ts | 2 +- .../src/helpers/ssrRenderAttrs.ts | 3 ++- packages/shared/src/index.ts | 20 ++++++++------ 15 files changed, 95 insertions(+), 70 deletions(-) diff --git a/packages/compiler-core/__tests__/transforms/vBind.spec.ts b/packages/compiler-core/__tests__/transforms/vBind.spec.ts index 76482fcc3c8..aec02647ca7 100644 --- a/packages/compiler-core/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vBind.spec.ts @@ -71,7 +71,7 @@ describe('compiler: transform v-bind', () => { const props = (node.codegenNode as VNodeCall).props as ObjectExpression expect(props.properties[0]).toMatchObject({ key: { - content: `id`, + content: `id || ""`, isStatic: false }, value: { @@ -130,7 +130,7 @@ describe('compiler: transform v-bind', () => { const props = (node.codegenNode as VNodeCall).props as ObjectExpression expect(props.properties[0]).toMatchObject({ key: { - content: `_${helperNameMap[CAMELIZE]}(foo)`, + content: `_${helperNameMap[CAMELIZE]}(foo || "")`, isStatic: false }, value: { @@ -149,10 +149,12 @@ describe('compiler: transform v-bind', () => { key: { children: [ `_${helperNameMap[CAMELIZE]}(`, + `(`, { content: `_ctx.foo` }, `(`, { content: `_ctx.bar` }, `)`, + `) || ""`, `)` ] }, diff --git a/packages/compiler-core/__tests__/transforms/vOn.spec.ts b/packages/compiler-core/__tests__/transforms/vOn.spec.ts index 4f7cd6c17c4..57408568fbc 100644 --- a/packages/compiler-core/__tests__/transforms/vOn.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vOn.spec.ts @@ -1,14 +1,14 @@ import { baseParse as parse, - transform, - ElementNode, - ObjectExpression, CompilerOptions, + ElementNode, ErrorCodes, - NodeTypes, - VNodeCall, + TO_HANDLER_KEY, helperNameMap, - CAPITALIZE + NodeTypes, + ObjectExpression, + transform, + VNodeCall } from '../../src' import { transformOn } from '../../src/transforms/vOn' import { transformElement } from '../../src/transforms/transformElement' @@ -76,7 +76,7 @@ describe('compiler: transform v-on', () => { key: { type: NodeTypes.COMPOUND_EXPRESSION, children: [ - `"on" + _${helperNameMap[CAPITALIZE]}(`, + `_${helperNameMap[TO_HANDLER_KEY]}(`, { content: `event` }, `)` ] @@ -101,7 +101,7 @@ describe('compiler: transform v-on', () => { key: { type: NodeTypes.COMPOUND_EXPRESSION, children: [ - `"on" + _${helperNameMap[CAPITALIZE]}(`, + `_${helperNameMap[TO_HANDLER_KEY]}(`, { content: `_ctx.event` }, `)` ] @@ -126,7 +126,7 @@ describe('compiler: transform v-on', () => { key: { type: NodeTypes.COMPOUND_EXPRESSION, children: [ - `"on" + _${helperNameMap[CAPITALIZE]}(`, + `_${helperNameMap[TO_HANDLER_KEY]}(`, { content: `_ctx.event` }, `(`, { content: `_ctx.foo` }, diff --git a/packages/compiler-core/src/runtimeHelpers.ts b/packages/compiler-core/src/runtimeHelpers.ts index e791cb6493a..dea6f460b19 100644 --- a/packages/compiler-core/src/runtimeHelpers.ts +++ b/packages/compiler-core/src/runtimeHelpers.ts @@ -23,6 +23,7 @@ export const MERGE_PROPS = Symbol(__DEV__ ? `mergeProps` : ``) export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``) export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``) export const CAPITALIZE = Symbol(__DEV__ ? `capitalize` : ``) +export const TO_HANDLER_KEY = Symbol(__DEV__ ? `toHandlerKey` : ``) export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``) export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``) export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``) @@ -56,6 +57,7 @@ export const helperNameMap: any = { [TO_HANDLERS]: `toHandlers`, [CAMELIZE]: `camelize`, [CAPITALIZE]: `capitalize`, + [TO_HANDLER_KEY]: `toHandlerKey`, [SET_BLOCK_TRACKING]: `setBlockTracking`, [PUSH_SCOPE_ID]: `pushScopeId`, [POP_SCOPE_ID]: `popScopeId`, diff --git a/packages/compiler-core/src/transforms/vBind.ts b/packages/compiler-core/src/transforms/vBind.ts index cb10ed1f4c5..0d31a266a2c 100644 --- a/packages/compiler-core/src/transforms/vBind.ts +++ b/packages/compiler-core/src/transforms/vBind.ts @@ -10,6 +10,14 @@ import { CAMELIZE } from '../runtimeHelpers' export const transformBind: DirectiveTransform = (dir, node, context) => { const { exp, modifiers, loc } = dir const arg = dir.arg! + + if (arg.type !== NodeTypes.SIMPLE_EXPRESSION) { + arg.children.unshift(`(`) + arg.children.push(`) || ""`) + } else if (!arg.isStatic) { + arg.content = `${arg.content} || ""` + } + // .prop is no longer necessary due to new patch behavior // .sync is replaced by v-model:arg if (modifiers.includes('camel')) { diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts index 31dd16a0bde..441e6fd1674 100644 --- a/packages/compiler-core/src/transforms/vOn.ts +++ b/packages/compiler-core/src/transforms/vOn.ts @@ -1,20 +1,20 @@ import { DirectiveTransform, DirectiveTransformResult } from '../transform' import { - DirectiveNode, + createCompoundExpression, createObjectProperty, createSimpleExpression, + DirectiveNode, + ElementTypes, ExpressionNode, NodeTypes, - createCompoundExpression, - SimpleExpressionNode, - ElementTypes + SimpleExpressionNode } from '../ast' -import { capitalize, camelize } from '@vue/shared' +import { camelize, toHandlerKey } from '@vue/shared' import { createCompilerError, ErrorCodes } from '../errors' import { processExpression } from './transformExpression' import { validateBrowserExpression } from '../validateExpression' -import { isMemberExpression, hasScopeRef } from '../utils' -import { CAPITALIZE } from '../runtimeHelpers' +import { hasScopeRef, isMemberExpression } from '../utils' +import { TO_HANDLER_KEY } from '../runtimeHelpers' const fnExpRE = /^\s*([\w$_]+|\([^)]*?\))\s*=>|^\s*function(?:\s+[\w$]+)?\s*\(/ @@ -43,11 +43,15 @@ export const transformOn: DirectiveTransform = ( if (arg.isStatic) { const rawName = arg.content // for all event listeners, auto convert it to camelCase. See issue #2249 - const normalizedName = capitalize(camelize(rawName)) - eventName = createSimpleExpression(`on${normalizedName}`, true, arg.loc) + eventName = createSimpleExpression( + toHandlerKey(camelize(rawName)), + true, + arg.loc + ) } else { + // #2388 eventName = createCompoundExpression([ - `"on" + ${context.helperString(CAPITALIZE)}(`, + `${context.helperString(TO_HANDLER_KEY)}(`, arg, `)` ]) @@ -55,7 +59,7 @@ export const transformOn: DirectiveTransform = ( } else { // already a compound expression. eventName = arg - eventName.children.unshift(`"on" + ${context.helperString(CAPITALIZE)}(`) + eventName.children.unshift(`${context.helperString(TO_HANDLER_KEY)}(`) eventName.children.push(`)`) } diff --git a/packages/compiler-dom/__tests__/transforms/vOn.spec.ts b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts index 76d5ca68968..84896a60d75 100644 --- a/packages/compiler-dom/__tests__/transforms/vOn.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts @@ -1,16 +1,16 @@ import { baseParse as parse, - transform, CompilerOptions, ElementNode, - ObjectExpression, - NodeTypes, - VNodeCall, + TO_HANDLER_KEY, helperNameMap, - CAPITALIZE + NodeTypes, + ObjectExpression, + transform, + VNodeCall } from '@vue/compiler-core' import { transformOn } from '../../src/transforms/vOn' -import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../../src/runtimeHelpers' +import { V_ON_WITH_KEYS, V_ON_WITH_MODIFIERS } from '../../src/runtimeHelpers' import { transformElement } from '../../../compiler-core/src/transforms/transformElement' import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression' import { genFlagText } from '../../../compiler-core/__tests__/testUtils' @@ -195,14 +195,14 @@ describe('compiler-dom: transform v-on', () => { const { props: [prop2] } = parseWithVOn(`
`) - // ("on" + (event)).toLowerCase() === "onclick" ? "onContextmenu" : ("on" + (event)) + // (_toHandlerKey(event)).toLowerCase() === "onclick" ? "onContextmenu" : (_toHandlerKey(event)) expect(prop2.key).toMatchObject({ type: NodeTypes.COMPOUND_EXPRESSION, children: [ `(`, { children: [ - `"on" + _${helperNameMap[CAPITALIZE]}(`, + `_${helperNameMap[TO_HANDLER_KEY]}(`, { content: 'event' }, `)` ] @@ -210,7 +210,7 @@ describe('compiler-dom: transform v-on', () => { `) === "onClick" ? "onContextmenu" : (`, { children: [ - `"on" + _${helperNameMap[CAPITALIZE]}(`, + `_${helperNameMap[TO_HANDLER_KEY]}(`, { content: 'event' }, `)` ] @@ -233,14 +233,14 @@ describe('compiler-dom: transform v-on', () => { const { props: [prop2] } = parseWithVOn(`
`) - // ("on" + (event)).toLowerCase() === "onclick" ? "onMouseup" : ("on" + (event)) + // (_eventNaming(event)).toLowerCase() === "onclick" ? "onMouseup" : (_eventNaming(event)) expect(prop2.key).toMatchObject({ type: NodeTypes.COMPOUND_EXPRESSION, children: [ `(`, { children: [ - `"on" + _${helperNameMap[CAPITALIZE]}(`, + `_${helperNameMap[TO_HANDLER_KEY]}(`, { content: 'event' }, `)` ] @@ -248,7 +248,7 @@ describe('compiler-dom: transform v-on', () => { `) === "onClick" ? "onMouseup" : (`, { children: [ - `"on" + _${helperNameMap[CAPITALIZE]}(`, + `_${helperNameMap[TO_HANDLER_KEY]}(`, { content: 'event' }, `)` ] diff --git a/packages/compiler-ssr/__tests__/ssrElement.spec.ts b/packages/compiler-ssr/__tests__/ssrElement.spec.ts index 30e75e36f2b..50b7060a58c 100644 --- a/packages/compiler-ssr/__tests__/ssrElement.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrElement.spec.ts @@ -161,7 +161,7 @@ describe('ssr: element', () => { expect(getCompiledString(`
`)) .toMatchInlineSnapshot(` "\`
\`" `) @@ -170,7 +170,7 @@ describe('ssr: element', () => { "\`
\`" `) @@ -180,7 +180,7 @@ describe('ssr: element', () => { "\`\`" `) @@ -212,7 +212,7 @@ describe('ssr: element', () => { expect(getCompiledString(`
`)) .toMatchInlineSnapshot(` "\`\`" `) diff --git a/packages/runtime-core/src/apiLifecycle.ts b/packages/runtime-core/src/apiLifecycle.ts index 5a24ec2da58..4d7b53d36a7 100644 --- a/packages/runtime-core/src/apiLifecycle.ts +++ b/packages/runtime-core/src/apiLifecycle.ts @@ -1,15 +1,15 @@ import { ComponentInternalInstance, - LifecycleHooks, currentInstance, - setCurrentInstance, - isInSSRComponentSetup + isInSSRComponentSetup, + LifecycleHooks, + setCurrentInstance } from './component' import { ComponentPublicInstance } from './componentPublicInstance' import { callWithAsyncErrorHandling, ErrorTypeStrings } from './errorHandling' import { warn } from './warning' -import { capitalize } from '@vue/shared' -import { pauseTracking, resetTracking, DebuggerEvent } from '@vue/reactivity' +import { toHandlerKey } from '@vue/shared' +import { DebuggerEvent, pauseTracking, resetTracking } from '@vue/reactivity' export { onActivated, onDeactivated } from './components/KeepAlive' @@ -49,9 +49,7 @@ export function injectHook( } return wrappedHook } else if (__DEV__) { - const apiName = `on${capitalize( - ErrorTypeStrings[type].replace(/ hook$/, '') - )}` + const apiName = toHandlerKey(ErrorTypeStrings[type].replace(/ hook$/, '')) warn( `${apiName} is called when there is no active component instance to be ` + `associated with. ` + diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index d8578589c4e..7f1bd1813fb 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -1,13 +1,13 @@ import { - isArray, - isOn, - hasOwn, + camelize, EMPTY_OBJ, - capitalize, + toHandlerKey, + extend, + hasOwn, hyphenate, + isArray, isFunction, - extend, - camelize + isOn } from '@vue/shared' import { ComponentInternalInstance, @@ -56,10 +56,10 @@ export function emit( } = instance if (emitsOptions) { if (!(event in emitsOptions)) { - if (!propsOptions || !(`on` + capitalize(event) in propsOptions)) { + if (!propsOptions || !(toHandlerKey(event) in propsOptions)) { warn( `Component emitted event "${event}" but it is neither declared in ` + - `the emits option nor as an "on${capitalize(event)}" prop.` + `the emits option nor as an "${toHandlerKey(event)}" prop.` ) } } else { @@ -82,7 +82,7 @@ export function emit( if (__DEV__) { const lowerCaseEvent = event.toLowerCase() - if (lowerCaseEvent !== event && props[`on` + capitalize(lowerCaseEvent)]) { + if (lowerCaseEvent !== event && props[toHandlerKey(lowerCaseEvent)]) { warn( `Event "${lowerCaseEvent}" is emitted in component ` + `${formatComponentName( @@ -97,12 +97,12 @@ export function emit( } // convert handler name to camelCase. See issue #2249 - let handlerName = `on${capitalize(camelize(event))}` + let handlerName = toHandlerKey(camelize(event)) let handler = props[handlerName] // for v-model update:xxx events, also trigger kebab-case equivalent // for props passed via kebab-case if (!handler && event.startsWith('update:')) { - handlerName = `on${capitalize(hyphenate(event))}` + handlerName = toHandlerKey(hyphenate(event)) handler = props[handlerName] } if (!handler) { diff --git a/packages/runtime-core/src/helpers/toHandlers.ts b/packages/runtime-core/src/helpers/toHandlers.ts index 38022edd7d9..d366a9b76c9 100644 --- a/packages/runtime-core/src/helpers/toHandlers.ts +++ b/packages/runtime-core/src/helpers/toHandlers.ts @@ -1,4 +1,4 @@ -import { isObject, capitalize } from '@vue/shared' +import { toHandlerKey, isObject } from '@vue/shared' import { warn } from '../warning' /** @@ -12,7 +12,7 @@ export function toHandlers(obj: Record): Record { return ret } for (const key in obj) { - ret[`on${capitalize(key)}`] = obj[key] + ret[toHandlerKey(key)] = obj[key] } return ret } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index ca1cadf28d9..b711f8895e8 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -240,7 +240,12 @@ export { createCommentVNode, createStaticVNode } from './vnode' -export { toDisplayString, camelize, capitalize } from '@vue/shared' +export { + toDisplayString, + camelize, + capitalize, + toHandlerKey +} from '@vue/shared' // For test-utils export { transformVNodeArgs } from './vnode' diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 997c015d8b8..db47c15ad5e 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1070,6 +1070,7 @@ function baseCreateRenderer( ) => { if (oldProps !== newProps) { for (const key in newProps) { + // empty string is not valid prop if (isReservedProp(key)) continue const next = newProps[key] const prev = oldProps[key] diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 56ff85f5eea..8be4314d5bb 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -644,7 +644,7 @@ export function mergeProps(...args: (Data & VNodeProps)[]) { ? [].concat(existing as any, toMerge[key] as any) : incoming } - } else { + } else if (key !== '') { ret[key] = toMerge[key] } } diff --git a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts index 958e470805c..c06def0a992 100644 --- a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts +++ b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts @@ -10,7 +10,8 @@ import { makeMap } from '@vue/shared' -const shouldIgnoreProp = makeMap(`key,ref,innerHTML,textContent`) +// leading comma for empty string "" +const shouldIgnoreProp = makeMap(`,key,ref,innerHTML,textContent`) export function ssrRenderAttrs( props: Record, diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index f32988096ba..fb355ac399a 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -94,7 +94,8 @@ export const isIntegerKey = (key: unknown) => '' + parseInt(key, 10) === key export const isReservedProp = /*#__PURE__*/ makeMap( - 'key,ref,' + + // the leading comma is intentional so empty string "" is also included + ',key,ref,' + 'onVnodeBeforeMount,onVnodeMounted,' + 'onVnodeBeforeUpdate,onVnodeUpdated,' + 'onVnodeBeforeUnmount,onVnodeUnmounted' @@ -122,19 +123,22 @@ const hyphenateRE = /\B([A-Z])/g /** * @private */ -export const hyphenate = cacheStringFunction( - (str: string): string => { - return str.replace(hyphenateRE, '-$1').toLowerCase() - } +export const hyphenate = cacheStringFunction((str: string) => + str.replace(hyphenateRE, '-$1').toLowerCase() ) /** * @private */ export const capitalize = cacheStringFunction( - (str: string): string => { - return str.charAt(0).toUpperCase() + str.slice(1) - } + (str: string) => str.charAt(0).toUpperCase() + str.slice(1) +) + +/** + * @private + */ +export const toHandlerKey = cacheStringFunction( + (str: string) => (str ? `on${capitalize(str)}` : ``) ) // compare whether a value has changed, accounting for NaN.