FieryRider
FieryRider

Reputation: 339

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>

Upvotes: 3

Views: 536

Answers (2)

rozsazoltan
rozsazoltan

Reputation: 8423

Solution # 1 (props.onClick, if you want to pass many properties)

Pass a reactive props object to v-bind where you embed the @click event into a property named onClick. See the example.

const { createApp, reactive } = Vue

const MyComponent = {
  template: `
    <div>
      <slot name="activator" :activatorProps="activatorProps"></slot>
    </div>
  `,
  setup() {    
    const activatorProps = reactive({
      onClick: () => alert('expand')
    })

    return {
      activatorProps 
    }
  }
}

const app = createApp({
  template: `
    <MyComponent>
      <template #activator="{ activatorProps }">
        <button type="button" v-bind="activatorProps">Expand me</button>
      </template>
    </MyComponent>
  `,
})

app.component('MyComponent', MyComponent)
app.mount('#app')
<script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script>

<div id="app"></div>

Solution # 2 (if you want to pass a function alone)

Alternatively, you can also pass just the onClick function under its original name, expand, as mentioned in the question.

const { createApp, reactive } = Vue

const MyComponent = {
  template: `
    <div>
      <slot name="activator" :expand="expand"></slot>
    </div>
  `,
  setup() {    
    const expand = () => alert('expand')

    return {
      expand 
    }
  }
}

const app = createApp({
  template: `
    <MyComponent>
      <template #activator="{ expand }">
        <button type="button" @click="expand">Expand me</button>
      </template>
    </MyComponent>
  `,
})

app.component('MyComponent', MyComponent)
app.mount('#app')
<script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script>

<div id="app"></div>

For my StackOverflow runnable example, I had to use CDN format. Feel free to break down the components into separate files and use the <template> HTML tag and <script setup> for Composition API, of course.

Upvotes: 2

Boris Maslennikov
Boris Maslennikov

Reputation: 361

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

Upvotes: 2

Related Questions