Trouble with collecting selected items from QTable



  • OK I lied, but not.

    I have a QTable with multi select, it works fine. but the project was getting a bit too big so I decided to separate into modules. But now I would need to somehow pass the selected data into the module(s).

    I decided to use Vuex for this.

    The Problem:

    The Qtable does not have an API for detecting what items are selected or deselected. There are 2 API Events for Selections:

    1. @selection -> function(details) This one is not very useful because it only detects what item got selected or deselected. It triggers before the array of selection is modified. I am not able to get the modified array. (maybe I could with some workaround but is this the way?)

    2. @update:selected -> function(newSelected) This one is the one QTable uses for updating the selected item’s array. The problem with t his one is that it does not triggers when the array is empty.

    My current template is using @selection -> function(details) method to update the selected array in vuex store but there is a delay of 1 change because the method triggers before the array is modified.

    <template>
      <div>
        <q-table
          ref="mainTable"
          class="my-sticky-virtscroll-table"
          style="height: 800px"
          :data="tableData"
          :columns="columns"
          row-key="CODIGO"
          :selected-rows-label="getSelectedString"
          selection="multiple"
          :selected.sync="selected"
          virtual-scroll
          :pagination.sync="pagination"
          :rows-per-page-options="[0]"
          :virtual-scroll-sticky-size-start="48"
          :filter="term"
          flat
          bordered
          @focusin.native="activateNavigation"
          @focusout.native="deactivateNavigation"
          @keydown.native="onKey"
          @selection="onSelect"
        >
          <template v-slot:top-left>
            <q-input
              ref="mainSearchInput"
              debounce="0"
              v-model="term"
              label="Búsqueda"
              filled
              bottom-slots
              clearable=""
              style="width:500px"
              @input="updateSearchTerm(term)"
            >
              <template v-slot:hint
                >Puede hacer búsquedas online con commandos.</template
              >
            </q-input>
          </template>
    
          <template v-slot:body-cell-qty="props">
            <q-td :props="props">
              <q-badge
                :class="{
                  'bg-black': props.value <= 0,
                  'bg-red': props.value > 0,
                  'bg-orange': props.value >= 100 && props.value < 500,
                  'bg-green': props.value >= 500,
                  'text-body1': true
                }"
                :label="props.value"
              />
            </q-td>
          </template>
    
          <template v-slot:body-cell-code="props">
            <q-td :props="props">
              <div class="tableCell">{{ props.value }}</div>
            </q-td>
          </template>
    
          <template v-slot:body-cell-description="props">
            <q-td :props="props" :style="{ width: '50px', whiteSpace: 'normal' }">
              <div class="tableCell">
                {{ props.value }}
              </div>
            </q-td>
          </template>
    
          <!-- <template v-slot:body-cell-qty="props">
            <q-td :props="props">
              <div class="tableCell">{{ props.value }}</div>
            </q-td>
          </template> -->
    
          <template v-slot:body-cell-codAlt="props">
            <q-td :props="props" :style="{ width: '50px', whiteSpace: 'normal' }">
              <div class="tableCell">{{ props.value }}</div>
            </q-td>
          </template>
    
          <template v-slot:body-cell-desAlt="props">
            <q-td :props="props" :style="{ width: '50px', whiteSpace: 'normal' }">
              <div class="tableCell">{{ props.value }}</div>
            </q-td>
          </template>
    
          <template v-slot:body-cell-group="props">
            <q-td :props="props">
              <div class="tableCell">{{ props.value }}</div>
            </q-td>
          </template>
    
          <template v-slot:body-cell-price="props">
            <q-td :props="props">
              <div class="tableCell">{{ props.value }}</div>
            </q-td>
          </template>
    
          <template v-slot:body-cell-discount20="props">
            <q-td :props="props">
              <div class="tableCell">{{ props.value }}</div>
            </q-td>
          </template>
        </q-table>
        <!-- <div class="q-mt-md">Selected: {{ JSON.stringify(selected) }}</div> -->
        <chipDetails></chipDetails>
      </div>
    </template>
    
    <script>
    import path from "path";
    import { remote } from "electron";
    import { Platform } from "quasar";
    import { mapGetters, mapActions } from "vuex";
    
    export default {
      computed: {
        ...mapGetters("central", ["searchTerm", "selectedTableData"])
      },
    
      components: {
        chipDetails: require("components/chipDetails.vue").default
      },
    
      data() {
        return {
          term: "",
          tableData: [],
          selected: [],
          selectedTableData: [],
          navigationActive: false,
          pagination: {
            rowsPerPage: 300
          },
          columns: [
            {
              name: "code",
              required: true,
              label: "Código",
              align: "left",
              // field: row => row.name,
              field: "CODIGO",
              format: val => `${val}`,
              sortable: true
            },
            {
              name: "description",
              align: "left",
              label: "Descipción",
              field: "DESCRIPCION",
              sortable: true
            },
            {
              name: "qty",
              label: "Qty",
              field: "INVENT",
              sortable: true,
              align: "right"
            },
            { name: "codAlt", label: "Cod Alt", field: "COD.ALT.", align: "left" },
            {
              name: "desAlt",
              label: "Desc Alt",
              field: "DESC.ALT.",
              align: "left"
            },
            {
              name: "group",
              label: "Grupo",
              field: "GRUPO",
              align: "left"
            },
            { name: "price", label: "Precio", field: "PRECIO", align: "right" },
            {
              name: "discount20",
              label: "20 %",
              field: "PRECIO",
              align: "right",
              sort: (a, b) => parseInt(a, 10) - parseInt(b, 10)
            }
          ]
        };
      },
    
      methods: {
        ...mapActions("central", ["updateSearchTerm", "updateSelectedTableData"]),
    
        getSelectedString() {
          return (
            this.selected.length +
            " selecionado" +
            (this.selected.length > 1 ? "s" : "") +
            " de " +
            this.tableData.length
          );
        },
    
        activateNavigation() {
          this.navigationActive = true;
        },
    
        deactivateNavigation() {
          this.navigationActive = false;
        },
    
        onSelect(evt) {
          console.log(evt);
          this.updateSelectedTableData(this.selected);
          console.log(this.selected);
        },
    
        onKey(evt) {
          if (
            this.navigationActive !== true ||
            [33, 34, 35, 36, 38, 40].indexOf(evt.keyCode) === -1 ||
            this.$refs.mainTable === void 0
          ) {
            return;
          }
    
          evt.preventDefault();
    
          switch (evt.keyCode) {
            case 36: // Home
              page = 1;
              index = 0;
              break;
            case 35: // End
              page = lastPage;
              index = rowsPerPage - 1;
              break;
            case 33: // PageUp
              page = currentPage <= 1 ? lastPage : currentPage - 1;
              if (index < 0) {
                index = 0;
              }
              break;
            case 34: // PageDown
              page = currentPage >= lastPage ? 1 : currentPage + 1;
              if (index < 0) {
                index = rowsPerPage - 1;
              }
              break;
            case 38: // ArrowUp
              if (currentIndex <= 0) {
                page = currentPage <= 1 ? lastPage : currentPage - 1;
                index = rowsPerPage - 1;
              } else {
                index = currentIndex - 1;
              }
              break;
            case 40: // ArrowDown
              if (currentIndex >= lastIndex) {
                page = currentPage >= lastPage ? 1 : currentPage + 1;
                index = 0;
              } else {
                index = currentIndex + 1;
              }
              break;
          }
    
          if (page !== this.pagination.page) {
            this.pagination = {
              ...this.pagination,
              page
            };
    
            this.$nextTick(() => {
              const { computedRows } = this.$refs.mainTable;
              this.selected = [
                computedRows[Math.min(index, computedRows.length - 1)]
              ];
            });
          } else {
            this.selected = [computedRows[index]];
          }
        }
      },
    
      mounted() {
        if (Platform.is.electron) {
          // get file path
          const filePath = path.join(
            remote.app.getPath("home"),
            "/Dropbox/Sync/inventarioHL.json"
          );
    
          // load the File System to execute our common tasks (CRUD)
          var fs = require("fs");
    
          // read file content
          fs.readFile(filePath, "utf-8", (err, getData) => {
            if (err) {
              alert("An error ocurred reading the file :" + err.message);
            }
            // parse text into json
            var jsonData = JSON.parse(getData);
    
            // add entire data to table
            this.tableData = jsonData;
    
            // console.log(jsonData[0]);
    
            // focus on q-input
            this.$refs.mainSearchInput.$el.focus();
          });
        }
    
        // update the q-input with searchTerm
        this.term = this.searchTerm;
      }
    };
    </script>
    
    <style lang="sass">
    .my-sticky-virtscroll-table
      /* height or max-height is important */
      height: 310px
    
      /* specifying max-width so the example can
        highlight the sticky column on any browser window */
      max-width: 1300px
    
      td:first-child
        /* bg color is important for td; just specify one */
        background-color: #fafafa !important
    
      tr th
        position: sticky
        /* higher than z-index for td below */
        z-index: 2
        /* bg color is important; just specify one */
        background: #fafafa
    
      /* this will be the loading indicator */
      thead tr:last-child th
        /* height of all previous header rows */
        top: 48px
        /* highest z-index */
        z-index: 3
      thead tr:first-child th
        top: 0
        z-index: 1
      tr:first-child th:first-child
        /* highest z-index */
        z-index: 3
    
      td:first-child
        z-index: 1
    
      td:first-child, th:first-child
        position: sticky
        left: 0
    </style>
    
    <style>
    .tableCell {
      font-size: 18px;
    }
    </style>
    

    This is the component I am using to access the selected array

    <template>
      <div class="q-pt-sm">
        <q-chip
          color="secondary"
          text-color="white"
          v-for="(sel, index) in selectedTableData"
          :key="index"
          :label="sel['CODIGO']"
          removable
          @remove="removeSelectedChip(index)"
          clickable
          @click="fixed = true"
        />
        <q-dialog v-model="fixed">
          <q-card>
            <q-card-section>
              <div class="text-h6">Terms of Agreement</div>
            </q-card-section>
    
            <q-separator />
    
            <q-card-section style="max-height: 50vh" class="scroll">
              <p v-for="n in 15" :key="n">
                Lorem ipsum dolor sit amet consectetur adipisicing elit. Rerum
                repellendus sit voluptate voluptas eveniet porro. Rerum blanditiis
                perferendis totam, ea at omnis vel numquam exercitationem aut, natus
                minima, porro labore.
              </p>
            </q-card-section>
    
            <q-separator />
    
            <q-card-actions align="right">
              <q-btn flat label="..." color="primary" v-close-popup />
              <q-btn flat label="OK" color="primary" v-close-popup />
            </q-card-actions>
          </q-card>
        </q-dialog>
      </div>
    </template>
    
    <script>
    import { mapGetters } from "vuex";
    export default {
      data() {
        return {
          fixed: false
        };
      },
      computed: {
        ...mapGetters("central", ["selectedTableData"])
      }
    };
    </script>
    

    Finally this is the vuex store

    const state = {
      searchTerm: "",
      selectedTableData: []
    };
    
    const mutations = {
      updateSearchTerm(state, term) {
        state.searchTerm = term;
      },
      updateSelectedTableData(state, data) {
        state.selectedTableData = data;
      }
    };
    
    const actions = {
      updateSearchTerm({ commit }, term) {
        commit("updateSearchTerm", term);
      },
      updateSelectedTableData({ commit }, data) {
        commit("updateSelectedTableData", data);
      }
    };
    
    const getters = {
      searchTerm: state => {
        return state.searchTerm;
      },
      selectedTableData: state => {
        return state.selectedTableData;
      }
    };
    
    export default {
      namespaced: true,
      state,
      mutations,
      actions,
      getters
    };
    


  • @fenchai

    You can use a 2-way computed property for your selected array :

    computed: {
      selected: {
        get() {
          return this.$store.state.central.selectedTableData
        },
       set(val) {
         this.$store.commit('central/updateSelectedTableData', val)
       }
     }
    

    And of course, remove selected property in data()
    This way, your vuex state will always be in sync with your selection.

    You can also have a look to vuex-map-fields library, which will create getters&setters for you 🙂



  • @tof06 I thought this

    computed: {
        ...mapGetters("central", ["searchTerm", "selectedTableData"])
      },
    

    already did the job.

    I guess I will use 2-way-computed thanks for the info


Log in to reply