Reputation: 1838
I have a reusable Badge component. I want to be able to add a close/delete button when an onDelete event listener is present on the component instance.
<template>
<div class="flex inline-flex items-center px-2.5 py-0.5 text-xs font-medium select-none" :class="[square ? '' : 'rounded-full']">
<slot />
<button class="cursor-pointer ml-2" @click="$emit('onDelete')">
<XIcon class="flex-shrink-0 h-3.5 w-3.5 text-gray-400 hover:text-gray-500" aria-hidden="true" />
</button>
</div>
</template>
<script>
import { XIcon } from '@heroicons/vue/solid';
export default {
props: {
color: { type: String },
square: { type: Boolean, default: false },
},
components: {
XIcon,
},
emits: ['onDelete'],
};
</script>
If I add a v-if statement to the button, the emit event is executed immediately
<button v-if="$emit('onDelete')" class="cursor-pointer ml-2" @click="$emit('onDelete')">
I'm using Vue 3
Upvotes: 6
Views: 9191
Reputation: 11440
Silly this is so hard to do. I ended up going to the Vue internals to look at how they determine what the event handler is. I got this code from https://github.com/vuejs/core/blob/main/packages/runtime-core/src/componentEmits.ts#L166 which defines the behavior of how a component emits an event and adapted it.
The below code looks at if there is a function that would be called if you invoked emit
and returns true if there is. It's likely only compatibility with the version of Vue I took it from which as of writing this is 3.4.
Edit
Works with Vue 3.5.11
This should work in theory with all event types and could be adapted further to return what ones are there and what ones aren't using the instances emitOptions
.
import { getCurrentInstance, toHandlerKey } from 'vue';
import { hyphenate, camelize } from '@vue/shared';
/**
* Determines if the current component has a listener for a specific event
*
* See https://github.com/vuejs/core/blob/main/packages/runtime-core/src/componentEmits.ts#L166
*
* @param event Name of the event you wish to look for, eg: 'click'
*/
export function hasEmitListener(event) {
const instance = getCurrentInstance();
const props = instance?.vnode.props;
const isModelListener = event.startsWith('update:')
if (!props) return false
let handler =
props[toHandlerKey(event)] ||
// also try camelCase event handler (#2249)
props[toHandlerKey(camelize(event))];
if (!handler && isModelListener) {
handler = props[toHandlerKey(hyphenate(event))]
}
return Boolean(handler);
}
Example code
const { getCurrentInstance, toHandlerKey } = Vue;
const { hyphenate, camelize } = Vue;
/**
* Determines if the current component has a listener for a specific event
*
* See https://github.com/vuejs/core/blob/main/packages/runtime-core/src/componentEmits.ts#L166
*
* @param event Name of the event you wish to look for, eg: 'click'
*/
function hasEmitListener(event) {
const instance = getCurrentInstance();
const props = instance.vnode.props;
if(!props) {
return false;
}
const isModelListener = event.startsWith('update:')
let handler =
props[toHandlerKey(event)] ||
// also try camelCase event handler (#2249)
props[toHandlerKey(camelize(event))];
if (!handler && isModelListener) {
handler = props[toHandlerKey(hyphenate(event))]
}
return Boolean(handler);
}
const Button = {
name: 'Button',
inheritAttrs: false,
components: {
},
data() {
return {
hasClick: hasEmitListener('click'),
};
},
template: `
<button v-if="hasClick" @click="$emit('click', $event)">
Click Me! 😊
</button>
<button v-else>Nothing to do 😔</button
`,
};
const App = {
name: 'App',
components: {
Button,
},
data() {
return {
};
},
template: `
<div>
Button 1:
<Button @click="() => { console.log('hello'); }"></Button>
<br />
Button 2:
<Button></Button>
</div>
`,
};
const app = Vue.createApp({
render: () => Vue.h(App),
});
app.mount('#app');
<script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script>
<div id="app">
</div>
When using composition API, be sure to set the values during the setup flow or in a lifecycle hook like onMounted
. This is because getCurrentInstance
will probably be empty at the time you handle your emit and maybe need this value.
Upvotes: 3
Reputation: 383
My working solution for Vue 3.3
{
...
props: {
onRemove: {
type: Function,
default: undefined,
}
}
}
and for the template:
<template>
<div v-if="onRemove" class="btn" @click="$emit('remove')">Remove</div>
</template>
eslint rule:
"vue/require-explicit-emits": [
"error", {
"allowProps": true
}
]
and usage:
<my-button @remove="remove" />
Upvotes: 0
Reputation: 5701
UPDATE: If your component is using the new emits option in Vue3, which is the recommended best practice from the Vue3 docs, the event listeners will not be apart of the $attrs
. An issue will be submitted to the Vue team for clarification and guidance on why this behaves this way.
I have simplified your example above in StackBlitz to isolate the functionality you are after.
Important note, I am using Vue 3.2.26.
In Vue3 $listeners were removed.
Event listeners are now part of $attrs
. However simply console logging this.$attrs
in Badge
won't display the key you are looking for, they are within targets
but accessible by prepending on
to the bound event name. In your case in the Badge
component you will use onOnDelete
.
Complete working example with Two Badges. One will display because it has a bound event onDelete
, the other will not due to the fact that the bound onDelete
is not present.
https://stackblitz.com/edit/vue-8b6exq?devtoolsheight=33&file=src/components/Badge.vue
Upvotes: 14