vuejs / language-tools

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

Question about "visualize event argument" #2445

Closed jfparadis-appomni closed 1 year ago

jfparadis-appomni commented 1 year ago

Volar 1.1.4 added a new setting, volar.inlayHints.eventArgumentInInlineHandlers, enabled by default. Here's the commit: https://github.com/vuejs/language-tools/commit/274b027cf30482b95fd20ff822b66b40698cec87

The commit doesn't reference an issue; we're very interested to know more about the intent and how to leverage it best!

johnsoncodehk commented 1 year ago

It was added for new users to understand these two points.

However, depending on user experience this may also be a nuisance, so just disable it in user space settings as needed.

jfparadis-appomni commented 1 year ago

Thanks, John; your explanation is much appreciated.

johnsoncodehk commented 1 year ago

You're welcome, I'm Johnson btw. :)

jfparadis-appomni commented 1 year ago

For those who need more context:

Volar now proposes to add $event => to event handlers via Intellisense:

Screenshot 2023-02-24 at 3 56 20 PM

You can disable this feature in Volar’s settings or with volar.inlayHints.eventArgumentInInlineHandlers.

Johnson mentions two reasons for the addition:

  1. Vue already exposes the variable $event in event handlers. Volar proposes adding an arrow function to make it more obvious. See https://vuejs.org/guide/essentials/event-handling.html#accessing-event-argument-in-inline-handlers

Both styles below are correct, but the first one might surprise a new user because $event doesn't exist on the component instance, only in event callbacks:

  <button @click="warn('Form cannot be submitted yet.', $event)">
    Submit
  </button>

  <button @click="$event => warn('Form cannot be submitted yet.', $event)">
    Submit
  </button>

Note that Vue compiles both styles to the same result, so you’re not losing any performance by adding the $event => wrapper:

    _createElementVNode("button", {
      onClick: _cache[0] || (_cache[0] = $event => (_ctx.warn('Form cannot be submitted yet.', $event)))
    }, " Submit "),
    _createElementVNode("button", {
      onClick: _cache[1] || (_cache[1] = $event => _ctx.warn('Form cannot be submitted yet.', $event))
    }, " Submit ")

Check for yourself in this playground (click on the JS tab to see how the template is compiled): https://sfc.vuejs.org/#eNqtkD0OwjAMha9iWUiABMleFQQLF2DN0hYDBeJEiUuFqt6dpkVMjGz+ef6s9[…]SeoNXER8zreO5SjndonLhoodKhYaltqQo2nUZXBspDGCDCdEb7rF/A3K8dfo=

  1. Event handlers are callbacks, and types won’t be narrowed inside event handlers by a v-if. See https://github.com/vuejs/language-tools/issues/1249 and https://github.com/vuejs/language-tools/issues/269 and many, many other discussions on the subject.

In other words, assume that foo is a data property of type object or undefined:

  <div v-if="foo">

    <!-- 'foo' is narrowed correctly to an object inside of the curly braces -->
    Test 1: {{ foo.bar }}

    <!-- 'foo' is narrowed correctly to an object inside of the assignment to the property ':title' -->
    <span :title="foo.bar">Test 2 nested</span>

    <!-- 'foo' still possibly undefined inside of `@click` and you get a TS error "foo is possibly undefined" -->
    <button @click="alert(foo.bar)">
      Submit
    </button>

    <!-- same error, and this wrapper makes it more obvious -->
    <button @click="$event => alert(foo.bar)">
      Submit
    </button>
  </div>

Why is this happening, and why the wrapper makes it more obvious?

The v-if and the @click handler execute at different times, with foo taking different values. In other words, the type narrowing provided by the v-if is lost by the time the callback is involved. Adding an arrow function makes it clearer that a callback is present.

Many people stumble on this one, which has nothing to do with Volar or Vue. This can be reproduced in pure TypeScript:

let foo: string | undefined;
if (foo) {
  ($event: any) => onSelect(foo) // Argument of type 'string | undefined' is not assignable to parameter of type 'string'.ts(2345)
}
function onSelect(foo: string) { }

Check for yourself in this playground: https://www.typescriptlang.org/play?#code/DYUwLgBAZg9jBcEDOYBOBLAdgcwgHwgFdMATEKLEEgbgCh0oI[…]GxnADowJCYAJgBmABYAVjZaAF9aKGItdGUlVXUQLR0EZCKcDk4IFqA

johnsoncodehk commented 1 year ago

Thanks! Just link your explanation to the hint.