fix(v-model): v-model listeners should not fallthrough to plain element root

fix #1643
This commit is contained in:
Evan You 2020-07-21 14:17:48 -04:00
parent 304830a764
commit c852bf18d7
4 changed files with 71 additions and 53 deletions

View File

@ -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 ||

View File

@ -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) {

View File

@ -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)) {

View File

@ -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) => {