2

I'm using VueJs 3 to create a dynamic table component, which involves sending down loop index variable to all child components inside a slot

the usage of the component as following

<template>
  <h1>my table</h1>
  <MyTable
    :source="[
      { fname: 'Jhon', lname: 'Smith' },
      { fname: 'Arthur', lname: 'Cromer' },
    ]"
  >
    <MyTableColumn cellTitle="First Name" cellValue="fname"></MyTableColumn>
    <MyTableColumn cellTitle="Last Name" cellValue="lname"></MyTableColumn>
  </MyTable>
</template>

The above shows what i am trying to achieve in which you may set the table data source (array) and any number of columns where each column accepts a title and cell value.

I'm not sure how to send the iteration index (v-for index) down to all components within a slot.

here is the MyTable.vue template

<template>
  <table>
    <tr v-for="(row, index) in $props.source" :key="index">
      {{
        (currentRow = index)
      }}
      <slot></slot>
    </tr>
  </table>
</template>

I've tried scoped slots but did not help. and tried the provide/inject technique by providing the index from MyTable.vue and injecting it in each instance of MyTableColumn.vue, but this hasn't worked as expected because the injected variable always has the last index. not sure if it requires usage of render function.

I've have spent the last couple of days trying to find a solution with no luck.

here you can access the full code demo which has the unexpected behaviour

tony19
  • 125,647
  • 18
  • 229
  • 307
ahmed
  • 14,316
  • 30
  • 94
  • 127

3 Answers3

6

Rendering rows

You can use scoped slots to pass data from MyTable.vue to its slot children:

  1. In MyTable.vue, bind the value of index and row to the <slot>:
<tr v-for="(row, index) in $props.source" :key="index">
  <slot :index="index" :row="row"></slot>
</tr>
  1. In App.vue, access MyTable.vue's slot props via v-slot on a <template>, and bind them to the MyTableColumn's props:
<MyTable
  :source="[
    { fname: 'Jhon', lname: 'Smith' },
    { fname: 'Arthur', lname: 'Cromer' },
  ]"
>
                           
  <template v-slot="{ index, row }">
                                                           
    <MyTableColumn cellTitle="First Name" :index="index" :cellValue="row.fname"></MyTableColumn>
                                                           
    <MyTableColumn cellTitle="Last Name" :index="index" :cellValue="row.lname"></MyTableColumn>
  </template>
</MyTable>

Rendering headers

  1. In MyTable.vue, add a headers prop to contain an array of column titles, and render them above the table body:
defineProps({
  headers: Array,
})
<table>
  <tr>
    <td v-for="header in headers">{{ header }}</td>
  </tr>
  <!-- table body... -->
</table>
  1. In App.vue, bind the desired column titles to <MyTable>.headers:
<MyTable :headers="['First Name', 'Last Name']" ⋯>

demo

tony19
  • 125,647
  • 18
  • 229
  • 307
  • Thank you very much, this is almost what I'm looking for, and after the bounty is over, I believe I'll accept this answer; the only reason I'm not accepting it right now is because scoped slot includes three unnecessary steps that I would want to avoid if possible. 1- adding an index to each column 2-using templates 3-defining field names in an array rather than in the column component. therefore, i am starting a bounty to see if anybody can achieve exactly the same design language as provided in my question. – ahmed Jul 15 '22 at 08:42
  • 1
    @ahmed, your requirement for a solution which doesn't use templates tells us you don't understand how slots work in Vue. The two answers are a lot more alike than different. The major difference in tony's solution is that he only used one slot, which renders two cells on each row. Because he only used one slot, there was no need to name it. My solution uses multiple slots, so I needed to name them. But the principle is the same. Exposing slot scope to outer scope is the same. That's how Vue slots work. – tao Jul 15 '22 at 15:51
  • @tao thanks for clarification. i knew that slots require templates, but their must be other solution that does not use slot/template, maybe "render function" or any other solution that i am not aware of. let us wait and watch with a hope we get more answers :) – ahmed Jul 15 '22 at 17:45
  • @ahmed If you look an alternative to slots, consider updating the question, because currently it doesn't imply that. – Estus Flask Jul 15 '22 at 21:29
2

Let's start with a simple slot example:

<template>
  content which always renders
  <slot name="slotName" :foo="'bar'">
    content to be rendered if slot is not present
  </slot>
  some more content which always renders
</template>

Given the above slotted component, the following markup:

<Slotted>
  <template #slotName="{foo}">
    replacement content for slot - {{ foo }}
  </template>
