This ties in to question/suggestion https://github.com/vuejs/vue/issues/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.
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.
To my knowledge, there are currently three ways to implement similar behaviour:
1) 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>
...
2) 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>
...
3) The proposed solution in https://github.com/vuejs/vue/issues/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>
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.
As explained in #4332, it doesn't make sense to add listeners on <slot>
because <slot>
doesn't always render only a single element. This is why a wrapper element is required.
I'd also take a step back and say slot is simply not the proper mechanism for what you want to do.
When the actual visual content is expected to be provided by the parent component, the only thing your <logout-button>
component does, in fact, is providing the implementation of the logout
method. In this case scoped slots actually looks like the most plausible solution. However, if all it does is providing a JavaScript method, why should it be a component in the first place? It can simply be a JavaScript module that exports the logout
method. Instead of importing a component and try to compose it in the template using esoteric techniques, you simply import the logout
method and use it:
<button @click="logout"></button>
import { logout } from './auth-service'
export default {
methods: {
logout
}
}
If your point is that you want to encapsulate some common markup/styling in <logout-button>
, then I'd suggest allowing the customizations via props instead of slots.
Thanks for the insights.
The example was perhaps a bit simple and you're right that just importing the method would be better in that case.
The idea behind the reusable components I'm creating is that they both provide functionality (e.g. the logout button, or something more complex like a navigation control that's automatically populated) as well as a default look and feel using our style guide. Apps can then implement these components without being involved with their implementation, but still override the styling not just through classes but also through custom elements.
In these more complex scenarios, scoped slots might indeed be the way to go.
I stumbled upon this and would follow @yyx990803 on that. The behaviour right now, however not documented well, is good enough. Maybe a PR for docs would be suited.
I agree with the OP's suggestion. The currently best recommended approach, using scoped slots, is verbose and hampers component reuse.
Say I'm authoring a component that mimics the native \
Just create an event listener component (e.g. "EventListener") and all it does is render the default slot like so:
_EventListener.vue_
export default {
name: 'EventListener'
render() {
return this.$slots.default;
}
}
Now use this <event-listener>
component and wrap it on your <slot>
. Child components inside the slot should emit events to the parent like so: this.$parent.$emit('myevent')
.
Attach your custom events to the <event-listener @myevent="handleEvent">
component.
Most helpful comment
As explained in #4332, it doesn't make sense to add listeners on
<slot>
because<slot>
doesn't always render only a single element. This is why a wrapper element is required.I'd also take a step back and say slot is simply not the proper mechanism for what you want to do.
When the actual visual content is expected to be provided by the parent component, the only thing your
<logout-button>
component does, in fact, is providing the implementation of thelogout
method. In this case scoped slots actually looks like the most plausible solution. However, if all it does is providing a JavaScript method, why should it be a component in the first place? It can simply be a JavaScript module that exports thelogout
method. Instead of importing a component and try to compose it in the template using esoteric techniques, you simply import thelogout
method and use it:If your point is that you want to encapsulate some common markup/styling in
<logout-button>
, then I'd suggest allowing the customizations via props instead of slots.