Thore
Thore

Reputation: 1838

Vue3: Check if event listener is bound to component instance

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

Answers (3)

ug_
ug_

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>

Composition API

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

Grig Dodon
Grig Dodon

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

Tim Wickstrom
Tim Wickstrom

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.

enter image description here

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

Related Questions