Introduction

In this tutorial, we will explore how to use dynamic slots in Vue.js with Vuetify data table. Firstly, we will start with a CompDataTable.vue component and demonstrate how to define and use custom slots to dynamically customize the content of a data table. Additionally, we will explain how the dynamic slots work.

I’m not going to create a Vue.js project from scratch. Instead, I will use a Vuetify playground. You can also try the content of this blog without creating a local Vue.js project. Specifically, I’m going to use the playground available for the Data table Server-side paginate and sort (Here).

Setting Up a Vue component CompDataTable.vue

First, let’s create a CompDataTable.vue component that uses Vuetify’s v-data-table-server to display data fetched from a fake API. You should have three files as follows:

vuetify.js

import { createVuetify } from 'vuetify'

export const vuetify = createVuetify({
  theme: {
    defaultTheme: 'light',
    //
  },
})

App.vue

<template>
  <CompDataTable></CompDataTable>
</template>

<script setup>
  import CompDataTable from './CompDataTable.vue'
</script>

CompDataTable.vue

<template>
  <v-data-table-server
    v-model:items-per-page="itemsPerPage"
    :headers="headers"
    :items="serverItems"
    :items-length="totalItems"
    :loading="loading"
    :search="search"
    item-value="name"
    @update:options="loadItems"
  ></v-data-table-server>
</template>

<script setup>
  import { ref } from 'vue'

  const desserts = [
    {
      name: 'Frozen Yogurt',
      calories: 159,
      fat: 6,
      carbs: 24,
      protein: 4,
      iron: '1',
    },
    {
      name: 'Jelly bean',
      calories: 375,
      fat: 0,
      carbs: 94,
      protein: 0,
      iron: '0',
    },
    {
      name: 'KitKat',
      calories: 518,
      fat: 26,
      carbs: 65,
      protein: 7,
      iron: '6',
    },
    {
      name: 'Eclair',
      calories: 262,
      fat: 16,
      carbs: 23,
      protein: 6,
      iron: '7',
    },
    {
      name: 'Gingerbread',
      calories: 356,
      fat: 16,
      carbs: 49,
      protein: 3.9,
      iron: '16',
    },
    {
      name: 'Ice cream sandwich',
      calories: 237,
      fat: 9,
      carbs: 37,
      protein: 4.3,
      iron: '1',
    },
    {
      name: 'Lollipop',
      calories: 392,
      fat: 0.2,
      carbs: 98,
      protein: 0,
      iron: '2',
    },
    {
      name: 'Cupcake',
      calories: 305,
      fat: 3.7,
      carbs: 67,
      protein: 4.3,
      iron: '8',
    },
    {
      name: 'Honeycomb',
      calories: 408,
      fat: 3.2,
      carbs: 87,
      protein: 6.5,
      iron: '45',
    },
    {
      name: 'Donut',
      calories: 452,
      fat: 25,
      carbs: 51,
      protein: 4.9,
      iron: '22',
    },
  ]
  const FakeAPI = {
    async fetch({ page, itemsPerPage, sortBy }) {
      return new Promise(resolve => {
        setTimeout(() => {
          const start = (page - 1) * itemsPerPage
          const end = start + itemsPerPage
          const items = desserts.slice()
          if (sortBy.length) {
            const sortKey = sortBy[0].key
            const sortOrder = sortBy[0].order
            items.sort((a, b) => {
              const aValue = a[sortKey]
              const bValue = b[sortKey]
              return sortOrder === 'desc' ? bValue - aValue : aValue - bValue
            })
          }
          const paginated = items.slice(start, end)
          resolve({ items: paginated, total: items.length })
        }, 500)
      })
    },
  }
  const itemsPerPage = ref(5)
  const headers = ref([
    {
      title: 'Dessert (100g serving)',
      align: 'start',
      sortable: false,
      key: 'name',
    },
    { title: 'Calories', key: 'calories', align: 'end' },
    { title: 'Fat (g)', key: 'fat', align: 'end' },
    { title: 'Carbs (g)', key: 'carbs', align: 'end' },
    { title: 'Protein (g)', key: 'protein', align: 'end' },
    { title: 'Iron (%)', key: 'iron', align: 'end' },
  ])
  const search = ref('')
  const serverItems = ref([])
  const loading = ref(true)
  const totalItems = ref(0)
  function loadItems({ page, itemsPerPage, sortBy }) {
    loading.value = true
    FakeAPI.fetch({ page, itemsPerPage, sortBy }).then(({ items, total }) => {
      serverItems.value = items
      totalItems.value = total
      loading.value = false
    })
  }
