1

Intro: I am exploring Vue Js and got stuck while trying to make a dynamic data table component the problem I am facing is that I cannot pass a component via props and render it inside a table.

Problem: So basically what I am trying to do is to pass some custom component from headers prop in v-data-table such as:

headers = [
    { text: 'Name', value: 'name' },
    {
      text: 'Phone Number',
      value: 'phone_number',
      render: () => (
         <div>
           <p>Custom Render</p>
         </div>
     )
    },
    { text: 'Actions', value: 'actions' }
]

So from the code above we can see that I want to render that paragraph from the render function inside Phone Number header, I did this thing in React Js before, but I cannot find a way to do it in Vue Js if someone can point me in the right direction would be fantastic. Thank you in advance.

3
  • 1
    Does this answer your question? Is it possible to pass a component as props and use it in a child Component in Vue? Commented Dec 24, 2021 at 14:22
  • Think you want to look into slots. Commented Dec 24, 2021 at 14:23
  • @match kind of, but it needs to be more dynamic because I have to go to every header key and check if it has a render function and than render that function inside that <td> tag Commented Dec 24, 2021 at 14:44

2 Answers 2

8

You have 2 options - slots and dynamic components.

Let's first explore slots:

<template>
  <v-data-table :items="dataItems" :headers="headerItems">
    <template slot="item.phone_number" slot-scope="{item}">
      <v-chip>{{ item.phone_number }}</v-chip>
    </template>
    <template slot="item.company_name" slot-scope="{item}">
      <v-chip color="pink darken-4" text-color="white">{{ item.company_name }}</v-chip>
    </template>
  </v-data-table>
</template>

The data table provides you slots where you can customize the content. If you want to make your component more reusable and want to populate these slots from your parent component - then you need to re-expose these slots to the parent component:

<template>
  <v-data-table :items="dataItems" :headers="headerItems">
    <template slot="item.phone_number" slot-scope="props">
      <slot name="phone" :props="props" />
    </template>
    <template slot="item.company_name" slot-scope="props">
      <slot name="company" :props="props" />
    </template>
  </v-data-table>
</template>

If you don't know which slots will be customized - you can re-expose all of the data-table slots:

<template>
  <v-data-table
    :headers="headers"
    :items="items"
    :search="search"
    hide-default-footer
    :options.sync="pagination"
    :expanded="expanded"
    class="tbl_manage_students"
    height="100%"
    fixed-header
    v-bind="$attrs"
    @update:expanded="$emit('update:expanded', $event)"
  >
    <!-- https://devinduct.com/blogpost/59/vue-tricks-passing-slots-to-child-components -->
    <template v-for="(index, name) in $slots" v-slot:[name]>
      <slot :name="name" />
    </template>
    <template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
      <slot :name="name" v-bind="data" />
    </template>

    <v-alert slot="no-results" color="error" icon="warning">
      {{ $t("no_results", {term: search}) }}"
    </v-alert>
    <template #footer="data">
      <!-- you can safely skip the "footer" slot override here - so it will be passed through to the parent component -->
      <table-footer :info="data" @size="pagination.itemsPerPage = $event" @page="pagination.page = $event" />
    </template>
  </v-data-table>
</template>

<script>
import tableFooter from '@/components/ui/TableFooter'; // you can safely ignore this component in your own implementation

export default
{
  name: 'TeacherTable',
  components:
    {
      tableFooter,
    },
  props:
    {
      search:
        {
          type: String,
          default: ''
        },
      items:
        {
          type: Array,
          default: () => []
        },
      sort:
        {
          type: String,
          default: ''
        },
      headers:
        {
          type: Array,
          required: true
        },
      expanded:
        {
          type: Array,
          default: () => []
        }
    },
  data()
  {
    return {
      pagination:
        {
          sortDesc: [false],
          sortBy: [this.sort],
          itemsPerPageOptions: [25, 50, 100],
          itemsPerPage: 25,
          page: 1,
        },
    };
  },
  watch:
    {
      items()
      {
        this.pagination.page = 1;
      },
      sort()
      {
        this.pagination.sortBy = [this.sort];
        this.pagination.sortDesc = [false];
      },
    }
};
</script>

Dynamic components can be provided by props:

<template>
  <v-data-table :items="dataItems" :headers="headerItems">
    <template slot="item.phone_number" slot-scope="{item}">
      <component :is="compPhone" :phone="item.phone_number" />
    </template>
    <template slot="item.company_name" slot-scope="{item}">
      <component :is="compCompany" :company="item.company_name" />
    </template>
  </v-data-table>
</template>

<script>
export default
{
  name: 'MyTable',
  props:
  {
    compPhone:
    {
      type: [Object, String], // keep in mind that String type allows you to specify only the HTML tag - but not its contents
      default: 'span'
    },
    compCompany:
    {
      type: [Object, String],
      default: 'span'
    },
  }
}
</script>

Slots are more powerful than dynamic components as they (slots) use the Dependency Inversion principle. You can read more in the Markus Oberlehner's blog

Sign up to request clarification or add additional context in comments.

1 Comment

I didn't try your solution but I bet it will work, thank you for explaining everything to me. Do you see something wrong in my implementation that you can point out so I will know if I need to change my code and make it the same as yours because for now my implementation is working in my case. Thank you :)
2

Okay, I don't believe this is the best way possible but it works for me and maybe it will work for someone else.

What I did was I modified the headers array like this:

 headers = [
    { text: 'Name', align: 'start', sortable: false, value: 'name' },
    {
      text: 'Phone Number',
      key: 'phone_number',
      value: 'custom_render',
      render: Vue.component('phone_number', {
        props: ['item'],
        template: '<v-chip>{{item}}</v-chip>'
      })
    },
    { text: 'Bookings', value: 'bookings_count' },
    {
      text: 'Company',
      key: 'company.name',
      value: 'custom_render',
      render: Vue.component('company_name', {
        props: ['item'],
        template:
          '<v-chip color="pink darken-4" text-color="white">{{item}}</v-chip>'
      })
    },
    { text: 'Actions', value: 'actions', sortable: false }
  ]

And inside v-data-table I reference the slot of custom_render and render that component there like this:

 <template v-slot:[`item.custom_render`]="{ item, header }">
      <component
        :is="header.render"
        :item="getValue(item, header.key)"
      ></component>
    </template>

To go inside the nested object like company.name I made a function which I called getValue that accepts 2 parametes, the object and the path to that value we need which is stored in headers array as key (ex. company.name) and used loadash to return the value.

getValue function:

 getValue (item: any, path: string): any {
    return loadash.get(item, path)
  }

Note: This is just the initial idea, which worked for me. If someone has better ideas please engage with this post. Take a look at the props that I am passing to those dynamic components, note that you can pass more variables in that way.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.