Skip to content

Commit c852bf1

Browse files
committed
fix(v-model): v-model listeners should not fallthrough to plain element root
fix #1643
1 parent 304830a commit c852bf1

File tree

4 files changed

+70
-52
lines changed

4 files changed

+70
-52
lines changed

packages/runtime-core/src/componentRenderUtils.ts

+63-45
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
isVNode
1515
} from './vnode'
1616
import { handleError, ErrorCodes } from './errorHandling'
17-
import { PatchFlags, ShapeFlags, isOn } from '@vue/shared'
17+
import { PatchFlags, ShapeFlags, isOn, isModelListener } from '@vue/shared'
1818
import { warn } from './warning'
1919
import { isHmrUpdating } from './hmr'
2020

@@ -104,7 +104,9 @@ export function renderComponentRoot(
104104
)
105105
: render(props, null as any /* we know it doesn't need it */)
106106
)
107-
fallthroughAttrs = Component.props ? attrs : getFallthroughAttrs(attrs)
107+
fallthroughAttrs = Component.props
108+
? attrs
109+
: getFunctionalFallthrough(attrs)
108110
}
109111

110112
// attr merging
@@ -116,50 +118,56 @@ export function renderComponentRoot(
116118
;[root, setRoot] = getChildRoot(result)
117119
}
118120

