]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(v-model): v-model listeners should not fallthrough to plain element root
authorEvan You <yyx990803@gmail.com>
Tue, 21 Jul 2020 18:17:48 +0000 (14:17 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 21 Jul 2020 18:17:48 +0000 (14:17 -0400)
fix #1643

packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/vnode.ts
packages/runtime-dom/src/patchProp.ts
packages/shared/src/index.ts

index dad54bcadcae8367ad038be6a6d90f885057c7df..5f3910f7cc059941c0afe9e53203d49f18c0e90f 100644 (file)
@@ -14,7 +14,7 @@ import {
   isVNode
 } from './vnode'
 import { handleError, ErrorCodes } from './errorHandling'
-import { PatchFlags, ShapeFlags, isOn } from '@vue/shared'
+import { PatchFlags, ShapeFlags, isOn, isModelListener } from '@vue/shared'
 import { warn } from './warning'
 import { isHmrUpdating } from './hmr'
 
@@ -104,7 +104,9 @@ export function renderComponentRoot(
             )
           : render(props, null as any /* we know it doesn't need it */)
       )
-      fallthroughAttrs = Component.props ? attrs : getFallthroughAttrs(attrs)
+      fallthroughAttrs = Component.props
+        ? attrs
+        : getFunctionalFallthrough(attrs)
     }
 
     // attr merging
@@ -116,50 +118,56 @@ export function renderComponentRoot(
       ;[root, setRoot] = getChildRoot(result)
     }
 
