vue.jsvuejs3vue-composition-api

How to expose @click event handler on a slot to parent so the parent can bind it to an element passed to that slot with v-bind like in Vuetify?


First things first I use the <script setup> API.

In Vuetify components like v-menu have a slot called activator where you can put a custom element as your dropdown "button" and bind the onClick listener by v-binding the props passed to that slot from inside the v-menu component like so:

<v-menu>
  <template #activator="{ props: activatorProps }">
    <v-btn v-bind="activatorProps">Expand me</v-btn>
  </template>
  <v-list>
    <v-list-item>1</v-list-item>
    <v-list-item>1</v-list-item>
    <v-list-item>1</v-list-item>
  </v-list>
</v-menu>

How can I create such component with a slot that has @click binded to it from inside the component so I can "bind" the @click event to the element I pass to that slot?

Here is my in-progress dropdown component:

<template>
<div :class="$style['dropdown']">
    <slot name="activator" @click="expand">
    </slot>
    <div :class="$style['dropdown__content']">
        <a href="">Item1</a>
        <a href="">Item2</a>
        <a href="">Item3</a>
    </div>
</div>
</template>

<script setup lang="ts">
import { computed } from "vue"
import { ref } from "vue"

const props = defineProps<{
    backgroundColor?: string
}>()
const expanded = ref(false)
const menuDisplayCssProp = computed(() => expanded.value ? "block" : "none")
const expand = () => {
    expanded.value = !expanded.value
}
</script>

<style module lang="scss">
.dropdown {
    button {
      cursor: pointer;
    }
    a {
        display: block;
        text-decoration: none;
    }
    &__content {
        display: v-bind(menuDisplayCssProp);
        position: absolute;
        background-color: v-bind(backgroundColor);
    }
}
</style>

I want to be able to use it the same way as Vuetify's v-menu:

<AppDropdown>
  <template #activator="{ props: activatorProps }">
    <button type="button" v-bind="activatorProps">Expand me</button>
  </template>
</AppDropdown>

Solution

  • AppDropdown.vue:

    <template>
      <div>
        <slot name="activator" :props="activatorProps" :expanded="expanded"></slot>
        <slot name="content" v-if="expanded"></slot>
      </div>
    </template>
    
    <script setup>
      import { ref } from 'vue'
    
      const expanded = ref(false)
    
      const activatorProps = ref({
        onclick: () => {
          expanded.value = !expanded.value
        },
      })
    </script>
    

    Usage:

    <template>
      <AppDropdown>
        <template #activator="{ props, expanded }">
          <button v-bind="props">{{ expanded ? 'Hide' : 'Show' }}</button>
        </template>
        <template #content>
          <div>Hi!</div>
        </template>
      </AppDropdown>
    </template>
    
    <script setup>
      import AppDropdown from './AppDropdown.vue'
    </script>
    

    Playground