diff --git a/packages/runtime-core/__tests__/apiSetupContext.spec.ts b/packages/runtime-core/__tests__/apiSetupContext.spec.ts
index d0602fc99..759e78dd7 100644
--- a/packages/runtime-core/__tests__/apiSetupContext.spec.ts
+++ b/packages/runtime-core/__tests__/apiSetupContext.spec.ts
@@ -74,7 +74,7 @@ describe('api: setup context', () => {
expect(dummy).toBe(1)
})
- it('setup props should resolve the correct types from props object', async () => {
+ it.only('setup props should resolve the correct types from props object', async () => {
const count = ref(0)
let dummy
diff --git a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts
index 8ddaa644d..dfbf5d595 100644
--- a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts
+++ b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts
@@ -8,8 +8,11 @@ import {
onUpdated,
createComponent
} from '@vue/runtime-dom'
+import { mockWarn } from '@vue/runtime-test'
describe('attribute fallthrough', () => {
+ mockWarn()
+
it('everything should be in props when component has no declared props', async () => {
const click = jest.fn()
const childUpdated = jest.fn()
@@ -75,7 +78,7 @@ describe('attribute fallthrough', () => {
expect(node.style.fontWeight).toBe('bold')
})
- it('should separate in attrs when component has declared props', async () => {
+ it('should implicitly fallthrough on single root nodes', async () => {
const click = jest.fn()
const childUpdated = jest.fn()
@@ -103,18 +106,15 @@ describe('attribute fallthrough', () => {
props: {
foo: Number
},
- setup(props, { attrs }) {
+ setup(props) {
onUpdated(childUpdated)
return () =>
h(
'div',
- mergeProps(
- {
- class: 'c2',
- style: { fontWeight: 'bold' }
- },
- attrs
- ),
+ {
+ class: 'c2',
+ style: { fontWeight: 'bold' }
+ },
props.foo
)
}
@@ -147,7 +147,7 @@ describe('attribute fallthrough', () => {
expect(node.hasAttribute('foo')).toBe(false)
})
- it('should fallthrough on multi-nested components', async () => {
+ it('should fallthrough for nested components', async () => {
const click = jest.fn()
const childUpdated = jest.fn()
const grandChildUpdated = jest.fn()
@@ -183,18 +183,15 @@ describe('attribute fallthrough', () => {
props: {
foo: Number
},
- setup(props, { attrs }) {
+ setup(props) {
onUpdated(grandChildUpdated)
return () =>
h(
'div',
- mergeProps(
- {
- class: 'c2',
- style: { fontWeight: 'bold' }
- },
- attrs
- ),
+ {
+ class: 'c2',
+ style: { fontWeight: 'bold' }
+ },
props.foo
)
}
@@ -227,4 +224,104 @@ describe('attribute fallthrough', () => {
expect(node.hasAttribute('foo')).toBe(false)
})
+
+ it('should not fallthrough with inheritAttrs: false', () => {
+ const Parent = {
+ render() {
+ return h(Child, { foo: 1, class: 'parent' })
+ }
+ }
+
+ const Child = createComponent({
+ props: ['foo'],
+ inheritAttrs: false,
+ render() {
+ return h('div', this.foo)
+ }
+ })
+
+ const root = document.createElement('div')
+ document.body.appendChild(root)
+ render(h(Parent), root)
+
+ // should not contain class
+ expect(root.innerHTML).toMatch(`
1
`)
+ })
+
+ it('explicit spreading with inheritAttrs: false', () => {
+ const Parent = {
+ render() {
+ return h(Child, { foo: 1, class: 'parent' })
+ }
+ }
+
+ const Child = createComponent({
+ props: ['foo'],
+ inheritAttrs: false,
+ render() {
+ return h(
+ 'div',
+ mergeProps(
+ {
+ class: 'child'
+ },
+ this.$attrs
+ ),
+ this.foo
+ )
+ }
+ })
+
+ const root = document.createElement('div')
+ document.body.appendChild(root)
+ render(h(Parent), root)
+
+ // should merge parent/child classes
+ expect(root.innerHTML).toMatch(`1
`)
+ })
+
+ it('should warn when fallthrough fails on non-single-root', () => {
+ const Parent = {
+ render() {
+ return h(Child, { foo: 1, class: 'parent' })
+ }
+ }
+
+ const Child = createComponent({
+ props: ['foo'],
+ render() {
+ return [h('div'), h('div')]
+ }
+ })
+
+ const root = document.createElement('div')
+ document.body.appendChild(root)
+ render(h(Parent), root)
+
+ expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
+ })
+
+ it('should not warn when $attrs is used during render', () => {
+ const Parent = {
+ render() {
+ return h(Child, { foo: 1, class: 'parent' })
+ }
+ }
+
+ const Child = createComponent({
+ props: ['foo'],
+ render() {
+ return [h('div'), h('div', this.$attrs)]
+ }
+ })
+
+ const root = document.createElement('div')
+ document.body.appendChild(root)
+ render(h(Parent), root)
+
+ expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
+ expect(root.innerHTML).toBe(
+ ``
+ )
+ })
})
diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts
index b8df2fdea..9bf408b29 100644
--- a/packages/runtime-core/src/apiOptions.ts
+++ b/packages/runtime-core/src/apiOptions.ts
@@ -62,6 +62,7 @@ export interface ComponentOptionsBase<
render?: Function
components?: Record
directives?: Record
+ inheritAttrs?: boolean
}
export type ComponentOptionsWithoutProps<
@@ -80,7 +81,7 @@ export type ComponentOptionsWithArrayProps<
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
- Props = { [key in PropNames]?: unknown }
+ Props = { [key in PropNames]?: any }
> = ComponentOptionsBase & {
props: PropNames[]
} & ThisType>
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index b0debfeca..adaf95834 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -37,6 +37,7 @@ export type Data = { [key: string]: unknown }
export interface FunctionalComponent {
(props: P, ctx: SetupContext): VNodeChild
props?: ComponentPropsOptions
+ inheritAttrs?: boolean
displayName?: string
}
diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts
index 146e0ff3c..4490d2019 100644
--- a/packages/runtime-core/src/componentProps.ts
+++ b/packages/runtime-core/src/componentProps.ts
@@ -202,7 +202,7 @@ export function resolveProps(
instance.attrs = options
? __DEV__ && attrs != null
? readonly(attrs)
- : attrs!
+ : attrs || EMPTY_OBJ
: instance.props
}
diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts
index cf8448cbb..e39022c15 100644
--- a/packages/runtime-core/src/componentProxy.ts
+++ b/packages/runtime-core/src/componentProxy.ts
@@ -11,7 +11,10 @@ import {
import { UnwrapRef, ReactiveEffect } from '@vue/reactivity'
import { warn } from './warning'
import { Slots } from './componentSlots'
-import { currentRenderingInstance } from './componentRenderUtils'
+import {
+ currentRenderingInstance,
+ markAttrsAccessed
+} from './componentRenderUtils'
// public properties exposed on the proxy, which is used as the render context
// in templates (as `this` in the render option)
@@ -109,6 +112,9 @@ export const PublicInstanceProxyHandlers: ProxyHandler = {
} else if (key === '$el') {
return target.vnode.el
} else if (hasOwn(publicPropertiesMap, key)) {
+ if (__DEV__ && key === '$attrs') {
+ markAttrsAccessed()
+ }
return target[publicPropertiesMap[key]]
}
// methods are only exposed when options are supported
diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts
index b994f7065..cfe9e9a61 100644
--- a/packages/runtime-core/src/componentRenderUtils.ts
+++ b/packages/runtime-core/src/componentRenderUtils.ts
@@ -3,15 +3,31 @@ import {
FunctionalComponent,
Data
} from './component'
-import { VNode, normalizeVNode, createVNode, Comment } from './vnode'
+import {
+ VNode,
+ normalizeVNode,
+ createVNode,
+ Comment,
+ cloneVNode
+} from './vnode'
import { ShapeFlags } from './shapeFlags'
import { handleError, ErrorCodes } from './errorHandling'
-import { PatchFlags } from '@vue/shared'
+import { PatchFlags, EMPTY_OBJ } from '@vue/shared'
+import { warn } from './warning'
// mark the current rendering instance for asset resolution (e.g.
// resolveComponent, resolveDirective) during render
export let currentRenderingInstance: ComponentInternalInstance | null = null
+// dev only flag to track whether $attrs was used during render.
+// If $attrs was used during render then the warning for failed attrs
+// fallthrough can be suppressed.
+let accessedAttrs: boolean = false
+
+export function markAttrsAccessed() {
+ accessedAttrs = true
+}
+
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
@@ -27,6 +43,9 @@ export function renderComponentRoot(
let result
currentRenderingInstance = instance
+ if (__DEV__) {
+ accessedAttrs = false
+ }
try {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
result = normalizeVNode(instance.render!.call(renderProxy))
@@ -43,6 +62,27 @@ export function renderComponentRoot(
: render(props, null as any /* we know it doesn't need it */)
)
}
+
+ // attr merging
+ if (
+ Component.props != null &&
+ Component.inheritAttrs !== false &&
+ attrs !== EMPTY_OBJ &&
+ Object.keys(attrs).length
+ ) {
+ if (
+ result.shapeFlag & ShapeFlags.ELEMENT ||
+ result.shapeFlag & ShapeFlags.COMPONENT
+ ) {
+ result = cloneVNode(result, attrs)
+ } else if (__DEV__ && !accessedAttrs) {
+ warn(
+ `Extraneous non-props attributes (${Object.keys(attrs).join(',')}) ` +
+ `were passed to component but could not be automatically inhertied ` +
+ `because component renders fragment or text root nodes.`
+ )
+ }
+ }
} catch (err) {
handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
result = createVNode(Comment)
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index ad1d3ad0c..fec19fe12 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -229,11 +229,15 @@ export function createVNode(
return vnode
}
-export function cloneVNode(vnode: VNode): VNode {
+export function cloneVNode(vnode: VNode, extraProps?: Data): VNode {
return {
_isVNode: true,
type: vnode.type,
- props: vnode.props,
+ props: extraProps
+ ? vnode.props
+ ? mergeProps(vnode.props, extraProps)
+ : extraProps
+ : vnode.props,
key: vnode.key,
ref: vnode.ref,
children: vnode.children,