diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index dad54bca..5f3910f7 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -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)) - } - } else { - extraAttrs.push(key) + 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) + } + } + 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 || diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index ba0a4caf..07dafceb 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -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) { diff --git a/packages/runtime-dom/src/patchProp.ts b/packages/runtime-dom/src/patchProp.ts index e860cb11..a7c27730 100644 --- a/packages/runtime-dom/src/patchProp.ts +++ b/packages/runtime-dom/src/patchProp.ts @@ -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)) { diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index be0a9758..c0655ba5 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -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 = (arr: T[], el: T) => {