diff --git a/packages/core/src/componentProps.ts b/packages/core/src/componentProps.ts index bfcec057..069f5cdc 100644 --- a/packages/core/src/componentProps.ts +++ b/packages/core/src/componentProps.ts @@ -1,4 +1,4 @@ -import { EMPTY_OBJ, isReservedProp } from './utils' +import { EMPTY_OBJ } from './utils' import { Component, ComponentClass, MountedComponent } from './component' import { immutable, unwrap, lock, unlock } from '@vue/observer' import { @@ -38,29 +38,42 @@ export function updateProps(instance: MountedComponent, nextProps: Data) { // on every component vnode is guarunteed to be a fresh object. export function normalizeComponentProps( raw: any, - options: ComponentPropsOptions, + rawOptions: ComponentPropsOptions, Component: ComponentClass ): Data { - if (!raw) { + const hasDeclaredProps = rawOptions !== void 0 + const options = (hasDeclaredProps && + normalizePropsOptions(rawOptions)) as NormalizedPropsOptions + if (!raw && !hasDeclaredProps) { return EMPTY_OBJ } const res: Data = {} - const normalizedOptions = options && normalizePropsOptions(options) - for (const key in raw) { - if (isReservedProp(key)) { - continue - } - if (__DEV__ && normalizedOptions != null) { - validateProp(key, raw[key], normalizedOptions[key], Component) - } else { - res[key] = raw[key] + if (raw) { + for (const key in raw) { + if (key === 'key' || key === 'ref' || key === 'slot') { + continue + } + if (hasDeclaredProps) { + if (options.hasOwnProperty(key)) { + if (__DEV__) { + validateProp(key, raw[key], options[key], Component) + } + res[key] = raw[key] + } else { + // when props are explicitly declared, any non-matching prop is + // extracted into attrs instead. + ;(res.attrs || (res.attrs = {}))[key] = raw[key] + } + } else { + res[key] = raw[key] + } } } // set default values - if (normalizedOptions != null) { - for (const key in normalizedOptions) { + if (hasDeclaredProps) { + for (const key in options) { if (res[key] === void 0) { - const opt = normalizedOptions[key] + const opt = options[key] if (opt != null && opt.hasOwnProperty('default')) { const defaultValue = opt.default res[key] = diff --git a/packages/core/src/componentUtils.ts b/packages/core/src/componentUtils.ts index 093e5800..ff893b66 100644 --- a/packages/core/src/componentUtils.ts +++ b/packages/core/src/componentUtils.ts @@ -103,30 +103,31 @@ export function normalizeComponentRoot( componentVNode && (flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT) ) { - const parentData = componentVNode.data || EMPTY_OBJ - const childData = vnode.data || EMPTY_OBJ - let extraData: any = null - for (const key in parentData) { - // class/style bindings on parentVNode are merged down to child - // component root. - if (key === 'class') { - ;(extraData || (extraData = {})).class = childData.class - ? [].concat(childData.class, parentData.class) - : parentData.class - } else if (key === 'style') { - ;(extraData || (extraData = {})).style = childData.style - ? [].concat(childData.style, parentData.style) - : parentData.style - } else if (key.startsWith('nativeOn')) { - // nativeOn* handlers are merged down to child root as native listeners - const event = 'on' + key.slice(8) - ;(extraData || (extraData = {}))[event] = childData.event - ? [].concat(childData.event, parentData[key]) - : parentData[key] + const parentData = componentVNode.data + if (parentData != null) { + let extraData: any = null + for (const key in parentData) { + // attrs/class/style bindings on parentVNode are merged down to child + // component root, + // nativeOn* handlers are merged to child root as normal on* handlers. + // cloneVNode contains special logic for merging these props with + // existing values. + if (key === 'attrs') { + extraData = extraData || {} + const { attrs } = parentData + for (const attr in attrs) { + extraData[attr] = attrs[attr] + } + } else if (key === 'class' || key === 'style') { + ;(extraData || (extraData = {}))[key] = parentData[key] + } else if (key.startsWith('nativeOn')) { + ;(extraData || (extraData = {}))['on' + key.slice(8)] = + parentData[key] + } + } + if (extraData) { + vnode = cloneVNode(vnode, extraData) } - } - if (extraData) { - vnode = cloneVNode(vnode, extraData) } if (vnode.el) { vnode = cloneVNode(vnode) diff --git a/packages/core/src/vdom.ts b/packages/core/src/vdom.ts index 519ea095..3b632c21 100644 --- a/packages/core/src/vdom.ts +++ b/packages/core/src/vdom.ts @@ -258,7 +258,28 @@ export function cloneVNode(vnode: VNode, extraData?: VNodeData): VNode { } } for (const key in extraData) { - clonedData[key] = extraData[key] + const existing = clonedData[key] + const extra = extraData[key] + if (extra === void 0) { + continue + } + // special merge behavior for attrs / class / style / on. + let isOn + if (key === 'attrs') { + clonedData.attrs = existing + ? Object.assign({}, existing, extra) + : extra + } else if ( + key === 'class' || + key === 'style' || + (isOn = key.startsWith('on')) + ) { + // all three props can handle array format, so we simply merge them + // by concating. + clonedData[key] = existing ? [].concat(existing, extra) : extra + } else { + clonedData[key] = extra + } } } return createVNode(