logotype

Using Registered Event Listeners as Conditionals in Vue

Today I was writing a form component that needed an optional back button. Since the form component is generic, the back button could point to anything.

I decided to use a component event, so the parent can listen to a back event and do it's own thing. Also: The back button isn't always necessary, so I needed some sort of prop to decide if it should be rendered.

Here what my first iteration looked like:

<!-- GenericForm.vue -->

<template>
    <div>
        <!-- ... -->
        <button v-if="showBackButton" @click="$emit('back')">
            Back
        </button>
    </div>
</template>

<script>
    export default {
        props: {
            showBackButton: { default: false },
        },
    }
</script>

Which looks like this in use:

<generic-form
    :showBackButton="true"
    @back="goBack"
></generic-form>

Something feels wrong here. The prop looks pretty noisy to me, since I'm never going to show it if I didn't register a listener in the first place! (at least in the context of my application)

Turns out, as of Vue 2.4, components have a $listeners property, which is an object that contains a list of handlers listening to the component's events.

Let's clean up our component public API with this in mind.

<!-- GenericForm.vue -->

<template>
    <div>
        <!-- ... -->
        <button v-if="showBackButton" @click="$emit('back')">
            Back
        </button>
    </div>
</template>

<script>
    export default {
        computed: {
            showBackButton() {
                return !! this.$listeners.back;
            },
        },
    }
</script>

Which looks way better than the previous example in use!

<generic-form @back="goBack"></generic-form>

Now the form component will automagically render the back button when it has an event handler listening.