javascriptvue.jsbootstrap-vueexpandpersistent

bootstrap-vue b-table: keep expanded rows expanded on table reload


the expand/collapse part of this works just fine.

Right now I am using javascript startInterval() to reload the table every 2 seconds. Eventually this will be moving to web sockets.

In general, as part of the table load/reload, the system checks to see if it should display the icon " ^ " or " v " in the details column by checking row.detailsShowing, this works fine.

getChevron(row, index) {
 if (row.detailsShowing == true) {     
   return "chevronDown";
 }
   return "chevronUp";
}

When the user selects the " ^ " icon in the relationship column, @click=row.toggleDetails gets called to expand the row and then the function v-on:click="toggleRow(row)" is called to keep track of which row the user selected. This uses a server side system generated guid to track.

Within 2 seconds the table will reload and the row collapses. On load/reload, in the first column it loads, relationship, I call a function checkChild(row), to check the row guid against my locally stored array, to determine if this is a row that should be expanded on load.

<template #cell(relationship)="row"> {{checkChild(row)}} <\template>

if the row guid matches one in the array I try setting

checkChild(row){
  var idx = this.showRows.indexOf( row.item.id);
    if(idx > -1){
     row.item.detailsShowing = true;
     row.rowSelected = true;
     row.detailsShowing == true
     row._showDetails = true;
   }
}

and I am able to see that i have found match, but none of those variables set to true keeps the expanded row open, the row always collapses on reload

anyone have any ideas as to how i can make the row(s) stay open on table reload?


Solution

  • The issue with your code is because of a Vue 2 caveat. Adding properties to objects after they've been added to data will not be reactive. To get around this you have to utilize Vue.set.

    You can read more about that here.

    However, calling a function like you are doing in the template seems like bad practice. You should instead do it after fetching your data, or use something like a computed property to do your mapping.

    Here's two simplified examples.

    Mapping after API call

    {
      data() {
        return {
          items: [],
          showRows: []
        }
      },
      methods: {
        async fetchData() {
          const { data } = await axios.get('https://example.api')
          foreach(item of data) {
            const isRowExpanded = this.showRows.includes(item.id);
            item._showDetails = isRowExpanded;
          }
          this.items = data;
        }
      }
    }
    

    Using a computed

    {
      computed: {
        // Use `computedItems` in `<b-table :items="computedItems">`
        computedItems() {
          const { items, showRows } = this;
          return items.map(item => ({
            ...item, 
            _showDetails: .showRows.includes(item.id)
          }))
        }
      },
      data() {
        return {
          items: [],
          showRows: []
        }
      },
      methods: {
        async fetchData() {
          const { data } = await axios.get('https://example.api')
          this.items = data;
        }
      }
    }
    

    For a more complete example, check the snippet below.

    const {
      name,
      datatype,
      image
    } = faker;
    
    const getUser = () => ({
      uuid: datatype.uuid(),
      personal_info: {
        first_name: name.firstName(),
        last_name: name.lastName(),
        gender: name.gender(),
        age: Math.ceil(Math.random() * 75) + 15
      },
      avatar: image.avatar()
    });
    
    const users = new Array(10).fill().map(getUser);
    
    new Vue({
      el: "#app",
      computed: {
        computed_users() {
          const {
            expanded_rows,
            users
          } = this;
          return users.map((user) => ({
            ...user,
            _showDetails: expanded_rows[user.uuid]
          }));
        },
        total_rows() {
          const {
            computed_users
          } = this;
          return computed_users.length;
        }
      },
      created() {
        this.users = users;
    
        setInterval(() => {
          users.push(getUser());
          this.users = [...users];
        }, 5000);
      },
      data() {
        return {
          per_page: 5,
          current_page: 1,
          users: [],
          fields: [{
              key: "avatar",
              class: "text-center"
            },
            {
              key: "name",
              thClass: "text-center"
            },
            {
              key: "personal_info.gender",
              label: "Gender",
              thClass: "text-center"
            },
            {
              key: "personal_info.age",
              label: "Age",
              class: "text-center"
            }
          ],
          expanded_rows: {}
        };
      },
      methods: {
        onRowClicked(item) {
          const {
            expanded_rows
          } = this;
          const {
            uuid
          } = item;
    
          this.$set(expanded_rows, uuid, !expanded_rows[uuid]);
        }
      }
    });
    <link href="https://unpkg.com/bootstrap@4.5.3/dist/css/bootstrap.min.css" rel="stylesheet" />
    <link href="https://unpkg.com/bootstrap-vue@2.21.2/dist/bootstrap-vue.css" rel="stylesheet" />
    
    <script src="https://unpkg.com/vue@2.6.12/dist/vue.min.js"></script>
    <script src="https://unpkg.com/bootstrap-vue@2.21.2/dist/bootstrap-vue.js"></script>
    <script src="https://unpkg.com/faker@5.5.3/dist/faker.min.js"></script>
    
    <div id="app" class="p-3">
      <b-pagination v-model="current_page" :per-page="per_page" :total-rows="total_rows">
      </b-pagination>
    
      <h4>Table is refresh with a new item every 5 seconds.</h4>
      <h6>Click on a row to expand the row</h6>
      <b-table :items="computed_users" :fields="fields" bordered hover striped :current-page="current_page" :per-page="per_page" @row-clicked="onRowClicked">
        <template #cell(avatar)="{ value }">
          <b-avatar :src="value"></b-avatar>
        </template>
        <template #cell(name)="{ item: { personal_info: { first_name, last_name } }}">
          {{ first_name }} {{ last_name }}
        </template>
        <template #row-details="{ item }">
          <pre>{{ item }}</pre>
        </template>
      </b-table>
    </div>