]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compile-core): handle falsy dynamic args for v-on and v-bind (#2393)
authorᴜɴвʏтᴇ <i@shangyes.net>
Mon, 19 Oct 2020 21:15:53 +0000 (05:15 +0800)
committerGitHub <noreply@github.com>
Mon, 19 Oct 2020 21:15:53 +0000 (17:15 -0400)
fix #2388

15 files changed:
packages/compiler-core/__tests__/transforms/vBind.spec.ts
packages/compiler-core/__tests__/transforms/vOn.spec.ts
packages/compiler-core/src/runtimeHelpers.ts
packages/compiler-core/src/transforms/vBind.ts
packages/compiler-core/src/transforms/vOn.ts
packages/compiler-dom/__tests__/transforms/vOn.spec.ts
packages/compiler-ssr/__tests__/ssrElement.spec.ts
packages/runtime-core/src/apiLifecycle.ts
packages/runtime-core/src/componentEmits.ts
packages/runtime-core/src/helpers/toHandlers.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/vnode.ts
packages/server-renderer/src/helpers/ssrRenderAttrs.ts
packages/shared/src/index.ts

index 76482fcc3c8377647651044f9d40e6ebd363f601..aec02647ca7f3aaf64f3e5d97a2ecca9a0d1bffc 100644 (file)
@@ -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` },
           `)`,
+          `) || ""`,
           `)`
         ]
       },
index 4f7cd6c17c4a34bfb8dcd3c5a0c7e55aa07c2485..57408568fbc1d92f68882bffca0398501ab78a9a 100644 (file)
@@ -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` },
index e791cb6493a0ea0ddb7fe7e0a0d05c72b4a451f6..dea6f460b19bd24833960e9e5f6a82a1d46aa826 100644 (file)
@@ -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`,
index cb10ed1f4c54b7e55ad327ea75a6e84edc24b807..0d31a266a2c0db381c2ca6258c4a24973c2aad91 100644 (file)
@@ -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')) {
index 31dd16a0bde2408f52e9d1ad7f2fed7e2b135d6a..441e6fd1674a15e9c6e5ca983ba8961726a09214 100644 (file)
@@ -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(`)`)
   }
 
index 76d5ca689686f6a62604b523b17c6591166d9568..84896a60d759341cec7778426740c3e502c8041b 100644 (file)
@@ -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(`<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' },
             `)`
           ]
@@ -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(`<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' },
             `)`
           ]
@@ -248,7 +248,7 @@ describe('compiler-dom: transform v-on', () => {
         `) === "onClick" ? "onMouseup" : (`,
         {
           children: [
-            `"on" + _${helperNameMap[CAPITALIZE]}(`,
+            `_${helperNameMap[TO_HANDLER_KEY]}(`,
             { content: 'event' },
             `)`
           ]
index 30e75e36f2b10f0cca210b44095ca7fb8e0dbd34..50b7060a58cc6770ac7f18ab91bfb13440cd3a72 100644 (file)
@@ -161,7 +161,7 @@ describe('ssr: element', () => {
       expect(getCompiledString(`<div v-bind:[key]="value"></div>`))
         .toMatchInlineSnapshot(`
         "\`<div\${
-            _ssrRenderAttrs({ [_ctx.key]: _ctx.value })
+            _ssrRenderAttrs({ [_ctx.key || \\"\\"]: _ctx.value })
           }></div>\`"
       `)
 
@@ -170,7 +170,7 @@ describe('ssr: element', () => {
         "\`<div\${
             _ssrRenderAttrs({
               class: \\"foo\\",
-              [_ctx.key]: _ctx.value
+              [_ctx.key || \\"\\"]: _ctx.value
             })
           }></div>\`"
       `)
@@ -180,7 +180,7 @@ describe('ssr: element', () => {
         "\`<div\${
             _ssrRenderAttrs({
               id: _ctx.id,
-              [_ctx.key]: _ctx.value
+              [_ctx.key || \\"\\"]: _ctx.value
             })
           }></div>\`"
       `)
@@ -212,7 +212,7 @@ describe('ssr: element', () => {
       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>\`"
       `)
 
index 5a24ec2da58d6530188fcc98de1b0642eb2192f8..4d7b53d36a72229bcf0d6811965903802efdaf25 100644 (file)
@@ -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. ` +
index d8578589c4e240326033d549810b94435224af40..7f1bd1813fb6aeb1c34e93882229f917eea0237e 100644 (file)
@@ -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) {
index 38022edd7d9f7bc1634794296d57bbae3027c28a..d366a9b76c90de2c273cbb37d2e0124b7e548766 100644 (file)
@@ -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<string, any>): Record<string, any> {
     return ret
   }
   for (const key in obj) {
-    ret[`on${capitalize(key)}`] = obj[key]
+    ret[toHandlerKey(key)] = obj[key]
   }
   return ret
 }
index ca1cadf28d9074051b208e2e24c9c35b3cb3e9a3..b711f8895e8161ab21121f45d7ec1c73baed3d6f 100644 (file)
@@ -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'
index 997c015d8b861928b4ad160f2c66cb88360e9cff..db47c15ad5ef73425904c1a0c93e55ac0c92e1cc 100644 (file)
@@ -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]
index 56ff85f5eea0014a6280fda9c5c4c0f665735234..8be4314d5bb771eadd43fff307a43c9fcd6c792e 100644 (file)
@@ -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]
       }
     }
index 958e470805c5fc7a6d19bda02a6b8901d3aa9095..c06def0a992e5b3a2965e8d552823f45f7a7de7c 100644 (file)
@@ -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<string, unknown>,
index f32988096ba6c8a462d0ed9562d003ca4c7bdbc1..fb355ac399a16121ad01ab46e758bea6aaed0261 100644 (file)
@@ -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.