$attrs no longer contains events declared in the emits option #397
Replies: 22 comments 39 replies
-
There is another use case, not just passing on attributes. One might want to detect if a component has a listener attached to it or not, and render a part of the component conditional on the presence of the listener. This is currently not possible if the event is declared in emits. The workarounds presented (e.g. specify the listener as a prop) are not conforming to best practices and break consistency in how listeners are to be defined and applied. So we're currently forced to define additional redundant flag props to indicate whether a listener has been added. Rather than another flag like inheritAttrs we'd just like to see an easy way to access all attached listeners to a component like was possible in the past, whether they're present in emits or not. |
Beta Was this translation helpful? Give feedback.
-
Vue support for events (through $emit) is really as "pure pub-sub" as it gets. Anything that deviate from decoupled pub-sub isn't available with $emit, such as:
On the other hand, all those things are possible if you declare an Note that you can still bind it with |
Beta Was this translation helpful? Give feedback.
-
I've run into this limitation as well when trying to simply wrap a 3rd party component whilst maintaining typings. As far as I can tell, currently you have to choose between:
But 1. is not really favourable as I imagine it will obfuscate the events from IDEs etc. I imagine the workaround you are suggesting @jods4 will have similar issues with IDEs & autocompletion? As events as props will probably not be considered. Being able to access emits would allow you to easily wrap a component with typings by doing:
I imagine this type of wrapping is a fairly common use case enough that this should really be supported? e.g. want to just apply some styling to a 3rd party component. I am no Vue expert anymore, but my 2 cents: Prop is defined in your components custom props? It's in Seems neat and tidy and discoverable to me? And if listeners are passed as props under the hood then the similarity in behaviour between It's very strange to me that as soon as you define an event in emits, any listeners are completely hidden from you? I've dug through every item in |
Beta Was this translation helpful? Give feedback.
-
I actually believe that the removal of |
Beta Was this translation helpful? Give feedback.
-
today i lost 3h for 3 line of code thx... what is the current solution?
|
Beta Was this translation helpful? Give feedback.
-
Joining the thread. In process of migrating a huge codebase to vue3 and this is the task I am spending the most time on by very very far. Before <!--before -->
<template>
<!-- emits "app-event" -->
<app-base-component>
<!-- also emits "app-event" -->
<app-other-component v-on="$listeners" />
</app-base-component>
</template> After <!--after -->
<template>
<app-base-component v-bind="fallthroughtAttrs">
<!--
cannot do v-bind="$attrs" because that includes actual attributes like
class, style and other html attributes i don't want to foward
-->
<app-other-component v-bind="forwardListeners" />
</app-base-component>
</template>
<script>
import { omitAttrsListeners, pickAttrsListners } form '@/utils'
export default {
// AppBaseComponent have the same "app-event" listener and i don't want it bound
// to root component only forward to AppOtherComponent, so i disable the default behaviour. Not that by "app-event" i mean
// " a common generic name like "click" "update" etc.
inheritAttrs: false,
// Alternatively I could add the emits option to remove it from $attrs and prevent the
// above scenario, but if I do i'll break the "isInteractive" computed prop and the event will
// also not be forwarded to AppOtherComponent which was the original goal
// emits: ['app-event']
computed: {
// So i still want to fallthrough actual attributes
fallthroughAttrs() {
// omits any onX attr
return omitAttrsListeners(this.$attrs)
},
forwardListeners() {
// picks any onX attr
// Oh do you want to use it on v-on directive? you still have to remove "on" prefix
// and normalize the event name to kebab-case. Make sure your util has that option
return pickAttrsListeners(this.$attrs)
},
isInteractive() {
// before this.$listeners['app-event']
// Only works if i don't add emits option.
return !!this.$attrs.onAppEvent
}
}
}
</script> Of course, we might argue that instead of auto-fallthrough things we should document explicitly what the component receives and emits. I can agree with that to some extent, even if it's more laborious (imagine a component with 10 events). Still does not solve the The workaround checking if a listener was boundDeclaring an 'onPropX' and then using a prop as |
Beta Was this translation helpful? Give feedback.
-
Would be good to get some feedback from the Vue team on this. There seem to be some valid concerns in the community about the lines between these concepts having been blurred too far. Are there any plans to revisit this decision and potentially reintroduce some syntactic sugar to differentiate props and events better? (Even if they are the same object under the hood?) |
Beta Was this translation helpful? Give feedback.
-
I find the combined $attrs generally frustrating coming from Vue2, for the reason that's the inspiration for this thread (which I haven't run into yet but definitely does not feel intuitive — declaring an emit as a prop in order to know whether it exists doesn't feel right.) and for the inability to do reasonable things like separating If we could easily retrieve categorized aspects of listeners somehow, this would really help to remediate all these problems:
|
Beta Was this translation helpful? Give feedback.
-
Just ran into this. I scoured the docs trying to find what I was doing wrong, but they all seem to imply this should work (like for example here https://vuejs.org/guide/components/events.html#declaring-emitted-events), it doesn't say all listeners, just known listeners which makes this seem like a bug, or am I misunderstanding this?:
If this is intended behavior, at the very least there should be a warning for users of a component when they try to pass listeners that are deleted. Another thing I noticed is that the types of the components are too strict regardless, and generate type errors on extra properties. And because the autogeneration of typescript emits/props can't use imports, it's not easy to tell defineProps that there can be extra listeners/properties without defining them all, otherwise this wouldn't be such a problem. I'm working on a component lib I've tried all sorts of thing to get around this but the two things that work short of re-declaring all events, all have downsides:
<script lang="ts">
import { type Events, ... } from "vue"
type EventHandlers<E> = { // taken from vue type defs
[K in keyof E]?: E[K] extends (...args: any) => any
? E[K]
: (payload: E[K]) => void
}
const props = withDefaults(defineProps<{
listeners?: Partial<EventHandlers<Events> & Record<string, any>>
}>(), {
listeners: () => ({}),
})
})
</script>
//user
<lib-component :listeners="{
onKeydown: () => {}
}"/>
<script lang="ts">
export default {
inheritAttrs: false,
emits: {} as any as {
"update:modelValue": (val: string) => void
},
}
</script>
<script setup lang="ts">
const $attrs = omit(useAttrs(), ["onUpdate:modelValue"])
const emit = getCurrentInstance()!.emit
</script> Edit: It was wrong in the first example to use Anyways I heard about the new expanded type support for defineProps and thought finally, I can just import |
Beta Was this translation helpful? Give feedback.
-
Our team is also migrating from Vue 2.x and ran into this issue. Adding the listener via props feels unconventional in our codebase so I'm forced to add an additional prop to validate whether the listener exists or not. <MyComponent clickable @click="() => {}" /> <script>
export default defineComponent({
emits: ['click'],
props: {
clickable: Boolean,
},
...
})
</script>
Edit: after some reading and testing I've found a better solution than the one above. This works even if you don't have <MyComponent @click="() => {}" /> <script>
export default defineComponent({
emits: ['click'],
props: {
onClick: Function // use to determine if component has `@click` listener
},
// So basically @click is turned into an onClick prop
computed: {
hasClickListener() {
return !!this.onClick
}
}
...
})
</script>
|
Beta Was this translation helpful? Give feedback.
-
Is there any discussion at the Vue team to revert this change or otherwise
address this issue?
It keeps coming up and clearly affecting a lot of teams, probably more so
those working on larger, more complicated apps.
…On Sat, Apr 29, 2023, 00:55 Samuel Eiche ***@***.***> wrote:
Our team is also migrating from Vue 2.x and ran into this issue. Adding
the listener via props feels unconventional in our codebase so I'm forced
to add an additional prop to validate whether the listener exists or not.
<MyComponent clickable @click="() => {}" />
<script>export default defineComponent({ emits: ['click'], props: { clickable: Boolean, }, ...})</script>
—
Reply to this email directly, view it on GitHub
<#397 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AADXYQTWSCMA22BA2255VIDXDO42VANCNFSM5FLI55AA>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
Our usecase: We have a pretty flexible BaseTable component (even more so now with the new generic props). We are checking for This is no longer possible in Vue3 without adding extra (hacky) code. |
Beta Was this translation helpful? Give feedback.
-
I ran into this same problem in Nuxt + Vue + Ionic app, suggestion from Vue Land slack channel led me to the following solution. Parent with search <top-bar :search="search" @search="searchItems()"></top-bar> Parent without search <top-bar></top-bar> Child <ion-searchbar type="search" :value="search" @ionInput="doSearch" v-if="hasSearch"></ion-searchbar> const props = defineProps({
search: {
type: String,
default: ""
},
onSearch: {
type: Function
}
});
const hasSearch = props['onSearch'] !== undefined;
function doSearch() {
if (props.onSearch) {
props.onSearch();
}
} This allows me to show or hide the |
Beta Was this translation helpful? Give feedback.
-
Maybe this will help someone. In my project I ended up creating corresponding computed properties in my base component like this:
Whenever I need granular controls over attrs/props when it comes to listeners, I extend this class and use the corresponding computed prop. |
Beta Was this translation helpful? Give feedback.
-
Facing the same issue here, using Nuxt3 in <script setup> (but this detail should be unrelated, it's still using Vuejs3). Our solution for now was passing an "options" object in the props of the component, which contains keys like "'hoverable': true, 'shadow': true, 'grow': true, 'clickable': true". After that we made a function which returns the appropriate classes we want directly into the template, although a more proper way would be great. Hope this helps anyone in the meantime!:
|
Beta Was this translation helpful? Give feedback.
-
So they removed access to events but then what's the workaround? If there's none then what's the rationale behind? I'm making a wrapper component and need to pass all event handlers to the child. For props, |
Beta Was this translation helpful? Give feedback.
-
I just came across this migrating to Vue3. We have some components that have default behaviour that you can override by mapping the event. This is simply no longer possible and a huge pain. $listeners is not hurting anyone, so why not just put it back in? |
Beta Was this translation helpful? Give feedback.
-
I found a pretty sweet workaround for any // Component.vue
<script lang="ts" setup>
defineProps<{ value: string }>()
defineEmits(['click'])
const clickEventListener = getCurrentInstance()?.vnode.props?.onClick // handleClick
</setup> It seems, |
Beta Was this translation helpful? Give feedback.
-
I'm using Nuxt 3 and Ionic, but the following worked for me. Component <template>
<ion-searchbar :value="search" @ionInput="doSearch" v-if="hasSearch"></ion-searchbar>
</template>
<script setup>
const props = defineProps({
search: {
type: String,
default: ""
},
onSearch: {
type: Function
}
});
const hasSearch = props['onSearch'] !== undefined;
function doSearch(event) {
if (props.onSearch) {
props.onSearch(event.target.value);
}
} Page <template>
<search-bar :search="state.search" @search="searchChanged"></search-bar>
</template>
<script setup>
const state = reactive({
search: ""
});
function searchChanged(search) {
console.log("searchChanged", search);
state.search = search;
}
</script> |
Beta Was this translation helpful? Give feedback.
-
Makes no sense. Spent a couple of hours with no real solution found. Now the code is not concise:
And in MyComponent.vue:
|
Beta Was this translation helpful? Give feedback.
-
Hallelujah! I discovered a solution for this if your event has a colon 🎉 Register a prop with the syntax Here's an example: <Datatable @click:export="() => {}" /> <template>
<button @click="startExport">Export</button>
<table>
<!-- ... -->
</table>
</template>
<script>
// datatable.vue
export default {
props: {
'onClick:export': {
type: Function,
default: null,
},
},
emits: ['click:export'],
methods: {
startExport() {
this.$emit('click:export');
if (this['onClick:export']) {
// Listener is set.
} else {
// No listener set.
}
}
},
}
</script> |
Beta Was this translation helpful? Give feedback.
-
I will explain theoretically why this should be added. Every framework, theoretically, should only add functionality, never remove it. Otherwise developers have to choose which is better. By removing attrs support, this violates a fundamental law of framework design - never remove functionality! |
Beta Was this translation helpful? Give feedback.
-
As a follow up to vuejs/core#4736
It would seem that it would be a good idea to open up discussion as to whether or not there should be a way to declare somewhere in the component that
$attrs
should maintainonX
listeners even when they are declared in theemits
option of a component.In many cases, a component that extends a sub-component will both want to pass down listeners through
v-bind="$attrs"
attribute fallthrough and through an$emit
, which creates the conflict.Two workarounds are suggested in the above issue, one being adding a
prop
that contains the expected listener/function. The other, explicitly listening for and re-emitting the event.@posva suggests in his comment
a new option similar to
inheritAttrs
which could be a nice less-invasive solution to the problem.Beta Was this translation helpful? Give feedback.
All reactions