const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
- content: `id`,
+ content: `id || ""`,
isStatic: false
},
value: {
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: {
key: {
children: [
`_${helperNameMap[CAMELIZE]}(`,
+ `(`,
{ content: `_ctx.foo` },
`(`,
{ content: `_ctx.bar` },
`)`,
+ `) || ""`,
`)`
]
},
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'
key: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
- `"on" + _${helperNameMap[CAPITALIZE]}(`,
+ `_${helperNameMap[TO_HANDLER_KEY]}(`,
{ content: `event` },
`)`
]
key: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
- `"on" + _${helperNameMap[CAPITALIZE]}(`,
+ `_${helperNameMap[TO_HANDLER_KEY]}(`,
{ content: `_ctx.event` },
`)`
]
key: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
- `"on" + _${helperNameMap[CAPITALIZE]}(`,
+ `_${helperNameMap[TO_HANDLER_KEY]}(`,
{ content: `_ctx.event` },
`(`,
{ content: `_ctx.foo` },
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` : ``)
[TO_HANDLERS]: `toHandlers`,
[CAMELIZE]: `camelize`,
[CAPITALIZE]: `capitalize`,
+ [TO_HANDLER_KEY]: `toHandlerKey`,
[SET_BLOCK_TRACKING]: `setBlockTracking`,
[PUSH_SCOPE_ID]: `pushScopeId`,
[POP_SCOPE_ID]: `popScopeId`,
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')) {
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*\(/
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,
`)`
])
} 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(`)`)
}
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'
const {
props: [prop2]
} = parseWithVOn(`<div @[event].right="test"/>`)
- // ("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' },
`)`
]
`) === "onClick" ? "onContextmenu" : (`,
{
children: [
- `"on" + _${helperNameMap[CAPITALIZE]}(`,
+ `_${helperNameMap[TO_HANDLER_KEY]}(`,
{ content: 'event' },
`)`
]
const {
props: [prop2]
} = parseWithVOn(`<div @[event].middle="test"/>`)
- // ("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' },
`)`
]
`) === "onClick" ? "onMouseup" : (`,
{
children: [
- `"on" + _${helperNameMap[CAPITALIZE]}(`,
+ `_${helperNameMap[TO_HANDLER_KEY]}(`,
{ content: 'event' },
`)`
]
expect(getCompiledString(`<div v-bind:[key]="value"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${
- _ssrRenderAttrs({ [_ctx.key]: _ctx.value })
+ _ssrRenderAttrs({ [_ctx.key || \\"\\"]: _ctx.value })
}></div>\`"
`)
"\`<div\${
_ssrRenderAttrs({
class: \\"foo\\",
- [_ctx.key]: _ctx.value
+ [_ctx.key || \\"\\"]: _ctx.value
})
}></div>\`"
`)
"\`<div\${
_ssrRenderAttrs({
id: _ctx.id,
- [_ctx.key]: _ctx.value
+ [_ctx.key || \\"\\"]: _ctx.value
})
}></div>\`"
`)
expect(getCompiledString(`<div :[key]="id" v-bind="obj"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${
- _ssrRenderAttrs(_mergeProps({ [_ctx.key]: _ctx.id }, _ctx.obj))
+ _ssrRenderAttrs(_mergeProps({ [_ctx.key || \\"\\"]: _ctx.id }, _ctx.obj))
}></div>\`"
`)
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'
}
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. ` +
import {
- isArray,
- isOn,
- hasOwn,
+ camelize,
EMPTY_OBJ,
- capitalize,
+ toHandlerKey,
+ extend,
+ hasOwn,
hyphenate,
+ isArray,
isFunction,
- extend,
- camelize
+ isOn
} from '@vue/shared'
import {
ComponentInternalInstance,
} = 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 {
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(
}
// 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) {
-import { isObject, capitalize } from '@vue/shared'
+import { toHandlerKey, isObject } from '@vue/shared'
import { warn } from '../warning'
/**
return ret
}
for (const key in obj) {
- ret[`on${capitalize(key)}`] = obj[key]
+ ret[toHandlerKey(key)] = obj[key]
}
return ret
}
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'
) => {
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]
? [].concat(existing as any, toMerge[key] as any)
: incoming
}
- } else {
+ } else if (key !== '') {
ret[key] = toMerge[key]
}
}
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<string, 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'
/**
* @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.