vuejs / language-tools

⚡ High-performance Vue language tooling based-on Volar.js
https://marketplace.visualstudio.com/items?itemName=Vue.volar
MIT License
5.87k stars 402 forks source link

Allow some component events bypass `strictTemplate` #4985

Open minht11 opened 2 weeks ago

minht11 commented 2 weeks ago

What problem does this feature solve?

This is sort of like https://github.com/vuejs/language-tools/issues/4879 but even more granular.

For better or for worse props/emits compose in different ways in Vue. For example I can have component but not be sure what attributes it accepts, one example while keeping it type safe could like this:

interface Props {
    type: 'button'
    [k: `data-${string}`]: string
}
const { type, ...restProps } = defineProps<Props>()

If I want to compose props and only pass only state I need with v-bind and so on

<OtherComponent v-bind="restProps" />

The same cannot be said with vue events, while using defineEmits I need explicitely define all possible events or else when I enable strictTemplate things will break.

An actual example I am struggling with

// BaseButton.vue

defineEmits<{
    click: [e: MouseEvent]
    focusin: [e: FocusEvent]
    focusout: [e: FocusEvent]
    keyboard: [e: KeyboardEvent]
    mouse: [e: MouseEvent]
    touch: [e: TouchEvent]
    // ... and so on
}>()

<button
                 @click="$emit('click', $event)"
        @focusin="$emit('focusin', $event)"
        @focusout="$emit('focusout', $event)"
        @keydown="$emit('keyboard', $event)"
        @mousedown="$emit('mouse', $event)"
        @touchstart="$emit('touch', $event)"
>
  <slot />
</button>

I defined events I needed, but now actual event listeners are not lazy, they are actual event listeners even if parent component did not use any of them "mousemove" for example would still fire to the event listener vue registered.

Composability is another issue, lets say I have FancyButton which wraps BaseButton, to declare events I have to the whole:

<BaseButton
                 @click="$emit('click', $event)"
        @focusin="$emit('focusin', $event)"
        @focusout="$emit('focusout', $event)"
        @keydown="$emit('keyboard', $event)"
        @mousedown="$emit('mouse', $event)"
        @touchstart="$emit('touch', $event)"
>
  <slot />
</BaseButton>

Which quickly becomes hard to maintain.

Without enabling strictTemplate this issue is largely masked because component would accept any event, now not so much.

What does the proposed solution look like?

This is largely Vue limitation of how to compose events, but in mean time it would be great to allow disabling strict events on some components. I am not sure how it would look like maybe something with types, comment or tsconfig.json option. It would allow to retain most of benefits of strict templates, while still making it usable with design system components like buttons where you can't and shouldn't realistically write all of the possible events.

KazariEX commented 2 weeks ago

You can enable fallthroughAttributes.

minht11 commented 2 weeks ago

fallthroughAttributes will enable it globally while I want that only for few specific components Edit: enabling fallthroughAttributes introduced bunch of unrelated errors, while keeping previous event listener problem :(

KazariEX commented 2 weeks ago

If your button is not the single root node of the component (or if v-bind="$attrs" is not set on the button), this option will not take effect.

KazariEX commented 2 weeks ago

Perhaps we can have a comment syntax or defineCompilerOptions to configure vueCompilerOptions locally.

enabling fallthroughAttributes introduced bunch of unrelated errors

Can you explain this in detail? If it's indeed a bug, please create a minimal reproduction that allows us to investigate it.

minht11 commented 2 weeks ago

If your button is not the single root node of the component (or if v-bind="$attrs" is not set on the button), this option will not take effect.

BaseButton is dynamic component which can be either a or button based on prop. Yeah I don't use $attrs, I explicitely define props I need and use interface extensions inside wrapper components and to v-bind=props to pass them from up top.

Whole structure looks something like this:

<MyPage>
  <FancyButton>
     <template>
       <BaseButton>
         <template>
           <Component :is="button | a"><slot /></Component>
         </template>
       </BaseButton>
     </template>
  </FancyButton>
</MyPage>

Can you explain this in detail? If it's indeed a bug, please create a minimal reproduction that allows us to investigate it.

Will try later to make a reproduction later, when I enable fallthroughAttributes it seems to break interface merging. I have design system package, and apps can customize or add additional props to buttons like this:

export interface BaseButtonInitialProps {
    id?: string
    type?: 'button' | 'submit'
    as?: 'a' | 'button'
    disabled?: boolean
    href?: string
    target?: '_blank' | '_self'
    download?: boolean
}

// Interface is intentionally left empty to allow consumers of the component
// to extend and customize its properties through TypeScript's declaration merging.
export interface BaseButtonProps extends BaseButtonInitialProps {}

Perhaps we can have a comment syntax or defineCompilerOptions to configure vueCompilerOptions locally.

Would be great