Skip to content

Commit

Permalink
fix(v-model): v-model listeners should not fallthrough to plain eleme…
Browse files Browse the repository at this point in the history
…nt root

fix #1643
  • Loading branch information
yyx990803 committed Jul 21, 2020
1 parent 304830a commit c852bf1
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 52 deletions.
108 changes: 63 additions & 45 deletions packages/runtime-core/src/componentRenderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Expand All @@ -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))
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)
}
} 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.`
)
}
}
}
}
Expand Down Expand Up @@ -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)) {
Expand All @@ -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 ||
Expand Down
8 changes: 3 additions & 5 deletions packages/runtime-core/src/vnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
normalizeStyle,
PatchFlags,
ShapeFlags,
SlotFlags
SlotFlags,
isOn
} from '@vue/shared'
import {
ComponentInternalInstance,
Expand Down Expand Up @@ -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++) {
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime-dom/src/patchProp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]/
Expand Down Expand Up @@ -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)) {
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T>(arr: T[], el: T) => {
Expand Down

0 comments on commit c852bf1

Please sign in to comment.