Following my previous article about Vue.js slots, another issue I was dealing with were the properties objects in so-called scoped slots.
To understand what they are, consider following code first:
<MyComponent>
<template #custom>
<!-- we cannot access variables from MyComponent’s scope here -->
</template>
</MyComponent>
In the snippet above we are implementing a Vue component and passing the content to its custom
slot. We can do a lot of things here, but we only have access to current component’s scope. That means we can use everything defined in our <script setup>
, but nothing that lives inside <MyComponent>
.
But there are also use cases when you would use the children’s data. To make this possible, Vue automatically exposes a special object with all properties passed into <slot>
element.
Providing the slot in <MyComponent>
looks like this:
<slot name="custom" message="Hello" person="John Doe" />
That object would contain { message: 'Hello', person: 'John Doe' }
. Vue wraps everything except the name
property, as this is a special one reserved for identifying named slots.
The object can be referenced in template like this (the name is arbitrary):
<MyComponent>
<template #custom="slotProps">
<!-- we can access anything what is passed as a prop -->
<!-- into `slot` in MyComponent (except `name`) -->
{{ slotProps.message }}, {{ slotProps.person }}!
<!-- output: Hello, John Doe! -->
</template>
</MyComponent>
To make things a bit more confusing, you may encounter object destructuring together with scoped slots. I would say it is quite common in UI libraries, where devs want to give access to just one specific prop with relevant data (like current row in a table or a data object bind to a form).
Let’s say we only need to know the name of the person coming from within the child component. We can write:
<MyComponent>
<template #default="{ person }">
The person is {{ person }}!
<!-- output: The person is John Doe! -->
</template>
</MyComponent>
The values exposed from a child to a parent like this are not limited to JS primitives. Since you can pass complex objects down into components as props, you can also send them back via slots.
For example, in Nuxt UI’s Table component you can access a template for every single cell with data and you can work with the object rendered on a current row.
Let’s presume we are displaying an invoice object with customer
property. Let’s say it is also an object containing firstName
, lastName
, birthDate
and other personal data. By default, it will be JSON-stringified, which is usually not the right thing we want to display in a table. But we can customize the output anyway we like by utilizing respective <column>-data
template. Out from slot props available in Nuxt UI we can destructure row
to access the object whose data are displayed on the current row:
<template #customer-data="{ row }">
<!-- use `row` object to customize displayed data -->
<!-- customer data will be available as row.customer.[key] -->
</template>
So far, so good. But this becomes tricky in conjunction with TypeScript. Side note: If you haven’t yet, you should start learning TS and benefit from increased type-safety and IDE auto-completion hints. I'll presume you already did.
Although it may take some time to understand TypeScript, I got used to it quickly. But facing scoped slots props, I found out I cannot profit from TS. Depending on the child component you are working with, you might get either no type info at all, or – as in the case of Nuxt UI’s UTable
- it is typed so broadly, that it is effectively of any
type.
This makes sense, because otherwise the component would bother its users by forcing them to transform their data before using it. But it also means goodbye to type checking and code completion. Unless we manage to help ourselves and provide our IDE and respective TypeScript tool proper type based on our data.
Fortunately, this is quite easy. First, we define a custom type for an object with a row
property of the same type we are feeding to the table. Then we use this type to annotate slot prop like this:
<script setup lang="ts">
type TableData = {
row: Invoice // Invoice type has Order, Customer, etc.
}
</script>
<template>
…
<template #customer-data="{ row }: TableData">
<!-- TS now knows row is of type Invoice -->
{{ row.customer.lastName }}
</template>
…
</template>
This way we annotated the whole object before destructuring it. It costs us one extra type. If you insist on typing only the destructured part, you can:
<template #customer-data="{ row }: { row: Invoice }">
But somehow I find this less obvious and rather confusing. Check here to read more about this topic.
Regardless of the approach you pick, the IDE is back on track and you can utilize the cell value template with type safety again. Cheers!
To sum it up, in this article I tried to describe the mechanism of exposing child component properties via slots in Vue.js and also how to enhance them with proper TypeScript types. If you have further questions or remarks, feel free to share them in the comments section below.
Top comments (0)