-    if (
-      Component.inheritAttrs !== false &&
-      fallthroughAttrs &&
-      Object.keys(fallthroughAttrs).length
-    ) {
-      if (
-        root.shapeFlag & ShapeFlags.ELEMENT ||
-        root.shapeFlag & ShapeFlags.COMPONENT
-      ) {
-        root = cloneVNode(root, fallthroughAttrs)
-      } else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
-        const allAttrs = Object.keys(attrs)
-        const eventAttrs: string[] = []
-        const extraAttrs: string[] = []
-        for (let i = 0, l = allAttrs.length; i < l; i++) {
-          const key = allAttrs[i]
-          if (isOn(key)) {
-            // ignore v-model handlers when they fail to fallthrough
-            if (!key.startsWith('onUpdate:')) {
-              // remove `on`, lowercase first letter to reflect event casing
-              // accurately
-              eventAttrs.push(key[2].toLowerCase() + key.slice(3))
+    if (Component.inheritAttrs !== false && fallthroughAttrs) {
+      const keys = Object.keys(fallthroughAttrs)
+      const { shapeFlag } = root
+      if (keys.length) {
+        if (
+          shapeFlag & ShapeFlags.ELEMENT ||
+          shapeFlag & ShapeFlags.COMPONENT
+        ) {
+          if (shapeFlag & ShapeFlags.ELEMENT && keys.some(isModelListener)) {
+            // #1643, #1543
+            // component v-model listeners should only fallthrough for component
+            // HOCs
+            fallthroughAttrs = filterModelListeners(fallthroughAttrs)
+          }
+          root = cloneVNode(root, fallthroughAttrs)
+        } else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
+          const allAttrs = Object.keys(attrs)
+          const eventAttrs: string[] = []
+          const extraAttrs: string[] = []
+          for (let i = 0, l = allAttrs.length; i < l; i++) {
+            const key = allAttrs[i]
+            if (isOn(key)) {
+              // ignore v-model handlers when they fail to fallthrough
+              if (!isModelListener(key)) {
+                // remove `on`, lowercase first letter to reflect event casing
+                // accurately
+                eventAttrs.push(key[2].toLowerCase() + key.slice(3))
+              }
+            } else {
+              extraAttrs.push(key)
             }
-          } else {
-            extraAttrs.push(key)
           }
-        }
-        if (extraAttrs.length) {
-          warn(
-            `Extraneous non-props attributes (` +
-              `${extraAttrs.join(', ')}) ` +
-              `were passed to component but could not be automatically inherited ` +
-              `because component renders fragment or text root nodes.`
-          )
-        }
-        if (eventAttrs.length) {
-          warn(
-            `Extraneous non-emits event listeners (` +
-              `${eventAttrs.join(', ')}) ` +
-              `were passed to component but could not be automatically inherited ` +
-              `because component renders fragment or text root nodes. ` +
-              `If the listener is intended to be a component custom event listener only, ` +
-              `declare it using the "emits" option.`
-          )
+          if (extraAttrs.length) {
+            warn(
+              `Extraneous non-props attributes (` +
+                `${extraAttrs.join(', ')}) ` +
+                `were passed to component but could not be automatically inherited ` +
+                `because component renders fragment or text root nodes.`
+            )
+          }
+          if (eventAttrs.length) {
+            warn(
+              `Extraneous non-emits event listeners (` +
+                `${eventAttrs.join(', ')}) ` +
+                `were passed to component but could not be automatically inherited ` +
+                `because component renders fragment or text root nodes. ` +
+                `If the listener is intended to be a component custom event listener only, ` +
+                `declare it using the "emits" option.`
+            )
+          }
         }
       }
     }
@@ -246,7 +254,7 @@ const getChildRoot = (
   return [normalizeVNode(childRoot), setRoot]
 }
 
-const getFallthroughAttrs = (attrs: Data): Data | undefined => {
+const getFunctionalFallthrough = (attrs: Data): Data | undefined => {
   let res: Data | undefined
   for (const key in attrs) {
     if (key === 'class' || key === 'style' || isOn(key)) {
@@ -256,6 +264,16 @@ const getFallthroughAttrs = (attrs: Data): Data | undefined => {
   return res
 }
 
+const filterModelListeners = (attrs: Data): Data => {
+  const res: Data = {}
+  for (const key in attrs) {
+    if (!isModelListener(key)) {
+      res[key] = attrs[key]
+    }
+  }
+  return res
+}
+
 const isElementRoot = (vnode: VNode) => {
   return (
     vnode.shapeFlag & ShapeFlags.COMPONENT ||
index ba0a4cafb52d60f1988435c57c1c34ab518531fa..07dafcebac1a52fc2cc114715098b2451f625454 100644 (file)
@@ -9,7 +9,8 @@ import {
   normalizeStyle,
   PatchFlags,
   ShapeFlags,
-  SlotFlags
+  SlotFlags,
+  isOn
 } from '@vue/shared'
 import {
   ComponentInternalInstance,
@@ -583,8 +584,6 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
   vnode.shapeFlag |= type
 }
 
-const handlersRE = /^on|^vnode/
-
 export function mergeProps(...args: (Data & VNodeProps)[]) {
   const ret = extend({}, args[0])
   for (let i = 1; i < args.length; i++) {
@@ -596,8 +595,7 @@ export function mergeProps(...args: (Data & VNodeProps)[]) {
         }
       } else if (key === 'style') {
         ret.style = normalizeStyle([ret.style, toMerge.style])
-      } else if (handlersRE.test(key)) {
-        // on*, vnode*
+      } else if (isOn(key)) {
         const existing = ret[key]
         const incoming = toMerge[key]
         if (existing !== incoming) {
index e860cb114ad4180aa69700e6683ca4dc1af434b5..a7c27730981e92b5b682fd7ec532e6c8c1e65bf0 100644 (file)
@@ -3,7 +3,7 @@ import { patchStyle } from './modules/style'
 import { patchAttr } from './modules/attrs'
 import { patchDOMProp } from './modules/props'
 import { patchEvent } from './modules/events'
-import { isOn, isString, isFunction } from '@vue/shared'
+import { isOn, isString, isFunction, isModelListener } from '@vue/shared'
 import { RendererOptions } from '@vue/runtime-core'
 
 const nativeOnRE = /^on[a-z]/
@@ -35,7 +35,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
     default:
       if (isOn(key)) {
         // ignore v-model listeners
-        if (!key.startsWith('onUpdate:')) {
+        if (!isModelListener(key)) {
           patchEvent(el, key, prevValue, nextValue, parentComponent)
         }
       } else if (shouldSetAsProp(el, key, nextValue, isSVG)) {
index be0a9758a13ec5c1bd49ee6e335624868d94bb19..c0655ba5a51ac1dc0199bc7f9443607119e32ac3 100644 (file)
@@ -41,6 +41,8 @@ export const NO = () => false
 const onRE = /^on[^a-z]/
 export const isOn = (key: string) => onRE.test(key)
 
+export const isModelListener = (key: string) => key.startsWith('onUpdate:')
+
 export const extend = Object.assign
 
 export const remove = <T>(arr: T[], el: T) => {