Skip to content

Commit 624acc6

Browse files
committed
fix: Track $attrs access to suppress extraneous attribute warnings in dev mode.
1 parent 2206dba commit 624acc6

File tree

2 files changed

+140
-111
lines changed

2 files changed

+140
-111
lines changed

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

Lines changed: 111 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { type Ref, nextTick, onUpdated, ref } from '@vue/runtime-dom'
1+
import {
2+
type Ref,
3+
nextTick,
4+
onUpdated,
5+
ref,
6+
withModifiers,
7+
} from '@vue/runtime-dom'
28
import {
39
VaporTeleport,
410
createComponent,
@@ -356,133 +362,128 @@ describe('attribute fallthrough', () => {
356362
expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
357363
})
358364

359-
// it('should dedupe same listeners when $attrs is used during render', () => {
360-
// const click = vi.fn()
361-
// const count = ref(0)
362-
363-
// function inc() {
364-
// count.value++
365-
// click()
366-
// }
367-
368-
// const Parent = {
369-
// render() {
370-
// return h(Child, { onClick: inc })
371-
// },
372-
// }
373-
374-
// const Child = defineVaporComponent({
375-
// render() {
376-
// return h(
377-
// 'div',
378-
// mergeProps(
379-
// {
380-
// onClick: withModifiers(() => {}, ['prevent', 'stop']),
381-
// },
382-
// this.$attrs,
383-
// ),
384-
// )
385-
// },
386-
// })
387-
388-
// const root = document.createElement('div')
389-
// document.body.appendChild(root)
390-
// render(h(Parent), root)
391-
392-
// const node = root.children[0] as HTMLElement
393-
// node.dispatchEvent(new CustomEvent('click'))
394-
// expect(click).toHaveBeenCalledTimes(1)
395-
// expect(count.value).toBe(1)
396-
// })
397-
398-
// it('should not warn when $attrs is used during render', () => {
399-
// const Parent = {
400-
// render() {
401-
// return h(Child, { foo: 1, class: 'parent', onBar: () => {} })
402-
// },
403-
// }
365+
it('should dedupe same listeners when $attrs is used during render', () => {
366+
const click = vi.fn()
367+
const count = ref(0)
404368

405-
// const Child = defineVaporComponent({
406-
// props: ['foo'],
407-
// render() {
408-
// return [h('div'), h('div', this.$attrs)]
409-
// },
410-
// })
369+
function inc() {
370+
count.value++
371+
click()
372+
}
411373

412-
// const root = document.createElement('div')
413-
// document.body.appendChild(root)
414-
// render(h(Parent), root)
374+
const Parent = {
375+
render() {
376+
return createComponent(Child, { onClick: () => inc })
377+
},
378+
}
415379

416-
// expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
417-
// expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
380+
const Child = defineVaporComponent({
381+
setup(_, { attrs }) {
382+
const n0 = template('<div></div>', true)() as any
383+
n0.$evtclick = withModifiers(() => {}, ['prevent', 'stop'])
384+
renderEffect(() => setDynamicProps(n0, [attrs]))
385+
return n0
386+
},
387+
})
418388

419-
// expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
420-
// })
389+
const { host } = define(Parent).render()
390+
const node = host.children[0] as HTMLElement
391+
node.dispatchEvent(new CustomEvent('click'))
392+
expect(click).toHaveBeenCalledTimes(1)
393+
expect(count.value).toBe(1)
394+
})
421395

422-
// it('should not warn when context.attrs is used during render', () => {
423-
// const Parent = {
424-
// render() {
425-
// return h(Child, { foo: 1, class: 'parent', onBar: () => {} })
426-
// },
427-
// }
396+
it('should not warn when context.attrs is used during render', () => {
397+
const Parent = {
398+
render() {
399+
return createComponent(Child, {
400+
foo: () => 1,
401+
class: () => 'parent',
402+
onBar: () => () => {},
403+
})
404+
},
405+
}
428406

429-
// const Child = defineVaporComponent({
430-
// props: ['foo'],
431-
// setup(_props, { attrs }) {
432-
// return () => [h('div'), h('div', attrs)]
433-
// },
434-
// })
407+
const Child = defineVaporComponent({
408+
props: ['foo'],
409+
render(_ctx, $props, $emit, $attrs, $slots) {
410+
const n0 = template('<div></div>')() as Element
411+
const n1 = template('<div></div>')() as Element
412+
renderEffect(() => {
413+
setDynamicProps(n1, [$attrs])
414+
})
415+
return [n0, n1]
416+
},
417+
})
435418

436-
// const root = document.createElement('div')
437-
// document.body.appendChild(root)
438-
// render(h(Parent), root)
419+
const { html } = define(Parent).render()
439420

440-
// expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
441-
// expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
421+
expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
422+
expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
442423

443-
// expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
444-
// })
424+
expect(html()).toBe(`<div></div><div class="parent"></div>`)
425+
})
445426

446-
// it('should not warn when context.attrs is used during render (functional)', () => {
447-
// const Parent = {
448-
// render() {
449-
// return h(Child, { foo: 1, class: 'parent', onBar: () => {} })
450-
// },
451-
// }
427+
it('should not warn when context.attrs is used during render (functional)', () => {
428+
const Parent = {
429+
render() {
430+
return createComponent(Child, {
431+
foo: () => 1,
432+
class: () => 'parent',
433+
onBar: () => () => {},
434+
})
435+
},
436+
}
452437

453-
// const Child: FunctionalComponent = (_, { attrs }) => [
454-
// h('div'),
455-
// h('div', attrs),
456-
// ]
438+
const Child = defineVaporComponent((_, { attrs }) => {
439+
const n0 = template('<div></div>')() as Element
440+
const n1 = template('<div></div>')() as Element
441+
renderEffect(() => {
442+
setDynamicProps(n1, [attrs])
443+
})
444+
return [n0, n1]
445+
})
457446

458-
// Child.props = ['foo']
447+
Child.props = ['foo']
459448

460-
// const root = document.createElement('div')
461-
// document.body.appendChild(root)
462-
// render(h(Parent), root)
449+
const { html } = define(Parent).render()
463450

464-
// expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
465-
// expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
466-
// expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
467-
// })
451+
expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
452+
expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
453+
expect(html()).toBe(`<div></div><div class="parent"></div>`)
454+
})
468455

469-
// it('should not warn when functional component has optional props', () => {
470-
// const Parent = {
471-
// render() {
472-
// return h(Child, { foo: 1, class: 'parent', onBar: () => {} })
473-
// },
474-
// }
456+
it.todo(
457+
'should not warn when functional component has optional props',
458+
() => {
459+
const Parent = {
460+
render() {
461+
return createComponent(Child, {
462+
foo: () => 1,
463+
class: () => 'parent',
464+
onBar: () => () => {},
465+
})
466+
},
467+
}
475468

476-
// const Child = (props: any) => [h('div'), h('div', { class: props.class })]
469+
const Child = defineVaporComponent((props: any) => {
470+
const n0 = template('<div></div>')() as Element
471+
const n1 = template('<div></div>')() as Element
472+
renderEffect(() => {
473+
setClass(n1, 'class', props.class)
474+
})
475+
return [n0, n1]
476+
})
477477

478-
// const root = document.createElement('div')
479-
// document.body.appendChild(root)
480-
// render(h(Parent), root)
478+
const root = document.createElement('div')
479+
document.body.appendChild(root)
480+
const { html } = define(Parent).render()
481481

482-
// expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
483-
// expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
484-
// expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
485-
// })
482+
expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
483+
expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
484+
expect(html()).toBe(`<div></div><div class="parent"></div>`)
485+
},
486+
)
486487

487488
// it('should warn when functional component has props and does not use attrs', () => {
488489
// const Parent = {

packages/runtime-vapor/src/component.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,12 @@ export function setupComponent(
410410
const root = filterSingleRootElement(instance.block)
411411
if (root) {
412412
renderEffect(() => applyFallthroughProps(root, instance.attrs))
413-
} else if (__DEV__ && isArray(instance.block) && instance.block.length) {
413+
} else if (
414+
__DEV__ &&
415+
!instance.accessedAttrs &&
416+
isArray(instance.block) &&
417+
instance.block.length
418+
) {
414419
warnExtraneousAttributes(instance.attrs)
415420
}
416421
}
@@ -557,6 +562,13 @@ export class VaporComponentInstance implements GenericComponentInstance {
557562
emitsOptions?: ObjectEmitsOptions | null
558563
isSingleRoot?: boolean
559564

565+
/**
566+
* dev only flag to track whether $attrs was used during render.
567+
* If $attrs was used during render then the warning for failed attrs
568+
* fallthrough can be suppressed.
569+
*/
570+
accessedAttrs: boolean = false
571+
560572
constructor(
561573
comp: VaporComponent,
562574
rawProps?: RawProps | null,
@@ -629,6 +641,22 @@ export class VaporComponentInstance implements GenericComponentInstance {
629641
if (comp.ce) {
630642
comp.ce(this)
631643
}
644+
645+
if (__DEV__) {
646+
// in dev, mark attrs accessed if optional props (attrs === props)
647+
if (this.props === this.attrs) {
648+
this.accessedAttrs = true
649+
} else {
650+
const attrs = this.attrs
651+
const instance = this
652+
this.attrs = new Proxy(attrs, {
653+
get(target, key, receiver) {
654+
instance.accessedAttrs = true
655+
return Reflect.get(target, key, receiver)
656+
},
657+
})
658+
}
659+
}
632660
}
633661

634662
/**

0 commit comments

Comments
 (0)