</script>

Our application is now ready for our main goal, implement dynamic and custom slots.

What is a custom slot ?

In Vuejs, components can receive props, which can be JavaScript values of any type. But what about template content? Sometimes, we may need to pass a template fragment to a child component, allowing the child component to render this fragment within its own template. And that, is what a slot does.

Why do we use slots ?

Slots are a powerful feature in Vue that enable the creation of more flexible and reusable components. Vue slots enable a component to accept dynamic content, referred to as slot content, and display it in a designated spot within the component’s template, which we call the slot outlet. This designated spot is marked by the HTML element <slot>.

The element serves as a placeholder for the content provided by the parent component. This approach addresses the limitation of having a fixed template in a component. It allows users to integrate custom content into a Vue component’s layout, thereby enhancing its flexibility and reusability. In our case, let’s see how we can customize our data table.

Define Dynamic Slots in CompDataTable.vue

We’ll define a dynamic slot template within the CompDataTable.vue component to generate slots for each header defined in dataHeaders.

<template>
  <v-data-table-server
    v-model:items-per-page="itemsPerPage"
    :headers="headers"
    :items="serverItems"
    :items-length="totalItems"
    :loading="loading"
    :search="search"
    item-value="name"
    @update:options="loadItems"
  >
    <template
      v-for="header in headers"
      v-slot:[`item.${header.key}`]="{ item }"
    >
      <slot :name="`item.${header.key}`" :value="item[header.key]">
        {{ item[header.key] }}
      </slot>
    </template>
  </v-data-table-server>
</template>

Below please find explanation about different parts of the code:

  • Dynamic Slots: The v-for directive iterates over each header in the headers array. For each header, a slot is created dynamically.
  • Slot Name: The v-slot directive is bound dynamically using square bracket syntax ([]) and a computed key: item.${header.key}. This creates slots dynamically for each column of the data table, allowing for customized rendering of each column’s content. This creates slots like item.name, item.calories, etc.
  • Slot Props: The { item } syntax destructures the slot props. The item variable represents the data associated with each row of the data table. When using scoped slots in Vue.js, the parent component passes scoped data to the slot content. In this context, item refers to the specific data associated with the current row being rendered in the data table. This allows for customization of each cell in the data table based on the specific data associated with that cell’s row.
    In the context of the v-data-table-server component, Vuetify internally provides the item object representing the current row of data being rendered. When defining slots with v-slot, the content inside the slot gets access to the current item, enabling you to use its properties dynamically.

Using Custom Slots in App.vue

Next, we’ll use these dynamic slots in App.vue to customize the content for each column. For example, let’s start by changing the color of our column calories.

App.vue

<template>
  <CompDataTable>
    <template v-slot:item.calories="{ value }">
      <span style="color: red">{{ value }} kcal</span>
    </template>
  </CompDataTable>
</template>

<script setup>
  import CompDataTable from './CompDataTable.vue'
</script>

Here’s a breakdown of what’s happening:

  • v-slot:item.calories: This is a scoped slot directive (v-slot) that targets the slot named item.calories. In the context of a data table, item usually represents a single row of data, and calories likely refers to one of the columns in the data table.
  • { value }: This is the destructuring syntax used to extract the value associated with the slot. In Vue.js, when passing scoped data to a slot, the parent component can provide scoped data to the slot content. Here, { value } indicates that the parent component (in this case, CompDataTable) is passing the value associated with the calories column to the slot content. This scoped data allows the slot content to access the specific value of the calories column for each row of the data table.
  • Slot Content: Inside the slot content, there’s a <span> element styled with red color (color: red). The {{ value }} kcal part is where the scoped data is being used. {{ value }} outputs the value of the calories column for the current row, and "kcal" is appended to it. This customization allows for the rendering of the calories column with a specific style and formatting.

We could also add a custom logo or even an action button. Even both combined, for example a trash button to delete a row from the data table. The possibilities are limitless 😀

Conclusion

Understanding how to dynamically bind slots in Vue.js allows for powerful customization of components. Especially in scenarios like data tables where each column may require unique rendering logic. By using scoped slots and passing scoped data, we can create highly flexible and reusable components that adapt to various data structures and UI requirements. In the provided code snippet, the item variable plays a crucial role in providing the necessary data context for customizing the content of each cell in the data table.

I hope this blog will be useful to you and that you now understand how to use dynamic slots in Vue.js with Vuetify data table. Also, don’t mind sharing your experiences about it 🙂