119-
if (
120-
Component.inheritAttrs !== false &&
121-
fallthroughAttrs &&
122-
Object.keys(fallthroughAttrs).length
123-
) {
124-
if (
125-
root.shapeFlag & ShapeFlags.ELEMENT ||
126-
root.shapeFlag & ShapeFlags.COMPONENT
127-
) {
128-
root = cloneVNode(root, fallthroughAttrs)
129-
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
130-
const allAttrs = Object.keys(attrs)
131-
const eventAttrs: string[] = []
132-
const extraAttrs: string[] = []
133-
for (let i = 0, l = allAttrs.length; i < l; i++) {
134-
const key = allAttrs[i]
135-
if (isOn(key)) {
136-
// ignore v-model handlers when they fail to fallthrough
137-
if (!key.startsWith('onUpdate:')) {
138-
// remove `on`, lowercase first letter to reflect event casing
139-
// accurately
140-
eventAttrs.push(key[2].toLowerCase() + key.slice(3))
121+
if (Component.inheritAttrs !== false && fallthroughAttrs) {
122+
const keys = Object.keys(fallthroughAttrs)
123+
const { shapeFlag } = root
124+
if (keys.length) {
125+
if (
126+
shapeFlag & ShapeFlags.ELEMENT ||
127+
shapeFlag & ShapeFlags.COMPONENT
128+
) {
129+
if (shapeFlag & ShapeFlags.ELEMENT && keys.some(isModelListener)) {
130+
// #1643, #1543
131+
// component v-model listeners should only fallthrough for component
132+
// HOCs
133+
fallthroughAttrs = filterModelListeners(fallthroughAttrs)
134+
}
135+
root = cloneVNode(root, fallthroughAttrs)
136+
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
137+
const allAttrs = Object.keys(attrs)
138+
const eventAttrs: string[] = []
139+
const extraAttrs: string[] = []
140+
for (let i = 0, l = allAttrs.length; i < l; i++) {
141+
const key = allAttrs[i]
142+
if (isOn(key)) {
143+
// ignore v-model handlers when they fail to fallthrough
144+
if (!isModelListener(key)) {
145+
// remove `on`, lowercase first letter to reflect event casing
146+
// accurately
147+
eventAttrs.push(key[2].toLowerCase() + key.slice(3))
148+
}
149+
} else {
150+
extraAttrs.push(key)
141151
}
142-
} else {
143-
extraAttrs.push(key)
144152
}
145-
}
146-
if (extraAttrs.length) {
147-
warn(
148-
`Extraneous non-props attributes (` +
149-
`${extraAttrs.join(', ')}) ` +
150-
`were passed to component but could not be automatically inherited ` +
151-
`because component renders fragment or text root nodes.`
152-
)
153-
}
154-
if (eventAttrs.length) {
155-
warn(
156-
`Extraneous non-emits event listeners (` +
157-
`${eventAttrs.join(', ')}) ` +
158-
`were passed to component but could not be automatically inherited ` +
159-
`because component renders fragment or text root nodes. ` +
160-
`If the listener is intended to be a component custom event listener only, ` +
161-
`declare it using the "emits" option.`
162-
)
153+
if (extraAttrs.length) {
154+
warn(
155+
`Extraneous non-props attributes (` +
156+
`${extraAttrs.join(', ')}) ` +
157+
`were passed to component but could not be automatically inherited ` +
158+
`because component renders fragment or text root nodes.`
159+
)
160+
}
161+
if (eventAttrs.length) {
162+
warn(
163+
`Extraneous non-emits event listeners (` +
164+
`${eventAttrs.join(', ')}) ` +
165+
`were passed to component but could not be automatically inherited ` +
166+
`because component renders fragment or text root nodes. ` +
167+
`If the listener is intended to be a component custom event listener only, ` +
168+
`declare it using the "emits" option.`
169+
)
170+
}
163171
}
164172
}
165173
}
@@ -246,7 +254,7 @@ const getChildRoot = (
246254
return [normalizeVNode(childRoot), setRoot]
247255
}
248256

249-
const getFallthroughAttrs = (attrs: Data): Data | undefined => {
257+
const getFunctionalFallthrough = (attrs: Data): Data | undefined => {
250258
let res: Data | undefined
251259
for (const key in attrs) {
252260
if (key === 'class' || key === 'style' || isOn(key)) {
@@ -256,6 +264,16 @@ const getFallthroughAttrs = (attrs: Data): Data | undefined => {
256264
return res
257265
}
258266

267+
const filterModelListeners = (attrs: Data): Data => {
268+
const res: Data = {}
269+
for (const key in attrs) {
270+
if (!isModelListener(key)) {
271+
res[key] = attrs[key]
272+
}
273+
}
274+
return res
275+
}
276+
259277
const isElementRoot = (vnode: VNode) => {
260278
return (
261279
vnode.shapeFlag & ShapeFlags.COMPONENT ||

packages/runtime-core/src/vnode.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
normalizeStyle,
1010
PatchFlags,
1111
ShapeFlags,
12-
SlotFlags
12+
SlotFlags,
13+
isOn
1314
} from '@vue/shared'
1415
import {
1516
ComponentInternalInstance,
@@ -583,8 +584,6 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
583584
vnode.shapeFlag |= type
584585
}
585586

586-
const handlersRE = /^on|^vnode/
587-
588587
export function mergeProps(...args: (Data & VNodeProps)[]) {
589588
const ret = extend({}, args[0])
590589
for (let i = 1; i < args.length; i++) {
@@ -596,8 +595,7 @@ export function mergeProps(...args: (Data & VNodeProps)[]) {
596595
}
597596
} else if (key === 'style') {
598597
ret.style = normalizeStyle([ret.style, toMerge.style])
599-
} else if (handlersRE.test(key)) {
600-
// on*, vnode*
598+
} else if (isOn(key)) {
601599
const existing = ret[key]
602600
const incoming = toMerge[key]
603601
if (existing !== incoming) {

packages/runtime-dom/src/patchProp.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { patchStyle } from './modules/style'
33
import { patchAttr } from './modules/attrs'
44
import { patchDOMProp } from './modules/props'
55
import { patchEvent } from './modules/events'
6-
import { isOn, isString, isFunction } from '@vue/shared'
6+
import { isOn, isString, isFunction, isModelListener } from '@vue/shared'
77
import { RendererOptions } from '@vue/runtime-core'
88

99
const nativeOnRE = /^on[a-z]/
@@ -35,7 +35,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
3535
default:
3636
if (isOn(key)) {
3737
// ignore v-model listeners
38-
if (!key.startsWith('onUpdate:')) {
38+
if (!isModelListener(key)) {
3939
patchEvent(el, key, prevValue, nextValue, parentComponent)
4040
}
4141
} else if (shouldSetAsProp(el, key, nextValue, isSVG)) {

packages/shared/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export const NO = () => false
4141
const onRE = /^on[^a-z]/
4242
export const isOn = (key: string) => onRE.test(key)
4343

44+
export const isModelListener = (key: string) => key.startsWith('onUpdate:')
45+
4446
export const extend = Object.assign
4547

4648
export const remove = <T>(arr: T[], el: T) => {

0 commit comments

Comments
 (0)