Skip to content

Commit 36b47d1

Browse files
committed
feat: implement attribute fallthrough logic for functional components and adjust fragment attribute warning conditions.
1 parent 19bba81 commit 36b47d1

File tree

5 files changed

+69
-61
lines changed

5 files changed

+69
-61
lines changed

packages/runtime-core/src/componentRenderUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ export function filterSingleRoot(
341341
return singleRoot
342342
}
343343

344-
const getFunctionalFallthrough = (attrs: Data): Data | undefined => {
344+
export const getFunctionalFallthrough = (attrs: Data): Data | undefined => {
345345
let res: Data | undefined
346346
for (const key in attrs) {
347347
if (key === 'class' || key === 'style' || isOn(key)) {

packages/runtime-core/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -678,4 +678,7 @@ export type { GenericComponent } from './component'
678678
/**
679679
* @internal
680680
*/
681-
export { warnExtraneousAttributes } from './componentRenderUtils'
681+
export {
682+
warnExtraneousAttributes,
683+
getFunctionalFallthrough,
684+
} from './componentRenderUtils'

packages/runtime-vapor/__tests__/componentAttrs.spec.ts

Lines changed: 54 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -66,67 +66,64 @@ describe('attribute fallthrough', () => {
6666
expect(host.innerHTML).toBe('<div id="b">2</div>')
6767
})
6868

69-
it.todo(
70-
'should only allow whitelisted fallthrough on functional component with optional props',
71-
async () => {
72-
const click = vi.fn()
73-
const childUpdated = vi.fn()
74-
75-
const count = ref(0)
69+
it('should only allow whitelisted fallthrough on functional component with optional props', async () => {
70+
const click = vi.fn()
71+
const childUpdated = vi.fn()
7672

77-
function inc() {
78-
count.value++
79-
click()
80-
}
73+
const count = ref(0)
8174

82-
const Hello = () =>
83-
createComponent(Child, {
84-
foo: () => count.value + 1,
85-
id: () => 'test',
86-
class: () => 'c' + count.value,
87-
style: () => ({
88-
color: count.value ? 'red' : 'green',
89-
}),
90-
onClick: () => inc,
91-
})
75+
function inc() {
76+
count.value++
77+
click()
78+
}
9279

93-
const { component: Child } = define((props: any) => {
94-
childUpdated()
95-
const n0 = template(
96-
'<div class="c2" style="font-weight: bold"></div>',
97-
true,
98-
)() as Element
99-
renderEffect(() => setElementText(n0, props.foo))
100-
return n0
80+
const Hello = () =>
81+
createComponent(Child, {
82+
foo: () => count.value + 1,
83+
id: () => 'test',
84+
class: () => 'c' + count.value,
85+
style: () => ({
86+
color: count.value ? 'red' : 'green',
87+
}),
88+
onClick: () => inc,
10189
})
10290

103-
const { host: root } = define(Hello).render()
104-
expect(root.innerHTML).toBe(
105-
'<div class="c2 c0" style="font-weight: bold; color: green;">1</div>',
106-
)
107-
108-
const node = root.children[0] as HTMLElement
109-
110-
// not whitelisted
111-
expect(node.getAttribute('id')).toBe(null)
112-
expect(node.getAttribute('foo')).toBe(null)
113-
114-
// whitelisted: style, class, event listeners
115-
expect(node.getAttribute('class')).toBe('c2 c0')
116-
expect(node.style.color).toBe('green')
117-
expect(node.style.fontWeight).toBe('bold')
118-
node.dispatchEvent(new CustomEvent('click'))
119-
expect(click).toHaveBeenCalled()
120-
121-
await nextTick()
122-
expect(childUpdated).toHaveBeenCalled()
123-
expect(node.getAttribute('id')).toBe(null)
124-
expect(node.getAttribute('foo')).toBe(null)
125-
expect(node.getAttribute('class')).toBe('c2 c1')
126-
expect(node.style.color).toBe('red')
127-
expect(node.style.fontWeight).toBe('bold')
128-
},
129-
)
91+
const { component: Child } = define((props: any) => {
92+
childUpdated()
93+
const n0 = template(
94+
'<div class="c2" style="font-weight: bold"></div>',
95+
true,
96+
)() as Element
97+
renderEffect(() => setElementText(n0, props.foo))
98+
return n0
99+
})
100+
101+
const { host: root } = define(Hello).render()
102+
expect(root.innerHTML).toBe(
103+
'<div class="c2 c0" style="font-weight: bold; color: green;">1</div>',
104+
)
105+
106+
const node = root.children[0] as HTMLElement
107+
108+
// not whitelisted
109+
expect(node.getAttribute('id')).toBe(null)
110+
expect(node.getAttribute('foo')).toBe(null)
111+
112+
// whitelisted: style, class, event listeners
113+
expect(node.getAttribute('class')).toBe('c2 c0')
114+
expect(node.style.color).toBe('green')
115+
expect(node.style.fontWeight).toBe('bold')
116+
node.dispatchEvent(new CustomEvent('click'))
117+
expect(click).toHaveBeenCalled()
118+
119+
await nextTick()
120+
expect(childUpdated).toHaveBeenCalled()
121+
expect(node.getAttribute('id')).toBe(null)
122+
expect(node.getAttribute('foo')).toBe(null)
123+
expect(node.getAttribute('class')).toBe('c2 c1')
124+
expect(node.style.color).toBe('red')
125+
expect(node.style.fontWeight).toBe('bold')
126+
})
130127

131128
it('should allow all attrs on functional component with declared props', async () => {
132129
const click = vi.fn()
@@ -438,7 +435,7 @@ describe('attribute fallthrough', () => {
438435
},
439436
}
440437

441-
const Child = defineVaporComponent((_, { attrs }) => {
438+
const { component: Child } = define((_: any, { attrs }: any) => {
442439
const n0 = template('<div></div>')() as Element
443440
const n1 = template('<div></div>')() as Element
444441
renderEffect(() => {

packages/runtime-vapor/src/component.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
endMeasure,
1818
expose,
1919
getComponentName,
20+
getFunctionalFallthrough,
2021
isAsyncWrapper,
2122
isKeepAlive,
2223
nextUid,
@@ -409,7 +410,12 @@ export function setupComponent(
409410
false,
410411
)
411412
if (root) {
412-
renderEffect(() => applyFallthroughProps(root, instance.attrs))
413+
renderEffect(() => {
414+
const attrs = isFunction(component)
415+
? getFunctionalFallthrough(instance.attrs)
416+
: instance.attrs
417+
if (attrs) applyFallthroughProps(root, attrs)
418+
})
413419
} else if (
414420
__DEV__ &&
415421
((!instance.accessedAttrs &&

packages/runtime-vapor/src/fragment.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
locateHydrationNode,
3333
} from './dom/hydration'
3434
import { getParentInstance } from './componentSlots'
35+
import { isArray } from '@vue/shared'
3536

3637
export class VaporFragment<T extends Block = Block>
3738
implements TransitionOptions
@@ -213,7 +214,8 @@ export class DynamicFragment extends VaporFragment {
213214
__DEV__ &&
214215
// preventing attrs fallthrough on slots
215216
// consistent with VDOM slots behavior
216-
this.anchorLabel === 'slot'
217+
(this.anchorLabel === 'slot' ||
218+
(isArray(this.nodes) && this.nodes.length))
217219
) {
218220
warnExtraneousAttributes(this.attrs)
219221
}

0 commit comments

Comments
 (0)