DEV Community

Cover image for Vue.js tips - How to re-expose slots in wrapping component
Alois Sečkár
Alois Sečkár Subscriber

Posted on • Updated on

Vue.js tips - How to re-expose slots in wrapping component

In my job I am recently building a Vue (Nuxt) component library for future UI modules of our long-term IT project. I decided to base it on Nuxt UI. That is I am taking various components available in Nuxt UI and wrap it with our custom styles and special functionality we need.

Such task require balance between forcing devs using pre-designed components, but also giving them enough freedom to create variety – like having toolbars that always look the same, but allow putting custom set of buttons inside, each with unique functionality related to the actual use case in the future module. This is where Vue’s components slots start to become extremely handy, as this is exactly what they are designed for. If you are not familiar with slots yet, I suggest you take a look at them first, as in this article I just want to talk about a specific issue I came across related to them.

Another example of customizable component is a table – you want a component that provides common features related to tables (having column headers, data rows, being able to filter/sort/select, etc.), but it shouldn’t be coupled to its data. It should allow essentially any type of data to be passed in and displayed as needed.

Nuxt UI’s UTable component works like that. It has some constraints imposed on column definitions (you need to provide an array of objects with certain properties), but you can display anything as data rows, just by passing in objects you want. The values are by default displayed “as they are”, which is not always sufficient, especially with more complex objects, that are JSON-stringified by default. But the component also provides a full set of custom named slots of <COLUMN>-data that allow to tailor contents for each cell as you see fit.

So, when you are embracing UTable component itself, you’re good to go. All you need to do, is to provide <template #<column>-data = { row }> inside the component definition and use row variable, which contains the data object for the current table row to customize cell’s output.

But if you wrap UTable in other component, you’ll get into trouble.

Component slots are not re-exposed in the wrapping component by default, so you cannot call <template #<column>-data = { row }> on a wrapper instance. That was a problem for me, because for sure I didn’t want to limit my users to use primitive values only for table data objects. But how to re-define all the slots, when their names are dynamic and actual values depend on column definition being passed in?

Good news! There is a way and it is actually quite easy. All you need is to include a simple snippet in your wrapper component template:

<template v-for="(_, slot) of $slots" #[slot]="scope">
  <slot :name="slot" v-bind="scope" />
</template>
Enter fullscreen mode Exit fullscreen mode

Having this piece of code in our AVATable component, we can now define <template #<column>-data = { row }> and the custom content bubbles through to the underlying UTable where it is processed and displayed in each table cell accordingly.

YAY! My job here is done and I can call it a day. But what’s going on here?

Why this just works?

Whenever you pass a slot content into the component using a <template #name>, it appears in the $slots object of the component instance. So, this is what we are cycling through using v-for directive. When nothing is being passed, nothing is being rendered inside.

If one or more slots come from above, #[slot]="scope" ensures each will be exposed to a child component(s) and will be processed there as if it is hard coded. But thanks to v-for we will dynamically render just what we need, that is what we receive from a parent implementing our wrapper. Symbol # is a shorthand for v-slot: directive, which assings name to a named slot. And square brackets allow to use dynamic value as the name based on the slot variable.

Template content <slot :name="slot" v-bind="scope" /> allows parent component to reference the named slot and bind data (in our case destructured row object) correctly. It feels like a weird circle-reference, but it gets the job done.

Conclusion

Slots are a powerful tool in Vue.js arsenal. In this article you saw how to stack them up throughout the component tree. The solution is universal and may be re-used on more levels, but “slot-drilling” may not be the best idea, so use it with caution in appropriate use-cases.

If you like to learn more about slots in Vue, I wrote another article about scoped slot props you may want to check out.

Top comments (0)