feat(core): adjust attrs fallthrough behavior

This commit is contained in:
Evan You 2019-10-25 12:12:17 -04:00
parent d76cfba7fb
commit 8edfbf9df9
8 changed files with 175 additions and 26 deletions

View File

@ -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

View File

@ -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><!---->`
)
})
}) })

View File

@ -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>>

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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

View File

@ -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)

View File

@ -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,