Vue 3's v-on syntax does not support listening to custom events with capital letters
Version
3.2.30
Reproduction link
Steps to reproduce
Notice that the event named 'lower' is being handled by the Vue component, but the event named 'Upper' is not
What is expected?
Event names are an open set, and can include capital letters
What is actually happening?
The event named with a capital letter is not handled
This issue seems to also occur with capital letters in other parts of the event name too
I am a rank Vue novice, and so I may be making a simple error or oversight.
This came up when updating the web site custom-elements-everywhere.com which tracks library and framework support for custom elements. See https://github.com/webcomponents/custom-elements-everywhere/pull/1388
<fires-events @Upper=inc1 @lower=inc2>
compiles to indistinguishable prop name
onUpper
onLower
this is unlikely to change.
to work around this limitation, either use a directive or a template ref to add an event listener directly to the dom element.
@LinusBorg If that's the case then the Vue doesn't get a perfect 100% as mentioned in the docs so the docs need to be updated to reflect that.
Sure, PRs are welcome.
However, consider that we don't prevent you from listening to this event - you can add the event listener with a small custom directive, as mentioned - it's just not supported via v-on
as it collides with Vue's own event name semantics.
Sent over a docs PR, but if there's an ergonomic to support arbitrary event names, we could instead update the custom-elements-everywhere test for this feature: https://github.com/webcomponents/custom-elements-everywhere/blob/221ea4744373f34112331949c0d85d4b45e41d69/libraries/vue/src/components.js#L157-L163
@rictic thanks!
So here's the recommendation:
In Vue, one can define custom directives (like that internal ones, i.e. v-on
) to wrap imperative DOM operations in a declarative way. These custom directives can be declared locally in each component or registered globally in an app to be available in all child components.
What we would want here is a basic way to add and remove event listeners while preserving the event name casing at all times. This can be achieved with a few lines of code. here's a demo with local registration: SFC Playground
Registering this directive globally (in one app's context) would look like this:
// v-event:arg"value"
app.directive('event', {
beforeMount(el, { arg, value }) {
el.addEventListener(arg, value)
},
beforeUnmount(el, { arg, value }) {
el.removeEventListener(arg, value)
}
})
Would that be simple enough to be considered as a declarative solution for these tests?
That works well.
Given its small size, what do you think about adding the event directive into Vue by default, just so that it's a pattern that components can rely on existing, and that is more likely to be findable?
@rictic That's indeed something to consider. However I'm worrried that it's similarity to v-on
would make it an easy point of confusion, and given that it's only real purpose is to work around a limitation in the handling of custom events, I'd be a bit hesitant to make it a new API on the same level as v-on
. However that's just my gut feeling for now.
At the minimum, we should document the limitation and this pattern as a recommended solution in your section about "Vue & Custom Elements" here.
The Vue docs still say that "Vue scores a perfect 100% in the Custom Elements Everywhere tests" but the tests actually show 91% score:
@LinusBorg I’ve seen this become a point of friction in migrating from vue2 to vue3 if the app uses a lot of custom elements - you end up with either less idiomatic Vue code throughout by using custom directives for very common event handling, or waiting on publishers of the web components you use to update their library.
I find it surprising as a user that you can’t simply consume a web component and listen for the exact event you want - I know this leads to confusion for others, too. The pattern Vue already follows for props with the .prop
modifier seems like it would make sense here; that prefixes the prop with .
to indicate different treatment. That helps with setting properties on Web Components, and I’d expect a similar feature for CustomEvents, like v-on:fooBar.custom
to indicate that you are trying to listen for this exact CustomEvent. I’d anticipate the resulting prop after transformation could be on.fooBar
, so that existing case transformations don’t capitalize it, and in patchEvent you can check for .
to know to use the following event name unchanged. Or it can be a different distinguishing character (like :
).
Happy to open an RFC for this if you think it has any potential - I do think this feels like a gap that Vue itself should handle somehow.
related RFC discussion https://github.com/vuejs/rfcs/discussions/451
I believe this was fixed in 3.2.38 via https://github.com/vuejs/core/commit/0739f8909a0e56ae0fa760f233dfb8c113c9bde2.
Update: Running the original reproduction against the latest Vue still shows the reported problem. However, I believe that's because isCustomElement
is not set correctly, which is also why it shows a warning. I'm not aware of any way to change that setting on the SFC Playground, so I've created a reproduction here: https://jsfiddle.net/skirtle/4ery6m1z/. I believe that shows that the original problem is fixed.
You may notice that it works even without the isCustomElement
. However, if you move the customElements.define
call to the bottom (rather than having it at the top) then it behaves the same as the SFC Playground example. In that case, isCustomElement
does fix the problem. Here's the broken version, for comparison: https://jsfiddle.net/skirtle/y4z2n0w8/.
Just checked this and yes, the changed mentioned by @skirtle addresses this issue, though only the related issues in the docs repo were mentjoned in the commit message as this one was already closed at that point.
So this issue is now solved.
As for now, the website https://custom-elements-everywhere.com is still showing Vue as 91% web component compliant.
I raise a PR to solve that: https://github.com/webcomponents/custom-elements-everywhere/pull/2074