Support `emit` on `<slot>`
What problem does this feature solve?
https://github.com/vuejs/vue/issues/4332#issuecomment-1435960777
What does the proposed API look like?
The usual emit API but with support for slots.
Slots have a very different purpose. passing properties (or event listeners) to slots enables the parent component to decide where to place those.
See: https://vuejs.org/guide/components/slots.html#scoped-slots
So I'm not sure what the purpose of this request is, what kind of problem it solves. I read the linked comment, but your observation isn't really correct:
Emit does not work on
<slot></slot>
but it does work on<RouterView />
. This behavior is inconsistent. Both of these things host unknown children.
<RouterView>
is a component, and it always renders another component. So it can pass on all props and listeners that you pass to it to that component, and leave it to that component to decide what to do with them.
a <slot>
is not a component, it's a placeholder, marking where to render child elements passed in from the parent. And as explained above, props and listeners defined on a slot will be exposed to the parent in what we call "scoped Slots" so the parent can decide what to do with them.
So this feature request sounds like it's asking that we change the semantics of <slots>
and automatically apply event listeners to the root element that is contained in the slot. Is that about right?
Ignoring the fact that this would be a big breaking change so it would have to wait for Vue 4 (which we are not even thinking about):
- A slot could render a list of many elements, should the listener be placed on each of these?
- How should the current functionality of scoped slots be kept working with this change?
so my use case looks something like this
Dialog
component which has show/hide logic :
<div>
<slot></slot>
</div>
and then
<Dialog>
<SomeDialogContents /> <-- responsible for telling parent to "hide" when finished
</Dialog>
at the moment, I have implemented it like this: https://github.com/vuejs/vue/issues/4332#issuecomment-1418207447
does this make sense? if not I can add more details 🙂
also, @LinusBorg what is the Vue 3 way of doing this https://github.com/vuejs/vue/issues/4332#issuecomment-263444492
The Vue 3 way of doing something like the Dialog example is to use a scoped slot:
If we had something like you asked, then the @close
event on the slot in <Dialog>
would be passed on to <SomeComp />
automatically - but SomeComp
does not emit a close
event, it emits a done
event - so the close
event would never be triggered. You would then have to rewrite <SomeComp>
to comply with the requirements of <Dialog>
to emit a close
event. Something you will not be able to do with 3rd party components.
A scoped slot as shown in the example is more verbose, but
- makes it clear what is being passed from where to where and
- allows the parent to decide where to pass it to (and do necessary changes like
close
->done
).
if, on the other hand, Dialog
and SomeComp
were two tightly coupled components that are meant to be used strictly together in this way, you could instead fall back on using provide
/ìnject` to open a "backchannel" between parent and child. See i.e. https://skirtles-code.github.io/vue-examples/components/tabs.html#tabs-example
Concerning the linked comment which essentially is about event bubbling like in normal DOM events: We don't do bubbling because it essentially makes component events share a global namespace, which is not something you want in a component architecture focussed on modularity.
You can work around this i.e. with provide/inject for dedicated usecases, but component events will stay "local"; we very, very likely won't change our stance on this in the future. We've been there in Vue 1 and removed it for good reason.
I like the SFC playground example, but it seems like a lot to achieve something fairly simple 🤔
Concerning the linked comment which essentially is about event bubbling like in normal DOM events: We don't do bubbling because it essentially makes component events share a global namespace, which is not something you want in a component architecture focussed on modularity.
You can work around this i.e. with provide/inject for dedicated usecases, but component events will stay "local"; we very, very likely won't change our stance on this in the future. We've been there in Vue 1 and removed it for good reason.
I agree that staying local is the better way to do things, I'm using a DOM event with bubbling only as a workaround.
Earlier you said
a
<slot>
is not a component, it's a placeholder, marking where to render child elements passed in from the parent ...
I propose the addition of a new <slot-component>
that allows the Emit functionality to work as it does with <RouterView />
This would mean no breaking changes, and we can start to use Emit functionality on slots.
Looks like Angular has this functionality https://stackoverflow.com/a/65294062
I propose the addition of a new
that allows the Emit functionality to work as it does with
Not sure how that should work. This should still render slot content, right? so in the following, the listener would be inherited by All the children? If so: I'd vote no, we again have unsafe "global" event names.
<!-- in parent -->
<my-dialog>
<Closebutton /> <!-- we are interested in this component's close event-->
<SomeUnrelatedCompB />
<SomeUnrelatedCompC />
<div>
<SomeUnrelatedCompD />
</div>
</my-dialog>
<!-- my-dialog -->
<slot @close="closeDialog" />
I believe @LinusBorg's solution above is a more solid pattern for a parent component to expose logic to its slot content (not only in Vue 3 but also in Vue 2).
I believe @LinusBorg's solution above is a more solid pattern for a parent component to expose logic to its slot content (not only in Vue 3 but also in Vue 2).
What if the slot content was not just 1 component, but 20? You would have to repeat yourself that many times.
In the unlikely event that you have a component slot that contains 20 individual components (not rendered with v-for
) and all of them should emit the same event, then yes: you would have to add an event binding on each of those 20 components.
Just like you would do if you had all of these 20 components directly in the template instead of passing them in a slot.
the more likely case is that only a few of those 20 actually emit the event we are intersted in, and the explicit syntax of a scoped slot tells you which those are.
Just like you would do if you had all of these 20 components directly in the template instead of passing them in a slot.
no no, we wouldn't have to
<Dialog v-slot="{ onClose }">
<SomeComp1 @done="onClose" />
<SomeComp2 @done="onClose" />
<SomeComp3 @done="onClose" />
...
</Dialog>
would become:
<Dialog>
<SomeComp1 />
<SomeComp2 />
<SomeComp3 />
...
</Dialog>
where the emitted event from SomeCompX
goes straight through to the handler defined on our new slot component in Dialog
.
oh sorry I misunderstood, yes if they were in the template we'd have to explicitly set the emit handler, but luckily for us, they could all be grouped in a slot component where we could set a shared emit handler.
and I mean a new implementation without removing or replacing the current <slot>
so there are no breaking changes and can be introduced in Vue 3
I've repeated this a couple times: RouterView
does not do anything like what you ask. Concerning props and events, all RouterView
does is this, conceptually:
(props) => h(ComponentForCurrentlyActivePath, props)
it passes all attributes and eventlisteners (here represented by props
) on to that one component it should render according to the route config. Nothing else. There's no event bubbling with multiple components going on like what you ask for.
Our component architecture is not built for bubbling events through the vdom, that would be big internal change. We also, as repeated multiple times, value explicitness over terseness/convenience in the case of events.
So we see little value in such a feature, which would be pretty complex to implement as so far, nothing in Vue bubbles. components only communicate parent <-> child.
This is a valid feature request and this issue should have been kept open to host any future discussion and to highlight areas for improvement. Thank you for the display of courtesy. I'm moving to Angular. Bye.
I don't see what good it does to keep a feature request open when we can say with confidence that this is not something we intend to invest any time developing and/or maintaining.
Why can I say that confidently? Because we have discussed and decided against features related to "event bubbling" multiple times in the past. It's something we don't want in the API. It's a conscious design decision, like many others that a framework makes. You are free to disagree with that design decision, of course. And you are free to choose another framework that better fits your design preferences. But it's a decision we still stand by.
I have given you, I think, exhaustive explanation as to why we have things work the things the way they do, the weaknesses we see with an approach such as the one you proposed, and the relative cost we would have if we nevertheless chose to implement it.
Given that picture, I can confidently say we don't see such a feature as part of Vue core, as so I closed the request. That doesn't mean that your use case does not exist or that it's not valid for you and other folks who like things to work the way you want them to. But it's not a fit for how we see Vue in that area.
So it's not going to be part of Vue, and I don't see any value in re-iterating a discussion, which we have already had before in other similar requests, any longer. Closing the request makes that intent clear.
Why can I say that confidently? Because we have discussed and decided against features related to "event bubbling" multiple times in the past. It's something we don't want in the API ...
Ok, so you don't want event bubbling, I understand this.
But I'm certain that the intelligent people working on Vue such as yourself could come up with a "non event bubbling" solution to facilitate this feature.
And if you look at the original issue https://github.com/vuejs/vue/issues/4332
You will see that I am not the only one thinking about this.
In Vue 2 this would have been possible by using this.$on
https://github.com/vuejs/vue/issues/4332#issuecomment-263444492
I think a somewhat way of doing it is like @LinusBorg mentioned, using provide/inject, if you know those components are tight coupled there isn't a problem on doing:
// Dialog
const isClosed = ref(false);
const close = () => isClosed.value = true;
provide('Dialog', { isClosed: readonly(isClosed), close });
// DialogCloseButton
const Dialog = inject('Dialog');
Dialog.close();
You could also use an eventBus
, vueuse
has one:
// Dialog
const isClosed = ref(false);
const bus = useEventBus('dialog-close');
bus.on(() => isClosed.value = true);
// DialogCloseButton
const bus = useEventBus('dialog-close');
bus.emit();