-
-
Notifications
You must be signed in to change notification settings - Fork 33.8k
Description
This ties in to question/suggestion #4332 which was closed, but I have a common scenario where this would be very useful, and the proposed solution in that suggestion would be difficult to apply here.
Suggestion
A limitation I've ran into when authoring reusable components is that you can't add event handlers to a <slot>
.
For example: I'm making a logout button I intend to reuse throughout multiple apps. I want to allow apps to override the actual button element, without having to worry about handling the click event to call the logout() method. This is a slightly contrived example but it illustrates my point.
What I would like to be able to do is the following:
logout-button.vue
<template>
<div class="logout-button">
<slot @click="logout()">
<button type="button" class="btn btn-primary">Log out</button>
</slot>
</div>
</template>
<script>
export default {
methods: {
logout() {
// ...
}
}
};
</script>
The @click
handler on the slot would then be applied to the components within the slot (here just the button).
(ideally I'd also like to be able to use a <slot>
as a component's root as long as it never ends up containing more than one element, but that's off topic here, and I'm not sure if that would ever be possible)
A parent component could then override the button like this:
parent-component.vue
...
<logout-button>
<my-button>Sign out</my-button>
</logout-button>
...
And everything would still work. Clicking the <my-button>
would trigger the @click
handler.
Current solutions
To my knowledge, there are currently three ways to implement similar behaviour:
- Add the
@click
directive to a wrapper element. This seems like an obvious choice here because we already need the wrapper for our component to work, but when you have multiple named slots, this clutters the DOM with useless elements.
logout-button.vue
<template>
<div class="logout-button" @click="logout()">
<slot>
<button type="button" class="btn btn-primary">Log out</button>
</slot>
</div>
</template>
parent-component.vue
...
<logout-button>
<my-button>Sign out</my-button>
</logout-button>
...
- Use a scoped slot to pass the
logout()
method. This feels like a misuse of scoped slots and also tightly couples parent-component to the implementation of logout-button.
logout-button.vue
<template>
<div class="logout-button">
<slot :logout="logout">
<button type="button" class="btn btn-primary" @click="logout()">Log out</button>
</slot>
</div>
</template>
parent-component.vue
...
<logout-button>
<template scope="{ logout }">
<my-button @click="logout()">Sign out</my-button>
</template>
</logout-button>
...
- The proposed solution in Is it possible to emit event from component inside slot #4332 could work, but only if the reusable
<my-button>
I'm using to override logout-button's default slot were wrapped in another Vue component that knows which event to emit, again tightly coupling them and adding more complexity when I just want to use<my-button>
.
It would look a little like this:
logout-button.vue
<template>
<div class="logout-button">
<slot>
<button type="button" class="btn btn-primary" @click="logout()">Log out</button> <!-- alternative: @click="$emit('logout')" -->
</slot>
</div>
</template>
<script>
export default {
methods: {
logout() {
// ...
}
},
created() {
// catch the logout event emitted by component we insert in slot in parent template
this.$on('logout', this.logout);
}
};
</script>
parent-component.vue
...
<logout-button>
<my-logout-button></my-logout-button>
</logout-button>
...
my-logout-button.vue
<template>
<my-button @click="$parent.$emit('logout')">Sign out</my-button>
</template>
Conclusion
I'm partial to solution 1 here, and perhaps 3 for more complex scenarios, but I feel like it would be even cleaner using my suggested syntax for the reasons stated above.
What do you think? Note that I'm fairly new to Vue, so if I've overlooked anything, I apologize.