Allow components to define the slot they are always filling
What problem does this feature solve?
We were currently working on a new UI component library where we want to try and keep the work for devs using it as minimal as possible. Let's consider we have a Page component that comes with a Header and a Body. The Header consists of a Title, and an Icon, and we can have some action buttons.
Keeping our devs in mind, we want to allow them to be able to do the following:
<template>
<Page>
<PageHeader>
<PageTitle>Some brilliant page</PageTitle>
<PageIcon><SomeIconComponent></SomeIconComponent></PageIcon>
<PageActions>
<Button>Update</Button>
<Button>Delete</Button>
</PageActions>
</PageHeader>
<PageBody>
... Contents could go here
</PageBody>
</Page
</template>
Now obviously we don't want the devs to alwahs have to think about how to structure the child components or in what order to pass them in. Our UX designer might like the icon to the left and the buttons to the right of the screen, but he might change his mind later down the road... So: enter named slots.
Just looking at the PageHeader, we would create a component like this:
PageHeader.vue
<template>
<div class="flex flex-row items-center justify-between">
<div class="flex flex-row items-center gap-4>
<slot name="icon"></slot>
<slot name="title"></slot>
</div>
<slot name="actions"></slot>
</div>
</template>
Now this all works brilliantly, but now to pass down the content to the right scope, a developer would have to do the following:
<PageHeader>
<template v-slot:icon>
<PageIcon>
<SomeIconComponent></SomeIconComponent>
</PageIcon>
</template>
<template v-slot:title>
<PageTitle>
Some brilliant page
</PageTitle>
</template>
<template v-slot:actions>
<PageActions>
<Button>Update</Button>
<Button>Delete</Button>
</PageActions>
</template>
</PageHeader>
This gets a bit verbose to me, so I was looking at the short hands that are supported
When there is only one slot passed down, we can get rid of the template
tags because
<SomeComponent v-slot:name>
Hello
</someComponent>
is a shorthand for
<SomeComponent>
<template v-slot:name>
Hello
</template>
</SomeComponent>
But I can't get around the idea that this shorthand is a little useless. Most of the times, you would be naming your slots exactly because you have more than one in a parent component, so those are the use cases where the shorthand doesn't work.
in an ideal world, I want my devs to not having to worry about this, and just be able to nest the components and have the components do their work
What does the proposed API look like?
So building on the example above, we would really like it if we could build our components as follows:
Page.vue
<template>
<div>
<slot name="header"></slot>
<slot name="body"></slot>
</div>
</template>
PageHeader.vue
<template v-slot:header> <!-- we're saying that this component will ALWAYS be providing content for a slot named header -->
<div>
<slot></slot>
</div>
</template>
PageBody.vue
<template v-slot:body> <!-- we're saying that this component will ALWAYS be providing content for a slot named body -->
<div>
<slot></slot>
</div>
</template>
then when we are creating a page, we could just do this:
<Page>
<PageHeader>
header content goes here
</PageHeader>
<PageBody>
body goes here
</PageBody>
</Page>
and vue would know that the pageheader and pagebody components would need to go into the slots named header and body respectively as defined in the Page component.
I've been trying to bend my mind as to why the shorthand doesn't work like this to begin with, but I'm sure I'm missing some info there, so feel free to let me know.
But when creating UI components like this, I believe somehing like this would really make things easier, and clean up a lot of the code we sometimes have.
<Page>
<PageHeader>
header content goes here
</PageHeader>
<PageBody>
body goes here
</PageBody>
</Page>
The above code means the same as the following code in terms of the current semantics:
<Page>
<template v-slot:default>
<PageHeader>
header content goes here
</PageHeader>
<PageBody>
body goes here
</PageBody>
</template>
</Page>
So I think it's probably the biggest obstacle to supporting this feature.
Yes, I realise this. However, since a component in itself is a template, it feels like we have nested tags now.
I very much know it might be a breaking change, but it still feels like one I would enjoy ;-)
This is not even a breaking change, strictly speaking, but it inverts responsibilities and opens up a lot of questions both technically and from a DX perspective.
- Technical: This change would require the compiler to be aware of the child component's "slot setting. this means, we can no longer compile an SFC on its own.
- Similarly, from a DX POV: You can no longer actually infer from the parent template into which slot the content goes. You might be able to guess from the components names, but you will only ever know after having looked into the parent, the child and all of the individual slot components passed to the child.
- Reusability: Will these components
- throw an error if passed to a component not providing the proclaimed slot?
- be usable as children of a normal
<div>
element, where there is no named slot to fill?
- Another DX problem stemming from the last point: Wrapping a slot child component in an element would likely have to break this contract, and require you to refactor into using
<template v-slot:some-name>
again:
<!-- fine -->
<Page>
<PageHeader>
header content goes here
</PageHeader>
<PageBody>
body goes here
</PageBody>
</Page>
<! --- lets add a wrapper div for some positioning -->
<Page>
<PageHeader>
header content goes here
</PageHeader>
<div> <!-- oh no, that div now ends up in the default slot! -->
<PageBody>
body goes here
</PageBody>
<div>
</Page>
<!-- sso we refactor to: -->
<Page>
<PageHeader>
header content goes here
</PageHeader>
<template v-slot:body>
<div>
<PageBody>
body goes here
</PageBody>
<div>
</template>
</Page>
I'm sure a lot more questions would pop up. I would therefore like to move this to the discussions area. If a proper proposal would come of this, an RFC could be written and officially proposed in the rfcs
repo.
Hi @LinusBorg,
Thanks for the reply, and yes: I totally agree with your remarks / views. Some feedback / questions I have:
- Technical: you mean you can not compile the parent sfc on its own then? Because I don't see an issue for the child component, unless I'm missing something that I didn't get. Also, for the parent: what is the use case of wanting to compile a parent without its children? I'm guessing you have a reason and valid point here, but I just don't see it.
- I agree. However, it would be up to the developer to decide whether he wants the behaviour; he would still have the ability to use the syntax if he prefers. By reversing the logic, in fact, he would be able to do something like
<Page>
<PageHeader v-slot:header>
</PageHeader>
</Page>
as a shorthand for
<Page>
<template v-slot:header>
<PageHeader>
</PageHeader>
</template>
</Page>
which in my book also makes more sense as then it feels like I'm adding the v-slot directive to the of the PageHeader component. It just reads more logically to me.
- Reusability: In my book, this would only be used in components that belong to a UI library where you can't have a child without the parent for instance. You can't have a
li
without a wrappingul
orol
for instance. Likewise we would only be using it where a PageHeader for instance is supposed to be wrapped inside of a Page component otherwise messing up the UI. It would actually prevent devs to use the PageHeader outside of a Page component. I agree however that the usage would have to be documented, but I still see a lot of added value here.
I totally understand this is not a quick bugfix and definitely believe this should be treated as an RFC. Is that something you need me to do something for?
Technical: you mean you can not compile the parent sfc on its own then? Because I don't see an issue for the child component, unless I'm missing something that I didn't get.
slot content is compiled into a function which is passed down to the child component. This function has a closure over the parent template, but is run in the child, so the child collects the reactive dependencies (so that the child re-rendered when they change, not the parent. So the compiler has to know - while processing the parent component - which part of the slot content has to be wrapped in a function, and into which slot it has to be passed.
h(Parent, {}, {
header: () => h(PageHeader)
default: () => h(PageBody)
})
Also, for the parent: what is the use case of wanting to compile a parent without its children? I'm guessing you have a reason and valid point here, but I just don't see it.
So far, the compiler can process each SFC completely on its own. it's not important what other SFCs contain in order for the compiler to do its work.
Your proposal would require a re-work of the compiler to process the child components of an SFC as well, in order to understand which slot they belong in. Further: Vue allows for components to be globally registered. Now the compiler would have to somehow know which components from the template are registered globally, and where to find their code, in order to check which slot they belong in, and so forth.
Also, components could be imported from a precomiled package in node_modules
, so now the compiler has to analyse the AST of this package to find the component, and its slot setting while compiling Parent
.
Just a giant complexity increase.
Reusability: In my book, this would only be used in components that belong to a UI library where you can't have a child without the parent for instance. You can't have a li without a wrapping ul or olfor instance. Likewise we would only be using it where a PageHeader for instance is supposed to be wrapped inside of a Page component otherwise messing up the UI. It would actually prevent devs to use the PageHeader outside of a Page component. I agree however that the usage would have to be documented, but I still see a lot of added value here.
I find this to be a limited use case that so far, doesn't warrant the complexity and work is would bring with it.
I totally understand this is not a quick bugfix and definitely believe this should be treated as an RFC. Is that something you need me to do something for?
I personally am not a fan of the implicit context-dependent behavior this proposal would introduce, just for saving keystrokes. An RFC is a lot of work and needs something how believes in the proposal and is willing to work for it.
I am not that person. You either need to be that yourself or find someone willing to be.
PSA: I'll move this to discussions.