One of the coolest parts of Vue 3 (and 2) is a capability that is often times completely overlooked: the ability to use a slot
with a dynamic name.
Among other things, this gives you a really powerful method of injecting data exactly where you want it within an array of children.
What does that mean for us? Well, let's build a SimpleTable component to find out.
Let's say you want to offer a table component that automatically builds its rows based on an array of objects called items
:
const items = ref([
{ id: 1, title: 'First entry', description: 'Lorem ipsum' },
{ id: 1, title: 'Second entry', description: 'Sit dolar' },
])
In order to build the columns, let's use another array of objects called fields
:
const fields = ref([
{ key: 'title', label: 'Title' },
{ key: 'description', label: 'Description' }
])
With items
and fields
defined, I'd like the table to be used like this:
<SimpleTable :items="items" :fields="fields" />
Awesome, with just a few v-for
statements that loop over items and fields, and a little bit of logic, we can build out our SimpleTable component that automatically generates our rows and columns.
<template>
<table>
<thead>
<tr>
<th v-for="field in fields" :key="field.key">
{{ field.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id">
<td v-for="key in displayedFieldKeys">
{{ item[key] }}
</td>
</tr>
</tbody>
</table>
</template>
<script lang="ts" setup>
import { computed, PropType } from 'vue';
interface TableField {
key: string
label: string
}
interface TableItem {
id: number
[key: string]: unknown
}
const props = defineProps({
fields: {
type: Array as PropType<TableField[]>,
default: () => []
},
items: {
type: Array as PropType<TableItem[]>,
default: () => []
}
})
const displayedFieldKeys = computed(() => {
return Object.entries(props.fields).map(([_key, value]) => value.key)
})
</script>
Totally cool, right!? But, what if we want to modify the content of those glorious table cells based on their field key? For example, making the title bold, or injecting additional data within each cell.
Dynamic slot names to the rescue!
Let's wrap the content of each table cell in one of these slots:
...
<tr v-for="item in items" :key="item.id">
<td v-for="key in displayedFieldKeys">
<slot
:name="`cell(${key})`"
:value="item[key]"
:item="item"
>
{{ item[key] }}
</slot>
</td>
</tr>
...
Now, whenever you want to modify the content of a set of cells based on the field key, you can do this:
<SimpleTable :items="items" :fields="fields">
<template #cell(title)="{ value, item }">
<p>A bold item title: <strong>{{ value }}</strong></p>
<p>Item ID for some reason: {{ item.id }}</p>
</template>
</SimpleTable>
Neato! Now, you can pinpoint the content you want to modify without having to deal with excessive markup.
For the heck of it, I built out a slightly beefier version of this table component with some additional bells and whistles such as caption
support, col
styling, hiding and formatting fields, and determining whether to use th
or td
for cells.
Column sorting will come in a future revision of this article.
<template>
<table>
<caption v-if="!!$slots.caption || caption">
<slot name="caption">{{ caption }}</slot>
</caption>
<colgroup>
<template v-for="field in displayedFields" :key="field.key">
<slot :name="`col(${field.key})`">
<col>
</slot>
</template>
</colgroup>
<thead>
<tr>
<th v-for="field in displayedFields">
<slot :name="`head(${field.key})`" :field="field">
{{ field.label }}
</slot>
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id">
<template v-for="key in displayedFieldKeys">
<Component :is="cellElement(key as string)">
<slot
:name="`cell(${key})`"
:value="format(item, (key as string))"
:item="item"
:format="(k: string) => format(item, k)"
>
{{ format(item, (key as string)) }}
</slot>
</Component>
</template>
</tr>
</tbody>
</table>
</template>
<script lang="ts" setup>
import { computed, PropType } from 'vue';
interface TableField {
key: string
label: string
format?: Function
hidden?: boolean
header?: boolean
}
interface TableItem {
id: number
[key: string]: unknown
}
const props = defineProps({
fields: { type: Array as PropType<TableField[]>, default: () => [] },
items: { type: Array as PropType<TableItem[]>, default: () => [] },
caption: { type: String, default: null }
})
const displayedFields = computed(() => props.fields.filter((i) => !i.hidden))
const displayedFieldKeys = computed(() => {
return Object.entries(displayedFields.value).map(([_key, value]) => value.key)
})
const cellElement = (key: string) => {
const field = props.fields.find((f) => f.key === key)
return field && field.header ? 'th' : 'td'
}
const format = (item: TableItem, key: string) => {
const field = props.fields.find((f) => f.key === key)
return field && field.format ? field.format(item[key]) : item[key]
}
</script>
I hope you found this useful.
Thanks!
Top comments (9)
Awesome tut, thank you very much!
This does not work well with costumization (class)
I find that the end result is less verbose in use, and therefore it much easier to customize with native CSS or libraries like Tailwind CSS.
This article presents the building blocks of the table. You can always customize it further if you want to provide classes for columns or rows.
thank very much!, how can I add a filter for total items per page?
Thanks for the tutorial. One thing I can't wrap my head around is why do we need that computed function? Can you explain a bit more about its use case if you don't mind? TIA!
‘displayedFieldKeys’ allows each item in the items array to have more properties than what is found in the fields array. This means it only displays the properties of an item that actually exist in the fields array.
This is helpful in cases in which you don’t have access or knowledge of the items you are receiving, and only displays what you actually want.
Keeps it very flexible and easy to customize at a later date when you can’t remember what the heck you were doing in your code. 😀
thanks so much for this your good work
Thanks this was very helpful! Looking forward to the sortable version
Really interesting! I gonna recommend it to a friend who started learning about slots.
Thank you!