</Slotted>

would produce

content which always renders
  replacement content for slot - bar
some more content which always renders

, while <Slotted /> would produce:

content which always renders
  content to be rendered if slot is not present
some more content which always renders

Implementing the above into a table, we define the rendered columns as fields and define a slot for each field's cell and one for each field's header. They're dynamic.

To replace any column cell or column header, provide a template for that column's cells. Inside default content, nest a general slot for all cells (#cell) and one for all headers (#header). If provided, will be used for any cell (or header) which doesn't have a specified template. If not provided, default content.

In code:

Table.vue

<template>
  <table>
    <thead>
      <tr>
        <th v-for="{label, key} in fields" :key="key">
          <slot :name="`header(${key})`"
                v-bind="{ label, propName: key }">
            <slot name="header">
              {{ label }}
            </slot>
          </slot>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item, index) in rows" :key="index">
        <td v-for="field in fields" :key="field.key">
          <slot :name="`cell(${field.key})`"
                v-bind="{ 
                  item, 
                  index, 
                  value: item[field.key],
                  field
                }">
            <slot name="cell"
                  v-bind="{ 
                    item, 
                    index, 
                    value: item[field.key],
                    field
                  }">
              {{ item[field.key] }}
            </slot>
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

Use it as:

  <Table :rows="rows" :fields="fields">
    <template #cell(fname)="{ value, index }">
      <span style="color: red" v-text="`${index} - ${value}" />
    </template>
    <template #header(fname)="{ label }">
      <span style="color: blue" v-text="label" />
    </template>
    <template #header="{ label }">
      <!-- applies to all header cells which don't have 
           a #header({key}) template provided.
           The ones which do (eg: `#header(fname)`) override this. -->
      <span :style="{fontStyle: 'italic'}" v-text="label" />
    </template>

  </Table>

Working example.

The beauty of this approach is that now you can use this <Table /> to render any items, without having to change the <Table /> component. You only have to provide the slots which should render something other than the default content.

Note: Inside each slot you have access to whatever was bound to the slot scope. In this case { item, index, value, field } for cells and { label, key } for header cells. Obviously, you're not limited to those. You can bind whatever you want to them.

tao
  • 82,996
  • 16
  • 114
  • 150
  • You are attempting to achieve it in a very unique approach, and while it is impressive, I am more concerned about design language than flexibility. I'm sure your technique will be the first choice for most people that pass by this thread; however, In my situation, I've decided to stick with the design language as is; many thanks for providing this fantastic code. thanks again – ahmed Jul 15 '22 at 08:48
  • The approach is not original, it's a replica of BootstrapVue's [table slots](https://bootstrap-vue.org/docs/components/table#comp-ref-b-table-slots). All I've done was implement two of the slots here, using Vue3 syntax. As you can see from docs, it can be take taken a lot further. As I said, what's great about it is that the item data structure is irrelevant. That's extremely powerful, IMHO. – tao Jul 15 '22 at 14:28
  • @ahmed, I've added nested general slots in codesandbox, just to demo slots can override each other, so you could provide a general header (or cell) slot and still override it with specific ones, for particular columns. Hope it makes sense. – tao Jul 15 '22 at 14:53
1

The alternative to using scoped slots and associated boilerplate code is to add props to children in render function, this is done by modifying vnode objects, similarly to this answer.

MyTable can have a limited set of components that are expected to be used as its children and may receive special treatment, e.g. MyTableColumn.

() => {
  const tableChildren = slots.default?.() || [];
  const tableColumns = tableChildren.filter(vnode => vnode.type === MyTableColumn);
  const tableHeaders = tableColumns.map(vnode => vnode.props.cellTitle);

  return h('table', {}, [
    h('tr', {}, [
      ...tableHeaders.map(header => h('td', {}, header)),
    ]),
    ...props.source.map((row, index) => {
      return h('tr', {}, [
        ...tableColumns.map(vnode => {
          const name = vnode.props.cellValue

          return {
            ...vnode,
            props: {
              ...vnode.props,
              index,
              value: row[name]
            }
          };
        })
      ])
    })
  ...

Here additional index and value props are added to MyTableColumn vnodes prior to rendering.

This can be changed by making MyTableColumn not being rendered at all, just providing necessary information about columns to MyTable and optionally custom layout for them. This approach is more popular in React ecosystem, but can also be seen in Primevue DataTable, for instance. It can be seen that Column components have no templates and so aren't rendered.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565