6

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

11
  • How onDelete emit is passed by the parent component ? in this case only color and square are passed by parent Commented Dec 8, 2021 at 9:52
  • @Radeanu <Badge square @onDelete="onDeleteOption(selected_option)">Test</Badge> Commented Dec 8, 2021 at 9:55
  • Sorry, you can check if there exists onDelete listener in this way v-if="$listeners.onDelete" Commented Dec 8, 2021 at 10:05
  • But in Vue 3 $listeners was removed Commented Dec 8, 2021 at 10:06
  • For Vue 3 use v-if="$attrs.onOnDelete" Commented Dec 8, 2021 at 10:41

3 Answers 3

14
+50

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

Sign up to request clarification or add additional context in comments.

8 Comments

@Thore very interesting. I just verified this. You may want to consider reporting this issue to the Vue team
@MaxvandeLaar I do not believe it was submitted and it is explained why here v3.vuejs.org/api/options-data.html#emits and here v3.vuejs.org/guide/component-attrs.html#attribute-inheritance
I'll add a feature request now
@TimWickstrom Thanks! The proposed workaround feels just wrong to me. I hope the RFC has a chance to succeed.
Agreed. It seems very counter intuative to release, document, and promote emits, but then have it break other established functionality when used.
|
3

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.

Comments

0

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" />

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.