diff --git a/packages/runtime-core/__tests__/apiSetupContext.spec.ts b/packages/runtime-core/__tests__/apiSetupContext.spec.ts index 8f7ab078bbe..08f0235b94b 100644 --- a/packages/runtime-core/__tests__/apiSetupContext.spec.ts +++ b/packages/runtime-core/__tests__/apiSetupContext.spec.ts @@ -54,7 +54,8 @@ describe('api: setup context', () => { } const Child = defineComponent({ - setup(props: { count: number }) { + props: { count: Number }, + setup(props) { watchEffect(() => { dummy = props.count }) diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 4738c80ef78..5b71689a435 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -222,7 +222,8 @@ describe('Suspense', () => { test('content update before suspense resolve', async () => { const Async = defineAsyncComponent({ - setup(props: { msg: string }) { + props: { msg: String }, + setup(props: any) { return () => h('div', props.msg) } }) @@ -569,7 +570,8 @@ describe('Suspense', () => { const calls: number[] = [] const AsyncChildWithSuspense = defineAsyncComponent({ - setup(props: { msg: string }) { + props: { msg: String }, + setup(props: any) { onMounted(() => { calls.push(0) }) @@ -583,7 +585,8 @@ describe('Suspense', () => { const AsyncInsideNestedSuspense = defineAsyncComponent( { - setup(props: { msg: string }) { + props: { msg: String }, + setup(props: any) { onMounted(() => { calls.push(2) }) @@ -594,7 +597,8 @@ describe('Suspense', () => { ) const AsyncChildParent = defineAsyncComponent({ - setup(props: { msg: string }) { + props: { msg: String }, + setup(props: any) { onMounted(() => { calls.push(1) }) @@ -604,7 +608,8 @@ describe('Suspense', () => { const NestedAsyncChild = defineAsyncComponent( { - setup(props: { msg: string }) { + props: { msg: String }, + setup(props: any) { onMounted(() => { calls.push(3) }) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 837be3ee541..ea9a8a27774 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -8,7 +8,8 @@ import { createStaticVNode, Suspense, onMounted, - defineAsyncComponent + defineAsyncComponent, + defineComponent } from '@vue/runtime-dom' import { renderToString } from '@vue/server-renderer' import { mockWarn } from '@vue/shared' @@ -448,8 +449,9 @@ describe('SSR hydration', () => { const mountedCalls: number[] = [] const asyncDeps: Promise[] = [] - const AsyncChild = { - async setup(props: { n: number }) { + const AsyncChild = defineComponent({ + props: ['n'], + async setup(props) { const count = ref(props.n) onMounted(() => { mountedCalls.push(props.n) @@ -468,7 +470,7 @@ describe('SSR hydration', () => { count.value ) } - } + }) const done = jest.fn() const App = { diff --git a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts index 0f5f54dcdf2..d7eb0ac967d 100644 --- a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts +++ b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts @@ -15,7 +15,7 @@ import { mockWarn } from '@vue/shared' describe('attribute fallthrough', () => { mockWarn() - it('should allow whitelisted attrs to fallthrough', async () => { + it('should allow attrs to fallthrough', async () => { const click = jest.fn() const childUpdated = jest.fn() @@ -30,12 +30,12 @@ describe('attribute fallthrough', () => { return () => h(Child, { - foo: 1, + foo: count.value + 1, id: 'test', class: 'c' + count.value, style: { color: count.value ? 'red' : 'green' }, onClick: inc, - 'data-id': 1 + 'data-id': count.value + 1 }) } } @@ -47,7 +47,6 @@ describe('attribute fallthrough', () => { h( 'div', { - id: props.id, // id is not whitelisted class: 'c2', style: { fontWeight: 'bold' } }, @@ -62,8 +61,8 @@ describe('attribute fallthrough', () => { const node = root.children[0] as HTMLElement - expect(node.getAttribute('id')).toBe('test') // id is not whitelisted, but explicitly bound - expect(node.getAttribute('foo')).toBe(null) // foo is not whitelisted + expect(node.getAttribute('id')).toBe('test') + expect(node.getAttribute('foo')).toBe('1') expect(node.getAttribute('class')).toBe('c2 c0') expect(node.style.color).toBe('green') expect(node.style.fontWeight).toBe('bold') @@ -71,6 +70,121 @@ describe('attribute fallthrough', () => { node.dispatchEvent(new CustomEvent('click')) 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() expect(childUpdated).toHaveBeenCalled() expect(node.getAttribute('id')).toBe('test') diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index d20ba2b7468..6ba6728004e 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -55,6 +55,7 @@ export function renderComponentRoot( accessedAttrs = false } try { + let fallthroughAttrs if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { // withProxy is a proxy with a different `has` trap only for // runtime-compiled render functions using `with` block. @@ -62,6 +63,7 @@ export function renderComponentRoot( result = normalizeVNode( instance.render!.call(proxyToUse, proxyToUse!, renderCache) ) + fallthroughAttrs = attrs } else { // functional const render = Component as FunctionalComponent @@ -74,14 +76,14 @@ export function renderComponentRoot( }) : render(props, null as any /* we know it doesn't need it */) ) + fallthroughAttrs = Component.props ? attrs : getFallthroughAttrs(attrs) } // attr merging - let fallthroughAttrs if ( Component.inheritAttrs !== false && - attrs !== EMPTY_OBJ && - (fallthroughAttrs = getFallthroughAttrs(attrs)) + fallthroughAttrs && + fallthroughAttrs !== EMPTY_OBJ ) { if ( result.shapeFlag & ShapeFlags.ELEMENT || @@ -140,14 +142,7 @@ export function renderComponentRoot( const getFallthroughAttrs = (attrs: Data): Data | undefined => { let res: Data | undefined for (const key in attrs) { - if ( - key === 'class' || - key === 'style' || - key === 'role' || - isOn(key) || - key.indexOf('aria-') === 0 || - key.indexOf('data-') === 0 - ) { + if (key === 'class' || key === 'style' || isOn(key)) { ;(res || (res = {}))[key] = attrs[key] } }