Subscribe on changes!

Support `emit` on `<slot>`

avatar
Feb 19th 2023

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.

avatar
Feb 19th 2023

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):

  1. A slot could render a list of many elements, should the listener be placed on each of these?
  2. How should the current functionality of scoped slots be kept working with this change?
avatar
Feb 19th 2023

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 🙂

avatar
Feb 19th 2023

also, @LinusBorg what is the Vue 3 way of doing this https://github.com/vuejs/vue/issues/4332#issuecomment-263444492

avatar
Feb 19th 2023

The Vue 3 way of doing something like the Dialog example is to use a scoped slot:

https://sfc.vuejs.org/#eNqFkrFu3DAMhl+F1WIH6NnoenCCFEmB7h06VB3ubPpOiSQKknw3GH73UJbjOMmQyfrF3yQ/UqP46Vx1GVDsRRNar1yEgHFwd9Iq48hHGMFjDxP0ngwUbC3W0KM6aDotkarOMmV7s/whgw9k3Gp6vVhsANK2ZEMEE05wm2qVxW/UmuAved19K26kbercGjfFIqJx+hCRFUBz/nE3jvPP09TUrObbpbPLLmiKt1KMQPZBU0CYpJgtMjZrb/cdWWTX4pEC6pxmQWLR1GtZ8V1kuJ05uOopkOXhjXPOJRCk2MN8k+4YNGkpzjG6sK/r0LeJ/ilU5E81nyo/2KgMVhjM7ujpGtBzYilSiknaiUu+DTet6v0MOnWZD3xMvHDfJgwGmr8bnGzcsLD4sPV+sG1UZGH+t7zJHFdlO7pWB40+lsUy3XCmQXdwxOzNuwJI6ySNFVvKYhNhkHWRDLR9CJ+RjkOM3ASTqPaZSdAoLpz2xKnyBgEeWWawbP+KLfcWISXjt9Zhryz+YhHKfzn3/+1rE9MLT9cQqQ==

If we had something like you asked, then the @closeevent 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

  1. makes it clear what is being passed from where to where and
  2. 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

avatar
Feb 19th 2023

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.

avatar
Feb 19th 2023

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.

avatar
Feb 19th 2023

Looks like Angular has this functionality https://stackoverflow.com/a/65294062

avatar
Feb 19th 2023

I suppose this would be possible in Vue 3 if this.$on(...) still existed

avatar
Feb 20th 2023

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" />
avatar
Feb 20th 2023

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).

avatar
Feb 21st 2023

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.

avatar
Feb 21st 2023

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.

avatar
Feb 21st 2023

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.

avatar
Feb 21st 2023

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.

avatar
Feb 21st 2023

Not really, no.

avatar
Feb 21st 2023

@LinusBorg Why do you say that? Is a RouterView like impmentation of slot impossible?

avatar
Feb 21st 2023

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

avatar
Feb 21st 2023

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.

avatar
Feb 22nd 2023

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.

avatar
Feb 22nd 2023

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.

avatar
Feb 22nd 2023

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.

avatar
Feb 22nd 2023

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.

avatar
Feb 22nd 2023

In Vue 2 this would have been possible by using this.$on https://github.com/vuejs/vue/issues/4332#issuecomment-263444492

avatar
May 7th 2023

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();