feat(core): adjust attrs fallthrough behavior
This commit is contained in:
parent
d76cfba7fb
commit
8edfbf9df9
@ -74,7 +74,7 @@ describe('api: setup context', () => {
|
|||||||
expect(dummy).toBe(1)
|
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)
|
const count = ref(0)
|
||||||
let dummy
|
let dummy
|
||||||
|
|
||||||
|
@ -8,8 +8,11 @@ import {
|
|||||||
onUpdated,
|
onUpdated,
|
||||||
createComponent
|
createComponent
|
||||||
} from '@vue/runtime-dom'
|
} from '@vue/runtime-dom'
|
||||||
|
import { mockWarn } from '@vue/runtime-test'
|
||||||
|
|
||||||
describe('attribute fallthrough', () => {
|
describe('attribute fallthrough', () => {
|
||||||
|
mockWarn()
|
||||||
|
|
||||||
it('everything should be in props when component has no declared props', async () => {
|
it('everything should be in props when component has no declared props', async () => {
|
||||||
const click = jest.fn()
|
const click = jest.fn()
|
||||||
const childUpdated = jest.fn()
|
const childUpdated = jest.fn()
|
||||||
@ -75,7 +78,7 @@ describe('attribute fallthrough', () => {
|
|||||||
expect(node.style.fontWeight).toBe('bold')
|
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 click = jest.fn()
|
||||||
const childUpdated = jest.fn()
|
const childUpdated = jest.fn()
|
||||||
|
|
||||||
@ -103,18 +106,15 @@ describe('attribute fallthrough', () => {
|
|||||||
props: {
|
props: {
|
||||||
foo: Number
|
foo: Number
|
||||||
},
|
},
|
||||||
setup(props, { attrs }) {
|
setup(props) {
|
||||||
onUpdated(childUpdated)
|
onUpdated(childUpdated)
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
'div',
|
'div',
|
||||||
mergeProps(
|
{
|
||||||
{
|
class: 'c2',
|
||||||
class: 'c2',
|
style: { fontWeight: 'bold' }
|
||||||
style: { fontWeight: 'bold' }
|
},
|
||||||
},
|
|
||||||
attrs
|
|
||||||
),
|
|
||||||
props.foo
|
props.foo
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -147,7 +147,7 @@ describe('attribute fallthrough', () => {
|
|||||||
expect(node.hasAttribute('foo')).toBe(false)
|
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 click = jest.fn()
|
||||||
const childUpdated = jest.fn()
|
const childUpdated = jest.fn()
|
||||||
const grandChildUpdated = jest.fn()
|
const grandChildUpdated = jest.fn()
|
||||||
@ -183,18 +183,15 @@ describe('attribute fallthrough', () => {
|
|||||||
props: {
|
props: {
|
||||||
foo: Number
|
foo: Number
|
||||||
},
|
},
|
||||||
setup(props, { attrs }) {
|
setup(props) {
|
||||||
onUpdated(grandChildUpdated)
|
onUpdated(grandChildUpdated)
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
'div',
|
'div',
|
||||||
mergeProps(
|
{
|
||||||
{
|
class: 'c2',
|
||||||
class: 'c2',
|
style: { fontWeight: 'bold' }
|
||||||
style: { fontWeight: 'bold' }
|
},
|
||||||
},
|
|
||||||
attrs
|
|
||||||
),
|
|
||||||
props.foo
|
props.foo
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -227,4 +224,104 @@ describe('attribute fallthrough', () => {
|
|||||||
|
|
||||||
expect(node.hasAttribute('foo')).toBe(false)
|
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(`<div>1</div>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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(`<div class="child parent">1</div>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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(
|
||||||
|
`<!----><div></div><div class="parent"></div><!---->`
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -62,6 +62,7 @@ export interface ComponentOptionsBase<
|
|||||||
render?: Function
|
render?: Function
|
||||||
components?: Record<string, Component>
|
components?: Record<string, Component>
|
||||||
directives?: Record<string, Directive>
|
directives?: Record<string, Directive>
|
||||||
|
inheritAttrs?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComponentOptionsWithoutProps<
|
export type ComponentOptionsWithoutProps<
|
||||||
@ -80,7 +81,7 @@ export type ComponentOptionsWithArrayProps<
|
|||||||
D = {},
|
D = {},
|
||||||
C extends ComputedOptions = {},
|
C extends ComputedOptions = {},
|
||||||
M extends MethodOptions = {},
|
M extends MethodOptions = {},
|
||||||
Props = { [key in PropNames]?: unknown }
|
Props = { [key in PropNames]?: any }
|
||||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
|
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
|
||||||
props: PropNames[]
|
props: PropNames[]
|
||||||
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
|
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
|
||||||
|
@ -37,6 +37,7 @@ export type Data = { [key: string]: unknown }
|
|||||||
export interface FunctionalComponent<P = {}> {
|
export interface FunctionalComponent<P = {}> {
|
||||||
(props: P, ctx: SetupContext): VNodeChild
|
(props: P, ctx: SetupContext): VNodeChild
|
||||||
props?: ComponentPropsOptions<P>
|
props?: ComponentPropsOptions<P>
|
||||||
|
inheritAttrs?: boolean
|
||||||
displayName?: string
|
displayName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +202,7 @@ export function resolveProps(
|
|||||||
instance.attrs = options
|
instance.attrs = options
|
||||||
? __DEV__ && attrs != null
|
? __DEV__ && attrs != null
|
||||||
? readonly(attrs)
|
? readonly(attrs)
|
||||||
: attrs!
|
: attrs || EMPTY_OBJ
|
||||||
: instance.props
|
: instance.props
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,10 @@ import {
|
|||||||
import { UnwrapRef, ReactiveEffect } from '@vue/reactivity'
|
import { UnwrapRef, ReactiveEffect } from '@vue/reactivity'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { Slots } from './componentSlots'
|
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
|
// public properties exposed on the proxy, which is used as the render context
|
||||||
// in templates (as `this` in the render option)
|
// in templates (as `this` in the render option)
|
||||||
@ -109,6 +112,9 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
|||||||
} else if (key === '$el') {
|
} else if (key === '$el') {
|
||||||
return target.vnode.el
|
return target.vnode.el
|
||||||
} else if (hasOwn(publicPropertiesMap, key)) {
|
} else if (hasOwn(publicPropertiesMap, key)) {
|
||||||
|
if (__DEV__ && key === '$attrs') {
|
||||||
|
markAttrsAccessed()
|
||||||
|
}
|
||||||
return target[publicPropertiesMap[key]]
|
return target[publicPropertiesMap[key]]
|
||||||
}
|
}
|
||||||
// methods are only exposed when options are supported
|
// methods are only exposed when options are supported
|
||||||
|
@ -3,15 +3,31 @@ import {
|
|||||||
FunctionalComponent,
|
FunctionalComponent,
|
||||||
Data
|
Data
|
||||||
} from './component'
|
} from './component'
|
||||||
import { VNode, normalizeVNode, createVNode, Comment } from './vnode'
|
import {
|
||||||
|
VNode,
|
||||||
|
normalizeVNode,
|
||||||
|
createVNode,
|
||||||
|
Comment,
|
||||||
|
cloneVNode
|
||||||
|
} from './vnode'
|
||||||
import { ShapeFlags } from './shapeFlags'
|
import { ShapeFlags } from './shapeFlags'
|
||||||
import { handleError, ErrorCodes } from './errorHandling'
|
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.
|
// mark the current rendering instance for asset resolution (e.g.
|
||||||
// resolveComponent, resolveDirective) during render
|
// resolveComponent, resolveDirective) during render
|
||||||
export let currentRenderingInstance: ComponentInternalInstance | null = null
|
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(
|
export function renderComponentRoot(
|
||||||
instance: ComponentInternalInstance
|
instance: ComponentInternalInstance
|
||||||
): VNode {
|
): VNode {
|
||||||
@ -27,6 +43,9 @@ export function renderComponentRoot(
|
|||||||
|
|
||||||
let result
|
let result
|
||||||
currentRenderingInstance = instance
|
currentRenderingInstance = instance
|
||||||
|
if (__DEV__) {
|
||||||
|
accessedAttrs = false
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
|
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
|
||||||
result = normalizeVNode(instance.render!.call(renderProxy))
|
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 */)
|
: 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) {
|
} catch (err) {
|
||||||
handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
|
handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
|
||||||
result = createVNode(Comment)
|
result = createVNode(Comment)
|
||||||
|
@ -229,11 +229,15 @@ export function createVNode(
|
|||||||
return vnode
|
return vnode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cloneVNode(vnode: VNode): VNode {
|
export function cloneVNode(vnode: VNode, extraProps?: Data): VNode {
|
||||||
return {
|
return {
|
||||||
_isVNode: true,
|
_isVNode: true,
|
||||||
type: vnode.type,
|
type: vnode.type,
|
||||||
props: vnode.props,
|
props: extraProps
|
||||||
|
? vnode.props
|
||||||
|
? mergeProps(vnode.props, extraProps)
|
||||||
|
: extraProps
|
||||||
|
: vnode.props,
|
||||||
key: vnode.key,
|
key: vnode.key,
|
||||||
ref: vnode.ref,
|
ref: vnode.ref,
|
||||||
children: vnode.children,
|
children: vnode.children,
|
||||||
|
Loading…
Reference in New Issue
Block a user