diff --git a/packages/compiler-core/__tests__/transforms/vBind.spec.ts b/packages/compiler-core/__tests__/transforms/vBind.spec.ts index 76482fcc..aec02647 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 4f7cd6c1..57408568 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 e791cb64..dea6f460 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 cb10ed1f..0d31a266 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 31dd16a0..441e6fd1 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 76d5ca68..84896a60 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 30e75e36..50b7060a 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 5a24ec2d..4d7b53d3 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 d8578589..7f1bd181 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, - hyphenate, - isFunction, + toHandlerKey, extend, - camelize + hasOwn, + hyphenate, + isArray, + isFunction, + 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 38022edd..d366a9b7 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 ca1cadf2..b711f889 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 997c015d..db47c15a 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 56ff85f5..8be4314d 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 958e4708..c06def0a 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 f3298809..fb355ac3 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.