feat(compile-core): handle falsy dynamic args for v-on and v-bind (#2393)

fix #2388
This commit is contained in:
ᴜɴвʏтᴇ 2020-10-20 05:15:53 +08:00 committed by GitHub
parent 7390487c7d
commit 052a621762
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 96 additions and 71 deletions

View File

@ -71,7 +71,7 @@ describe('compiler: transform v-bind', () => {
const props = (node.codegenNode as VNodeCall).props as ObjectExpression const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({ expect(props.properties[0]).toMatchObject({
key: { key: {
content: `id`, content: `id || ""`,
isStatic: false isStatic: false
}, },
value: { value: {
@ -130,7 +130,7 @@ describe('compiler: transform v-bind', () => {
const props = (node.codegenNode as VNodeCall).props as ObjectExpression const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({ expect(props.properties[0]).toMatchObject({
key: { key: {
content: `_${helperNameMap[CAMELIZE]}(foo)`, content: `_${helperNameMap[CAMELIZE]}(foo || "")`,
isStatic: false isStatic: false
}, },
value: { value: {
@ -149,10 +149,12 @@ describe('compiler: transform v-bind', () => {
key: { key: {
children: [ children: [
`_${helperNameMap[CAMELIZE]}(`, `_${helperNameMap[CAMELIZE]}(`,
`(`,
{ content: `_ctx.foo` }, { content: `_ctx.foo` },
`(`, `(`,
{ content: `_ctx.bar` }, { content: `_ctx.bar` },
`)`, `)`,
`) || ""`,
`)` `)`
] ]
}, },

View File

@ -1,14 +1,14 @@
import { import {
baseParse as parse, baseParse as parse,
transform,
ElementNode,
ObjectExpression,
CompilerOptions, CompilerOptions,
ElementNode,
ErrorCodes, ErrorCodes,
NodeTypes, TO_HANDLER_KEY,
VNodeCall,
helperNameMap, helperNameMap,
CAPITALIZE NodeTypes,
ObjectExpression,
transform,
VNodeCall
} from '../../src' } from '../../src'
import { transformOn } from '../../src/transforms/vOn' import { transformOn } from '../../src/transforms/vOn'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
@ -76,7 +76,7 @@ describe('compiler: transform v-on', () => {
key: { key: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
`"on" + _${helperNameMap[CAPITALIZE]}(`, `_${helperNameMap[TO_HANDLER_KEY]}(`,
{ content: `event` }, { content: `event` },
`)` `)`
] ]
@ -101,7 +101,7 @@ describe('compiler: transform v-on', () => {
key: { key: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
`"on" + _${helperNameMap[CAPITALIZE]}(`, `_${helperNameMap[TO_HANDLER_KEY]}(`,
{ content: `_ctx.event` }, { content: `_ctx.event` },
`)` `)`
] ]
@ -126,7 +126,7 @@ describe('compiler: transform v-on', () => {
key: { key: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
`"on" + _${helperNameMap[CAPITALIZE]}(`, `_${helperNameMap[TO_HANDLER_KEY]}(`,
{ content: `_ctx.event` }, { content: `_ctx.event` },
`(`, `(`,
{ content: `_ctx.foo` }, { content: `_ctx.foo` },

View File

@ -23,6 +23,7 @@ export const MERGE_PROPS = Symbol(__DEV__ ? `mergeProps` : ``)
export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``) export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``)
export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``) export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``)
export const CAPITALIZE = Symbol(__DEV__ ? `capitalize` : ``) export const CAPITALIZE = Symbol(__DEV__ ? `capitalize` : ``)
export const TO_HANDLER_KEY = Symbol(__DEV__ ? `toHandlerKey` : ``)
export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``) export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``)
export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``) export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``)
export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``) export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``)
@ -56,6 +57,7 @@ export const helperNameMap: any = {
[TO_HANDLERS]: `toHandlers`, [TO_HANDLERS]: `toHandlers`,
[CAMELIZE]: `camelize`, [CAMELIZE]: `camelize`,
[CAPITALIZE]: `capitalize`, [CAPITALIZE]: `capitalize`,
[TO_HANDLER_KEY]: `toHandlerKey`,
[SET_BLOCK_TRACKING]: `setBlockTracking`, [SET_BLOCK_TRACKING]: `setBlockTracking`,
[PUSH_SCOPE_ID]: `pushScopeId`, [PUSH_SCOPE_ID]: `pushScopeId`,
[POP_SCOPE_ID]: `popScopeId`, [POP_SCOPE_ID]: `popScopeId`,

View File

@ -10,6 +10,14 @@ import { CAMELIZE } from '../runtimeHelpers'
export const transformBind: DirectiveTransform = (dir, node, context) => { export const transformBind: DirectiveTransform = (dir, node, context) => {
const { exp, modifiers, loc } = dir const { exp, modifiers, loc } = dir
const arg = dir.arg! 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 // .prop is no longer necessary due to new patch behavior
// .sync is replaced by v-model:arg // .sync is replaced by v-model:arg
if (modifiers.includes('camel')) { if (modifiers.includes('camel')) {

View File

@ -1,20 +1,20 @@
import { DirectiveTransform, DirectiveTransformResult } from '../transform' import { DirectiveTransform, DirectiveTransformResult } from '../transform'
import { import {
DirectiveNode, createCompoundExpression,
createObjectProperty, createObjectProperty,
createSimpleExpression, createSimpleExpression,
DirectiveNode,
ElementTypes,
ExpressionNode, ExpressionNode,
NodeTypes, NodeTypes,
createCompoundExpression, SimpleExpressionNode
SimpleExpressionNode,
ElementTypes
} from '../ast' } from '../ast'
import { capitalize, camelize } from '@vue/shared' import { camelize, toHandlerKey } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression' import { validateBrowserExpression } from '../validateExpression'
import { isMemberExpression, hasScopeRef } from '../utils' import { hasScopeRef, isMemberExpression } from '../utils'
import { CAPITALIZE } from '../runtimeHelpers' import { TO_HANDLER_KEY } from '../runtimeHelpers'
const fnExpRE = /^\s*([\w$_]+|\([^)]*?\))\s*=>|^\s*function(?:\s+[\w$]+)?\s*\(/ const fnExpRE = /^\s*([\w$_]+|\([^)]*?\))\s*=>|^\s*function(?:\s+[\w$]+)?\s*\(/
@ -43,11 +43,15 @@ export const transformOn: DirectiveTransform = (
if (arg.isStatic) { if (arg.isStatic) {
const rawName = arg.content const rawName = arg.content
// for all event listeners, auto convert it to camelCase. See issue #2249 // for all event listeners, auto convert it to camelCase. See issue #2249
const normalizedName = capitalize(camelize(rawName)) eventName = createSimpleExpression(
eventName = createSimpleExpression(`on${normalizedName}`, true, arg.loc) toHandlerKey(camelize(rawName)),
true,
arg.loc
)
} else { } else {
// #2388
eventName = createCompoundExpression([ eventName = createCompoundExpression([
`"on" + ${context.helperString(CAPITALIZE)}(`, `${context.helperString(TO_HANDLER_KEY)}(`,
arg, arg,
`)` `)`
]) ])
@ -55,7 +59,7 @@ export const transformOn: DirectiveTransform = (
} else { } else {
// already a compound expression. // already a compound expression.
eventName = arg eventName = arg
eventName.children.unshift(`"on" + ${context.helperString(CAPITALIZE)}(`) eventName.children.unshift(`${context.helperString(TO_HANDLER_KEY)}(`)
eventName.children.push(`)`) eventName.children.push(`)`)
} }

View File

@ -1,16 +1,16 @@
import { import {
baseParse as parse, baseParse as parse,
transform,
CompilerOptions, CompilerOptions,
ElementNode, ElementNode,
ObjectExpression, TO_HANDLER_KEY,
NodeTypes,
VNodeCall,
helperNameMap, helperNameMap,
CAPITALIZE NodeTypes,
ObjectExpression,
transform,
VNodeCall
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { transformOn } from '../../src/transforms/vOn' 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 { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression' import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression'
import { genFlagText } from '../../../compiler-core/__tests__/testUtils' import { genFlagText } from '../../../compiler-core/__tests__/testUtils'
@ -195,14 +195,14 @@ describe('compiler-dom: transform v-on', () => {
const { const {
props: [prop2] props: [prop2]
} = parseWithVOn(`<div @[event].right="test"/>`) } = parseWithVOn(`<div @[event].right="test"/>`)
// ("on" + (event)).toLowerCase() === "onclick" ? "onContextmenu" : ("on" + (event)) // (_toHandlerKey(event)).toLowerCase() === "onclick" ? "onContextmenu" : (_toHandlerKey(event))
expect(prop2.key).toMatchObject({ expect(prop2.key).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
`(`, `(`,
{ {
children: [ children: [
`"on" + _${helperNameMap[CAPITALIZE]}(`, `_${helperNameMap[TO_HANDLER_KEY]}(`,
{ content: 'event' }, { content: 'event' },
`)` `)`
] ]
@ -210,7 +210,7 @@ describe('compiler-dom: transform v-on', () => {
`) === "onClick" ? "onContextmenu" : (`, `) === "onClick" ? "onContextmenu" : (`,
{ {
children: [ children: [
`"on" + _${helperNameMap[CAPITALIZE]}(`, `_${helperNameMap[TO_HANDLER_KEY]}(`,
{ content: 'event' }, { content: 'event' },
`)` `)`
] ]
@ -233,14 +233,14 @@ describe('compiler-dom: transform v-on', () => {
const { const {
props: [prop2] props: [prop2]
} = parseWithVOn(`<div @[event].middle="test"/>`) } = parseWithVOn(`<div @[event].middle="test"/>`)
// ("on" + (event)).toLowerCase() === "onclick" ? "onMouseup" : ("on" + (event)) // (_eventNaming(event)).toLowerCase() === "onclick" ? "onMouseup" : (_eventNaming(event))
expect(prop2.key).toMatchObject({ expect(prop2.key).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
`(`, `(`,
{ {
children: [ children: [
`"on" + _${helperNameMap[CAPITALIZE]}(`, `_${helperNameMap[TO_HANDLER_KEY]}(`,
{ content: 'event' }, { content: 'event' },
`)` `)`
] ]
@ -248,7 +248,7 @@ describe('compiler-dom: transform v-on', () => {
`) === "onClick" ? "onMouseup" : (`, `) === "onClick" ? "onMouseup" : (`,
{ {
children: [ children: [
`"on" + _${helperNameMap[CAPITALIZE]}(`, `_${helperNameMap[TO_HANDLER_KEY]}(`,
{ content: 'event' }, { content: 'event' },
`)` `)`
] ]

View File

@ -161,7 +161,7 @@ describe('ssr: element', () => {
expect(getCompiledString(`<div v-bind:[key]="value"></div>`)) expect(getCompiledString(`<div v-bind:[key]="value"></div>`))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"\`<div\${ "\`<div\${
_ssrRenderAttrs({ [_ctx.key]: _ctx.value }) _ssrRenderAttrs({ [_ctx.key || \\"\\"]: _ctx.value })
}></div>\`" }></div>\`"
`) `)
@ -170,7 +170,7 @@ describe('ssr: element', () => {
"\`<div\${ "\`<div\${
_ssrRenderAttrs({ _ssrRenderAttrs({
class: \\"foo\\", class: \\"foo\\",
[_ctx.key]: _ctx.value [_ctx.key || \\"\\"]: _ctx.value
}) })
}></div>\`" }></div>\`"
`) `)
@ -180,7 +180,7 @@ describe('ssr: element', () => {
"\`<div\${ "\`<div\${
_ssrRenderAttrs({ _ssrRenderAttrs({
id: _ctx.id, id: _ctx.id,
[_ctx.key]: _ctx.value [_ctx.key || \\"\\"]: _ctx.value
}) })
}></div>\`" }></div>\`"
`) `)
@ -212,7 +212,7 @@ describe('ssr: element', () => {
expect(getCompiledString(`<div :[key]="id" v-bind="obj"></div>`)) expect(getCompiledString(`<div :[key]="id" v-bind="obj"></div>`))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"\`<div\${ "\`<div\${
_ssrRenderAttrs(_mergeProps({ [_ctx.key]: _ctx.id }, _ctx.obj)) _ssrRenderAttrs(_mergeProps({ [_ctx.key || \\"\\"]: _ctx.id }, _ctx.obj))
}></div>\`" }></div>\`"
`) `)

View File

@ -1,15 +1,15 @@
import { import {
ComponentInternalInstance, ComponentInternalInstance,
LifecycleHooks,
currentInstance, currentInstance,
setCurrentInstance, isInSSRComponentSetup,
isInSSRComponentSetup LifecycleHooks,
setCurrentInstance
} from './component' } from './component'
import { ComponentPublicInstance } from './componentPublicInstance' import { ComponentPublicInstance } from './componentPublicInstance'
import { callWithAsyncErrorHandling, ErrorTypeStrings } from './errorHandling' import { callWithAsyncErrorHandling, ErrorTypeStrings } from './errorHandling'
import { warn } from './warning' import { warn } from './warning'
import { capitalize } from '@vue/shared' import { toHandlerKey } from '@vue/shared'
import { pauseTracking, resetTracking, DebuggerEvent } from '@vue/reactivity' import { DebuggerEvent, pauseTracking, resetTracking } from '@vue/reactivity'
export { onActivated, onDeactivated } from './components/KeepAlive' export { onActivated, onDeactivated } from './components/KeepAlive'
@ -49,9 +49,7 @@ export function injectHook(
} }
return wrappedHook return wrappedHook
} else if (__DEV__) { } else if (__DEV__) {
const apiName = `on${capitalize( const apiName = toHandlerKey(ErrorTypeStrings[type].replace(/ hook$/, ''))
ErrorTypeStrings[type].replace(/ hook$/, '')
)}`
warn( warn(
`${apiName} is called when there is no active component instance to be ` + `${apiName} is called when there is no active component instance to be ` +
`associated with. ` + `associated with. ` +

View File

@ -1,13 +1,13 @@
import { import {
isArray, camelize,
isOn,
hasOwn,
EMPTY_OBJ, EMPTY_OBJ,
capitalize, toHandlerKey,
hyphenate,
isFunction,
extend, extend,
camelize hasOwn,
hyphenate,
isArray,
isFunction,
isOn
} from '@vue/shared' } from '@vue/shared'
import { import {
ComponentInternalInstance, ComponentInternalInstance,
@ -56,10 +56,10 @@ export function emit(
} = instance } = instance
if (emitsOptions) { if (emitsOptions) {
if (!(event in emitsOptions)) { if (!(event in emitsOptions)) {
if (!propsOptions || !(`on` + capitalize(event) in propsOptions)) { if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
warn( warn(
`Component emitted event "${event}" but it is neither declared in ` + `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 { } else {
@ -82,7 +82,7 @@ export function emit(
if (__DEV__) { if (__DEV__) {
const lowerCaseEvent = event.toLowerCase() const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && props[`on` + capitalize(lowerCaseEvent)]) { if (lowerCaseEvent !== event && props[toHandlerKey(lowerCaseEvent)]) {
warn( warn(
`Event "${lowerCaseEvent}" is emitted in component ` + `Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName( `${formatComponentName(
@ -97,12 +97,12 @@ export function emit(
} }
// convert handler name to camelCase. See issue #2249 // convert handler name to camelCase. See issue #2249
let handlerName = `on${capitalize(camelize(event))}` let handlerName = toHandlerKey(camelize(event))
let handler = props[handlerName] let handler = props[handlerName]
// for v-model update:xxx events, also trigger kebab-case equivalent // for v-model update:xxx events, also trigger kebab-case equivalent
// for props passed via kebab-case // for props passed via kebab-case
if (!handler && event.startsWith('update:')) { if (!handler && event.startsWith('update:')) {
handlerName = `on${capitalize(hyphenate(event))}` handlerName = toHandlerKey(hyphenate(event))
handler = props[handlerName] handler = props[handlerName]
} }
if (!handler) { if (!handler) {

View File

@ -1,4 +1,4 @@
import { isObject, capitalize } from '@vue/shared' import { toHandlerKey, isObject } from '@vue/shared'
import { warn } from '../warning' import { warn } from '../warning'
/** /**
@ -12,7 +12,7 @@ export function toHandlers(obj: Record<string, any>): Record<string, any> {
return ret return ret
} }
for (const key in obj) { for (const key in obj) {
ret[`on${capitalize(key)}`] = obj[key] ret[toHandlerKey(key)] = obj[key]
} }
return ret return ret
} }

View File

@ -240,7 +240,12 @@ export {
createCommentVNode, createCommentVNode,
createStaticVNode createStaticVNode
} from './vnode' } from './vnode'
export { toDisplayString, camelize, capitalize } from '@vue/shared' export {
toDisplayString,
camelize,
capitalize,
toHandlerKey
} from '@vue/shared'
// For test-utils // For test-utils
export { transformVNodeArgs } from './vnode' export { transformVNodeArgs } from './vnode'

View File

@ -1070,6 +1070,7 @@ function baseCreateRenderer(
) => { ) => {
if (oldProps !== newProps) { if (oldProps !== newProps) {
for (const key in newProps) { for (const key in newProps) {
// empty string is not valid prop
if (isReservedProp(key)) continue if (isReservedProp(key)) continue
const next = newProps[key] const next = newProps[key]
const prev = oldProps[key] const prev = oldProps[key]

View File

@ -644,7 +644,7 @@ export function mergeProps(...args: (Data & VNodeProps)[]) {
? [].concat(existing as any, toMerge[key] as any) ? [].concat(existing as any, toMerge[key] as any)
: incoming : incoming
} }
} else { } else if (key !== '') {
ret[key] = toMerge[key] ret[key] = toMerge[key]
} }
} }

View File

@ -10,7 +10,8 @@ import {
makeMap makeMap
} from '@vue/shared' } 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( export function ssrRenderAttrs(
props: Record<string, unknown>, props: Record<string, unknown>,

View File

@ -94,7 +94,8 @@ export const isIntegerKey = (key: unknown) =>
'' + parseInt(key, 10) === key '' + parseInt(key, 10) === key
export const isReservedProp = /*#__PURE__*/ makeMap( export const isReservedProp = /*#__PURE__*/ makeMap(
'key,ref,' + // the leading comma is intentional so empty string "" is also included
',key,ref,' +
'onVnodeBeforeMount,onVnodeMounted,' + 'onVnodeBeforeMount,onVnodeMounted,' +
'onVnodeBeforeUpdate,onVnodeUpdated,' + 'onVnodeBeforeUpdate,onVnodeUpdated,' +
'onVnodeBeforeUnmount,onVnodeUnmounted' 'onVnodeBeforeUnmount,onVnodeUnmounted'
@ -122,19 +123,22 @@ const hyphenateRE = /\B([A-Z])/g
/** /**
* @private * @private
*/ */
export const hyphenate = cacheStringFunction( export const hyphenate = cacheStringFunction((str: string) =>
(str: string): string => { str.replace(hyphenateRE, '-$1').toLowerCase()
return str.replace(hyphenateRE, '-$1').toLowerCase()
}
) )
/** /**
* @private * @private
*/ */
export const capitalize = cacheStringFunction( export const capitalize = cacheStringFunction(
(str: string): string => { (str: string) => str.charAt(0).toUpperCase() + str.slice(1)
return 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. // compare whether a value has changed, accounting for NaN.