fix(v-model): v-model listeners should not fallthrough to plain element root
fix #1643
This commit is contained in:
		
							parent
							
								
									304830a764
								
							
						
					
					
						commit
						c852bf18d7
					
				| @ -14,7 +14,7 @@ import { | |||||||
|   isVNode |   isVNode | ||||||
| } from './vnode' | } from './vnode' | ||||||
| import { handleError, ErrorCodes } from './errorHandling' | 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 { warn } from './warning' | ||||||
| import { isHmrUpdating } from './hmr' | import { isHmrUpdating } from './hmr' | ||||||
| 
 | 
 | ||||||
| @ -104,7 +104,9 @@ export function renderComponentRoot( | |||||||
|             ) |             ) | ||||||
|           : render(props, null as any /* we know it doesn't need it */) |           : 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
 |     // attr merging
 | ||||||
| @ -116,50 +118,56 @@ export function renderComponentRoot( | |||||||
|       ;[root, setRoot] = getChildRoot(result) |       ;[root, setRoot] = getChildRoot(result) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if ( |     if (Component.inheritAttrs !== false && fallthroughAttrs) { | ||||||
|       Component.inheritAttrs !== false && |       const keys = Object.keys(fallthroughAttrs) | ||||||
|       fallthroughAttrs && |       const { shapeFlag } = root | ||||||
|       Object.keys(fallthroughAttrs).length |       if (keys.length) { | ||||||
|     ) { |         if ( | ||||||
|       if ( |           shapeFlag & ShapeFlags.ELEMENT || | ||||||
|         root.shapeFlag & ShapeFlags.ELEMENT || |           shapeFlag & ShapeFlags.COMPONENT | ||||||
|         root.shapeFlag & ShapeFlags.COMPONENT |         ) { | ||||||
|       ) { |           if (shapeFlag & ShapeFlags.ELEMENT && keys.some(isModelListener)) { | ||||||
|         root = cloneVNode(root, fallthroughAttrs) |             // #1643, #1543
 | ||||||
|       } else if (__DEV__ && !accessedAttrs && root.type !== Comment) { |             // component v-model listeners should only fallthrough for component
 | ||||||
|         const allAttrs = Object.keys(attrs) |             // HOCs
 | ||||||
|         const eventAttrs: string[] = [] |             fallthroughAttrs = filterModelListeners(fallthroughAttrs) | ||||||
|         const extraAttrs: string[] = [] |           } | ||||||
|         for (let i = 0, l = allAttrs.length; i < l; i++) { |           root = cloneVNode(root, fallthroughAttrs) | ||||||
|           const key = allAttrs[i] |         } else if (__DEV__ && !accessedAttrs && root.type !== Comment) { | ||||||
|           if (isOn(key)) { |           const allAttrs = Object.keys(attrs) | ||||||
|             // ignore v-model handlers when they fail to fallthrough
 |           const eventAttrs: string[] = [] | ||||||
|             if (!key.startsWith('onUpdate:')) { |           const extraAttrs: string[] = [] | ||||||
|               // remove `on`, lowercase first letter to reflect event casing
 |           for (let i = 0, l = allAttrs.length; i < l; i++) { | ||||||
|               // accurately
 |             const key = allAttrs[i] | ||||||
|               eventAttrs.push(key[2].toLowerCase() + key.slice(3)) |             if (isOn(key)) { | ||||||
|             } |               // ignore v-model handlers when they fail to fallthrough
 | ||||||
|           } else { |               if (!isModelListener(key)) { | ||||||
|             extraAttrs.push(key) |                 // remove `on`, lowercase first letter to reflect event casing
 | ||||||
|  |                 // accurately
 | ||||||
|  |                 eventAttrs.push(key[2].toLowerCase() + key.slice(3)) | ||||||
|  |               } | ||||||
|  |             } 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] |   return [normalizeVNode(childRoot), setRoot] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const getFallthroughAttrs = (attrs: Data): Data | undefined => { | const getFunctionalFallthrough = (attrs: Data): Data | undefined => { | ||||||
|   let res: Data | undefined |   let res: Data | undefined | ||||||
|   for (const key in attrs) { |   for (const key in attrs) { | ||||||
|     if (key === 'class' || key === 'style' || isOn(key)) { |     if (key === 'class' || key === 'style' || isOn(key)) { | ||||||
| @ -256,6 +264,16 @@ const getFallthroughAttrs = (attrs: Data): Data | undefined => { | |||||||
|   return res |   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) => { | const isElementRoot = (vnode: VNode) => { | ||||||
|   return ( |   return ( | ||||||
|     vnode.shapeFlag & ShapeFlags.COMPONENT || |     vnode.shapeFlag & ShapeFlags.COMPONENT || | ||||||
|  | |||||||
| @ -9,7 +9,8 @@ import { | |||||||
|   normalizeStyle, |   normalizeStyle, | ||||||
|   PatchFlags, |   PatchFlags, | ||||||
|   ShapeFlags, |   ShapeFlags, | ||||||
|   SlotFlags |   SlotFlags, | ||||||
|  |   isOn | ||||||
| } from '@vue/shared' | } from '@vue/shared' | ||||||
| import { | import { | ||||||
|   ComponentInternalInstance, |   ComponentInternalInstance, | ||||||
| @ -583,8 +584,6 @@ export function normalizeChildren(vnode: VNode, children: unknown) { | |||||||
|   vnode.shapeFlag |= type |   vnode.shapeFlag |= type | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const handlersRE = /^on|^vnode/ |  | ||||||
| 
 |  | ||||||
| export function mergeProps(...args: (Data & VNodeProps)[]) { | export function mergeProps(...args: (Data & VNodeProps)[]) { | ||||||
|   const ret = extend({}, args[0]) |   const ret = extend({}, args[0]) | ||||||
|   for (let i = 1; i < args.length; i++) { |   for (let i = 1; i < args.length; i++) { | ||||||
| @ -596,8 +595,7 @@ export function mergeProps(...args: (Data & VNodeProps)[]) { | |||||||
|         } |         } | ||||||
|       } else if (key === 'style') { |       } else if (key === 'style') { | ||||||
|         ret.style = normalizeStyle([ret.style, toMerge.style]) |         ret.style = normalizeStyle([ret.style, toMerge.style]) | ||||||
|       } else if (handlersRE.test(key)) { |       } else if (isOn(key)) { | ||||||
|         // on*, vnode*
 |  | ||||||
|         const existing = ret[key] |         const existing = ret[key] | ||||||
|         const incoming = toMerge[key] |         const incoming = toMerge[key] | ||||||
|         if (existing !== incoming) { |         if (existing !== incoming) { | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ import { patchStyle } from './modules/style' | |||||||
| import { patchAttr } from './modules/attrs' | import { patchAttr } from './modules/attrs' | ||||||
| import { patchDOMProp } from './modules/props' | import { patchDOMProp } from './modules/props' | ||||||
| import { patchEvent } from './modules/events' | 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' | import { RendererOptions } from '@vue/runtime-core' | ||||||
| 
 | 
 | ||||||
| const nativeOnRE = /^on[a-z]/ | const nativeOnRE = /^on[a-z]/ | ||||||
| @ -35,7 +35,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = ( | |||||||
|     default: |     default: | ||||||
|       if (isOn(key)) { |       if (isOn(key)) { | ||||||
|         // ignore v-model listeners
 |         // ignore v-model listeners
 | ||||||
|         if (!key.startsWith('onUpdate:')) { |         if (!isModelListener(key)) { | ||||||
|           patchEvent(el, key, prevValue, nextValue, parentComponent) |           patchEvent(el, key, prevValue, nextValue, parentComponent) | ||||||
|         } |         } | ||||||
|       } else if (shouldSetAsProp(el, key, nextValue, isSVG)) { |       } else if (shouldSetAsProp(el, key, nextValue, isSVG)) { | ||||||
|  | |||||||
| @ -41,6 +41,8 @@ export const NO = () => false | |||||||
| const onRE = /^on[^a-z]/ | const onRE = /^on[^a-z]/ | ||||||
| export const isOn = (key: string) => onRE.test(key) | export const isOn = (key: string) => onRE.test(key) | ||||||
| 
 | 
 | ||||||
|  | export const isModelListener = (key: string) => key.startsWith('onUpdate:') | ||||||
|  | 
 | ||||||
| export const extend = Object.assign | export const extend = Object.assign | ||||||
| 
 | 
 | ||||||
| export const remove = <T>(arr: T[], el: T) => { | export const remove = <T>(arr: T[], el: T) => { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user