Slot for wrapper content in Vue

Viewed 338

Imagine that you have a user list component UserList that shows users in scrollable view. Each users details are represented with UserDetails component wrapped with Card component.

UserList

<template>
  <Card>
    <UserDetails />
  </Card>
  <Card>
    <UserDetails />
  </Card>
  ...
</template>

Now imagine that you're using this UserList component and want to re-implement the wrapper for UserDetails without modifying it's contents.

Adding a slot would work for replacing the wrapper but everything inside the wrapper would need to be re-implemented as well.

It would be nice if we could write Vue like this:

UserList

<template>
  <slot name="wrapper">
    <Card>
      <template #content>
        <UserDetails />
      </template>
    </Card>
  </slot>
  ...
</template>

Consuming component:

<UserList>
  <template #wrapper>
    ...
    <NewImplementation>
      <slot :name="content" />
    </NewImplementation>
  </template>
</UserList>

It would work by using slots in "reverse" way of how we're used to.

This isn't valid syntax but I bet someone else has thought about the same problem. The need to replace some content with slot but not all of it.

Wrapper component could be given as property but I think it's not the correct solution because we have slots to avoid doing just that.

Are there any good solutions?

2 Answers

If I understood right, what you're looking for is Teleports (previously called Portals) which is only available in version 3.0

What you want is problematic. Vue has no direct support for something like that.

Let me establish some naming conventions before I try to explain why:

  1. Child - UserList component
  2. Parent - component consuming Child

Main problem in this scenario is that slot content is completely generated in the consuming component and just passed to a Child component using

  1. $slots - this is for normal slots. $slots.wrapper is just a static array of VNodes
  2. $scopedSlots - for scoped slots. $scopedSlots.wrapper is a function. When Child renders it calls that function and renders returned an array of VNodes

(Note: In Vue 3 it is just $slots. All slots are functions)

What that means is that Child component has no way to somehow change the content of the slot (e.g. include something in the middle). It can pass some data into it (slot scope) but that's all...

Possible workarounds:

1. Just pass a wrapper component as a prop

Wrapper component could be given as property but I think it's not the correct solution because we have slots to avoid doing just that.

Well not really. Slots have a different purpose. If you want to change the wrapper and let the rest of the Child intact, this is pretty good and simple solution

2. Give a Parent the component it should render inside the wrapper using slot scope

This is something very well demonstrated in route-view component of Vue Router 4.x - see the docs

<UserList v-slot="{ UserComponent, UserData }">
  <NewWrapper>
    <component :is="UserComponent" v-bind:data="UserData" />
  </NewWrapper>
</UserList>

So you have <component :is="UserComponent" /> instead of <UserDetails />. This is useful as the UserList component can now control what component is rendered inside the wrapper (and even change it dynamically)

Related