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…
Reference in New Issue
Block a user