refactor(runtime-core): adjust attr fallthrough behavior

BREAKING CHANGE: attribute fallthrough behavior has been adjusted
according to https://github.com/vuejs/rfcs/pull/154
This commit is contained in:
Evan You 2020-04-02 21:51:01 -04:00
parent 2103a485d7
commit 21bcdec943
5 changed files with 144 additions and 27 deletions

View File

@ -54,7 +54,8 @@ describe('api: setup context', () => {
} }
const Child = defineComponent({ const Child = defineComponent({
setup(props: { count: number }) { props: { count: Number },
setup(props) {
watchEffect(() => { watchEffect(() => {
dummy = props.count dummy = props.count
}) })

View File

@ -222,7 +222,8 @@ describe('Suspense', () => {
test('content update before suspense resolve', async () => { test('content update before suspense resolve', async () => {
const Async = defineAsyncComponent({ const Async = defineAsyncComponent({
setup(props: { msg: string }) { props: { msg: String },
setup(props: any) {
return () => h('div', props.msg) return () => h('div', props.msg)
} }
}) })
@ -569,7 +570,8 @@ describe('Suspense', () => {
const calls: number[] = [] const calls: number[] = []
const AsyncChildWithSuspense = defineAsyncComponent({ const AsyncChildWithSuspense = defineAsyncComponent({
setup(props: { msg: string }) { props: { msg: String },
setup(props: any) {
onMounted(() => { onMounted(() => {
calls.push(0) calls.push(0)
}) })
@ -583,7 +585,8 @@ describe('Suspense', () => {
const AsyncInsideNestedSuspense = defineAsyncComponent( const AsyncInsideNestedSuspense = defineAsyncComponent(
{ {
setup(props: { msg: string }) { props: { msg: String },
setup(props: any) {
onMounted(() => { onMounted(() => {
calls.push(2) calls.push(2)
}) })
@ -594,7 +597,8 @@ describe('Suspense', () => {
) )
const AsyncChildParent = defineAsyncComponent({ const AsyncChildParent = defineAsyncComponent({
setup(props: { msg: string }) { props: { msg: String },
setup(props: any) {
onMounted(() => { onMounted(() => {
calls.push(1) calls.push(1)
}) })
@ -604,7 +608,8 @@ describe('Suspense', () => {
const NestedAsyncChild = defineAsyncComponent( const NestedAsyncChild = defineAsyncComponent(
{ {
setup(props: { msg: string }) { props: { msg: String },
setup(props: any) {
onMounted(() => { onMounted(() => {
calls.push(3) calls.push(3)
}) })

View File

@ -8,7 +8,8 @@ import {
createStaticVNode, createStaticVNode,
Suspense, Suspense,
onMounted, onMounted,
defineAsyncComponent defineAsyncComponent,
defineComponent
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import { renderToString } from '@vue/server-renderer' import { renderToString } from '@vue/server-renderer'
import { mockWarn } from '@vue/shared' import { mockWarn } from '@vue/shared'
@ -448,8 +449,9 @@ describe('SSR hydration', () => {
const mountedCalls: number[] = [] const mountedCalls: number[] = []
const asyncDeps: Promise<any>[] = [] const asyncDeps: Promise<any>[] = []
const AsyncChild = { const AsyncChild = defineComponent({
async setup(props: { n: number }) { props: ['n'],
async setup(props) {
const count = ref(props.n) const count = ref(props.n)
onMounted(() => { onMounted(() => {
mountedCalls.push(props.n) mountedCalls.push(props.n)
@ -468,7 +470,7 @@ describe('SSR hydration', () => {
count.value count.value
) )
} }
} })
const done = jest.fn() const done = jest.fn()
const App = { const App = {

View File

@ -15,7 +15,7 @@ import { mockWarn } from '@vue/shared'
describe('attribute fallthrough', () => { describe('attribute fallthrough', () => {
mockWarn() mockWarn()
it('should allow whitelisted attrs to fallthrough', async () => { it('should allow attrs to fallthrough', async () => {
const click = jest.fn() const click = jest.fn()
const childUpdated = jest.fn() const childUpdated = jest.fn()
@ -30,12 +30,12 @@ describe('attribute fallthrough', () => {
return () => return () =>
h(Child, { h(Child, {
foo: 1, foo: count.value + 1,
id: 'test', id: 'test',
class: 'c' + count.value, class: 'c' + count.value,
style: { color: count.value ? 'red' : 'green' }, style: { color: count.value ? 'red' : 'green' },
onClick: inc, onClick: inc,
'data-id': 1 'data-id': count.value + 1
}) })
} }
} }
@ -47,7 +47,6 @@ describe('attribute fallthrough', () => {
h( h(
'div', 'div',
{ {
id: props.id, // id is not whitelisted
class: 'c2', class: 'c2',
style: { fontWeight: 'bold' } style: { fontWeight: 'bold' }
}, },
@ -62,8 +61,8 @@ describe('attribute fallthrough', () => {
const node = root.children[0] as HTMLElement const node = root.children[0] as HTMLElement
expect(node.getAttribute('id')).toBe('test') // id is not whitelisted, but explicitly bound expect(node.getAttribute('id')).toBe('test')
expect(node.getAttribute('foo')).toBe(null) // foo is not whitelisted expect(node.getAttribute('foo')).toBe('1')
expect(node.getAttribute('class')).toBe('c2 c0') expect(node.getAttribute('class')).toBe('c2 c0')
expect(node.style.color).toBe('green') expect(node.style.color).toBe('green')
expect(node.style.fontWeight).toBe('bold') expect(node.style.fontWeight).toBe('bold')
@ -71,6 +70,121 @@ describe('attribute fallthrough', () => {
node.dispatchEvent(new CustomEvent('click')) node.dispatchEvent(new CustomEvent('click'))
expect(click).toHaveBeenCalled() expect(click).toHaveBeenCalled()
await nextTick()
expect(childUpdated).toHaveBeenCalled()
expect(node.getAttribute('id')).toBe('test')
expect(node.getAttribute('foo')).toBe('2')
expect(node.getAttribute('class')).toBe('c2 c1')
expect(node.style.color).toBe('red')
expect(node.style.fontWeight).toBe('bold')
expect(node.dataset.id).toBe('2')
})
it('should only allow whitelisted fallthrough on functional component with optional props', async () => {
const click = jest.fn()
const childUpdated = jest.fn()
const count = ref(0)
function inc() {
count.value++
click()
}
const Hello = () =>
h(Child, {
foo: count.value + 1,
id: 'test',
class: 'c' + count.value,
style: { color: count.value ? 'red' : 'green' },
onClick: inc
})
const Child = (props: any) => {
childUpdated()
return h(
'div',
{
class: 'c2',
style: { fontWeight: 'bold' }
},
props.foo
)
}
const root = document.createElement('div')
document.body.appendChild(root)
render(h(Hello), root)
const node = root.children[0] as HTMLElement
// not whitelisted
expect(node.getAttribute('id')).toBe(null)
expect(node.getAttribute('foo')).toBe(null)
// whitelisted: style, class, event listeners
expect(node.getAttribute('class')).toBe('c2 c0')
expect(node.style.color).toBe('green')
expect(node.style.fontWeight).toBe('bold')
node.dispatchEvent(new CustomEvent('click'))
expect(click).toHaveBeenCalled()
await nextTick()
expect(childUpdated).toHaveBeenCalled()
expect(node.getAttribute('id')).toBe(null)
expect(node.getAttribute('foo')).toBe(null)
expect(node.getAttribute('class')).toBe('c2 c1')
expect(node.style.color).toBe('red')
expect(node.style.fontWeight).toBe('bold')
})
it('should allow all attrs on functional component with declared props', async () => {
const click = jest.fn()
const childUpdated = jest.fn()
const count = ref(0)
function inc() {
count.value++
click()
}
const Hello = () =>
h(Child, {
foo: count.value + 1,
id: 'test',
class: 'c' + count.value,
style: { color: count.value ? 'red' : 'green' },
onClick: inc
})
const Child = (props: { foo: number }) => {
childUpdated()
return h(
'div',
{
class: 'c2',
style: { fontWeight: 'bold' }
},
props.foo
)
}
Child.props = ['foo']
const root = document.createElement('div')
document.body.appendChild(root)
render(h(Hello), root)
const node = root.children[0] as HTMLElement
expect(node.getAttribute('id')).toBe('test')
expect(node.getAttribute('foo')).toBe(null) // declared as prop
expect(node.getAttribute('class')).toBe('c2 c0')
expect(node.style.color).toBe('green')
expect(node.style.fontWeight).toBe('bold')
node.dispatchEvent(new CustomEvent('click'))
expect(click).toHaveBeenCalled()
await nextTick() await nextTick()
expect(childUpdated).toHaveBeenCalled() expect(childUpdated).toHaveBeenCalled()
expect(node.getAttribute('id')).toBe('test') expect(node.getAttribute('id')).toBe('test')

View File

@ -55,6 +55,7 @@ export function renderComponentRoot(
accessedAttrs = false accessedAttrs = false
} }
try { try {
let fallthroughAttrs
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// withProxy is a proxy with a different `has` trap only for // withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block. // runtime-compiled render functions using `with` block.
@ -62,6 +63,7 @@ export function renderComponentRoot(
result = normalizeVNode( result = normalizeVNode(
instance.render!.call(proxyToUse, proxyToUse!, renderCache) instance.render!.call(proxyToUse, proxyToUse!, renderCache)
) )
fallthroughAttrs = attrs
} else { } else {
// functional // functional
const render = Component as FunctionalComponent const render = Component as FunctionalComponent
@ -74,14 +76,14 @@ 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 */)
) )
fallthroughAttrs = Component.props ? attrs : getFallthroughAttrs(attrs)
} }
// attr merging // attr merging
let fallthroughAttrs
if ( if (
Component.inheritAttrs !== false && Component.inheritAttrs !== false &&
attrs !== EMPTY_OBJ && fallthroughAttrs &&
(fallthroughAttrs = getFallthroughAttrs(attrs)) fallthroughAttrs !== EMPTY_OBJ
) { ) {
if ( if (
result.shapeFlag & ShapeFlags.ELEMENT || result.shapeFlag & ShapeFlags.ELEMENT ||
@ -140,14 +142,7 @@ export function renderComponentRoot(
const getFallthroughAttrs = (attrs: Data): Data | undefined => { const getFallthroughAttrs = (attrs: Data): Data | undefined => {
let res: Data | undefined let res: Data | undefined
for (const key in attrs) { for (const key in attrs) {
if ( if (key === 'class' || key === 'style' || isOn(key)) {
key === 'class' ||
key === 'style' ||
key === 'role' ||
isOn(key) ||
key.indexOf('aria-') === 0 ||
key.indexOf('data-') === 0
) {
;(res || (res = {}))[key] = attrs[key] ;(res || (res = {}))[key] = attrs[key]
} }
} }