DEV Community

Jason Shimkoski
Jason Shimkoski

Posted on

Building a Table Component with Dynamic Slot Names in Vue 3

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' },
])
Enter fullscreen mode Exit fullscreen mode

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' }
])
Enter fullscreen mode Exit fullscreen mode

With items and fields defined, I'd like the table to be used like this:

<SimpleTable :items="items" :fields="fields" />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

I hope you found this useful.

Thanks!

Top comments (8)

Collapse
 
lambrero profile image
Avin Lambrero

Awesome tut, thank you very much!

Collapse
 
gokhancinar profile image
Gökhan Çınar

thanks so much for this your good work

Collapse
 
cole_maxwell profile image
Cole Maxwell

Thanks this was very helpful! Looking forward to the sortable version

Collapse
 
zeeshan profile image
Mohammed Zeeshan

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!

Collapse
 
ajscommunications profile image
Jason Shimkoski

‘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. 😀

Collapse
 
mariuszmalek profile image
Mariusz Malek

This does not work well with costumization (class)

.
Collapse
 
ajscommunications profile image
Jason Shimkoski

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.

Collapse
 
2devruiz profile image
2DevRuiz

thank very much!, how can I add a filter for total items per page?