-
-
Notifications
You must be signed in to change notification settings - Fork 33.7k
Suggestion: v-on on slots #4781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
As explained in #4332, it doesn't make sense to add listeners on 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 <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 |
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 <select> element. Doing it the standard "Vue way", its use would look something like: <my-select
v-model="model"
:options="options"
></my-select> If I want to make it easy to add a custom class to the option elements, the recommended approach is something like: <my-select
v-model="model"
:options="options"
:optionClass="custom-option"
></my-select> This seems like an anti-pattern to me, since we're using the custom "optionClass" rather than the native "class" HTML attribute. (Of course this doesn't just apply to class but any HTML attribute or component prop, which would otherwise have to be passed through via custom proxy props e.g. "optionClass", "optionId", "optionStyle", "optionTabIndex", etc.) A better approach would be to expose a slot for the options, allowing the parent to compose the structure like this: <my-select v-model="model">
<my-option
v-for="option in options"
class="custom-option"
></my-option>
</my-select> I think this looks a lot better and is more similar to idiomatic HTML. Now, however, we run into a problem when we need to handle the click event for a specific option. My understanding is that the recommended best approach is to use scoped slots to pass a callback like this: <my-select v-model="model" slot-scope="{ handleOptionClick }">
<my-option
v-for="option in options"
class="custom-option"
@click="handleOptionClick"
></my-option>
</my-select> (I realize This works, but now imagine there are event handlers for focus, blur, and 5 different keyboard events. Not only does it become very verbose and cluttered, but it makes it much more difficult and unwieldy to reuse the component in multiple places. Furthermore as the OP mentioned it increases coupling; if, for example, one of the callbacks is renamed, every consumer would have to be updated. Additionally, besides event handlers, I would also make a case for binding attributes. For example, I may require each option to have a certain id so it can be referenced with an ARIA attribute (this would pose a problem for the API, however, since using
Solution: attach listeners to each inserted element, or allow validation of slot content. I actually think validation would be a great addition; in the case of the example <my-select> component, it may require that only <my-option> components be passed to the default slot, just as a native <select> element can only have <option> elements as children. |
My SolutionJust 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 Attach your custom events to the |
For vue3, define it: export default {
name: 'event-listener',
render() {
return this.$slots.default ? this.$slots.default() : '';
}
} Parent: <event-listener @slot-click="onSlotClick">
<slot />
</event-listener> Slot: <button @click="(e) => $emit('slot-click', e)" /> |
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
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
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:
@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
parent-component.vue
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
parent-component.vue
<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
parent-component.vue
my-logout-button.vue
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.
The text was updated successfully, but these errors were encountered: