0
votes

I am currently writing my first full-stack app. I am using bootstrap <b-table> to display content. On row-click, I expand the row to display nested data. Is there a way to iterate over the nested data and display it in nested rows within the parent b-table?

Currently, I can display the data, however it displays in a single row.

component.vue:

<template>
    <div id="report-table" class="report-table">
        <b-container>
            <b-table striped hover sticky-header="100%" 
            :items="reports" 
            :fields="fields"
            responsive="xl"
            @click="clearRowClick"
            @row-clicked="reports=>$set(reports, '_showDetails', !reports._showDetails)"
            >
                <template slot="row-details" slot-scope="row">
                    <template v-for="(proc, index) in row.item.Processes">
                        <b-tr :key=index>
                            <td>{{ proc.Name }}</td>
                            <td>{{ proc.Id }}</td>
                        </b-tr>
                    </template>
                </template>
            </b-table>
        </b-container>
    </div>
</template>

example

In the attached image, the bottom row has been clicked. The content is displayed within a single row, but I would like it to be separate rows, so later I can further click on them to display even more nested content.

data example:

{"_id": <id>, "Hostname": <hostname>, "Address": <address>, "Processes": [{"Name": ApplicationHost, ...}, {"Name": svchost, ...}]

If this is not possible, is there some other Bootstrap element that makes more sense to achieve what I want?

2
If this is not possible, is there some other boostrap element that makes more sense to achieve what I want? => Collapse.tao

2 Answers

2
votes

To strictly answer your question: no, a BootstrapVue <b-table>'s row-details row can't be expanded into more than one row.

The row-details row has severe limitations:

  • it's only one row
  • it's actually only one cell which, through use of colspan is expanded to the full width of the row (which means you can't really use the table columns to align the content of the row-details row).

But... this is web. In web, because it's virtual, virtually anything is possible. When it's not, you're doing-it-wrong™.

What you want is achievable by replacing rows entirely when a row is expanded, using a computed and concatenating the children to their parent row when the parent is in expanded state. Proof of concept:

Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
  el: '#app',
  data: () => ({
    rows: [
      {id: '1', name: 'one', expanded: false, children: [
        {id: '1.1', name: 'one-one'},
        {id: '1.2', name: 'one-two'},
        {id: '1.3', name: 'one-three'}
      ]},
      {id: '2', name: 'two', expanded: false, children: [
        {id: '2.1', name: 'two-one'},
        {id: '2.2', name: 'two-two'},
        {id: '2.3', name: 'two-three'}
      ]}
    ]
  }),
  computed: {
    renderedRows() {
      return [].concat([...this.rows.map(row => row.expanded 
        ? [row].concat(row.children)
        : [row]
       )]).flat()
    }
  }
})
tr.parent { cursor: pointer }
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <table>
    <tr v-for="row in renderedRows" :key="row.id"
        @click="row.children && (row.expanded = !row.expanded)"
        :class="{parent: row.children}">
      <td>{{row.id}}</td>
      <td>{{row.name}}</td>
    </tr>
  </table>
</div>

The example is rather basic (I haven't added BootstrapVue to it, nor have I used its fancy <b-table>), but it demonstrates the principle. Apply it to <b-table>'s :items.


One could even take it a step further and make it recursive, by moving the expansion logic into a method:

new Vue({
  el: '#app',
  data: () => ({
    fields: ['id',  { key: 'expanded', label: ''}, 'name'],
    rows: [{
        id: '1',
        name: 'one',
        expanded: false,
        children: [
          { id: '1.1', name: 'one-one' },
          { id: '1.2', name: 'one-two' },
          {
            id: '1.3',
            name: 'one-three',
            expanded: false,
            children: [
              { id: '1.3.1', name: 'one-three-one' },
              { id: '1.3.2', name: 'one-three-two' }
            ]
          }
        ]
      },
      {
        id: '2',
        name: 'two',
        expanded: false,
        children: [
          { id: '2.1', name: 'two-one' },
          { id: '2.2', name: 'two-two' },
          { id: '2.3', name: 'two-three' }
        ]
      }
    ]
  }),
  computed: {
    items() {
      return [].concat(this.rows.map(row => this.unwrapRow(row))).flat()
    }
  },
  methods: {
    unwrapRow(row) {
      return row.children && row.expanded
        ? [row].concat(...row.children.map(child => this.unwrapRow(child)))
        : [row]
    },
    tbodyTrClass(row) {
      return { parent: row.children?.length, child: row.id.includes('.') }
    }
  }
})
.table td:not(:last-child) { width: 80px; }
.table .bi { cursor: pointer }
tr.child {
  background-color: #f5f5f5;
  font-style: italic;
}
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<script src="//cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>

<div id="app">
  <b-table :items="items"
           :fields="fields"
           :tbody-tr-class="tbodyTrClass">
    <template #cell(expanded)="{item}">
      <b-icon v-if="item.children"
              :icon="item.expanded ? 'chevron-up' : 'chevron-down'"
              @click="item.expanded = !item.expanded" />
    </template>
  </b-table>
</div>
0
votes

One approach (that I've personally used in the past) is simply to put a nested <b-table> inside your child row-details for child data, instead of trying to add them to the outer table.

It's also worth noting that adding child data rows to the outer table could be visually confusing if they don't look distinct enough from their parents.

Example:

new Vue({
  el: '#app',
  data() {
    return {
      reports: [{_id: 'ID#1', Hostname: 'Host1', Address: 'Addr1', Processes: [{Name: 'ApplicationHost', Id: '1'}, {Name: 'svchost', Id: '2'}]},
        {_id: 'ID#2', Hostname: 'Host2', Address: 'Addr2', Processes: [{Name: 'ApplicationHost', Id: '3'}, {Name: 'svchost', Id: '4'}]},],
      fields: ['Hostname', 'Address'],
    }
  },
});
<!-- Import Vue and Bootstrap-Vue -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap@4/dist/css/bootstrap.min.css" /><link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" /><script src="//unpkg.com/vue@latest/dist/vue.min.js"></script><script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>

<div id="app">
    <b-table
      bordered
      striped
      hover
      :items="reports" 
      :fields="fields" 
      @row-clicked="reports=>$set(reports, '_showDetails', !reports._showDetails)"
    >
      <!-- <b-table> nested inside 'row-details' slot: -->
      <template #row-details="row">
        <b-table
          bordered
          :items="row.item.Processes"
          :fields="['Name', 'Id']"
        ></b-table>
      </template>
    </b-table>
</div>