QTable with less clutter on hierarchical data



  • I have data that has multiple hierarchy levels; imagine each row being an entry with 3 hierarchy levels on top:

    Subject   Date        Sample   Property1  Property2
    Patient1  2018-04-12  Sample1        42g       blue
    Patient1  2018-04-12  Sample2        10g       blue
    Patient1  2019-02-24  Sample1        45g       blue
    Patient2  2018-07-01  Sample1        22g        red
    

    Now, I would like to suppress rendering cell bodies with repeated values from the previous row, at least for the first columns, similar to this:

    Subject   Date        Sample   Property1  Property2
    Patient1  2018-04-12  Sample1        42g       blue
                          Sample2        10g       blue
              2019-02-24  Sample1        45g       blue
    Patient2  2018-07-01  Sample1        22g        red
    

    While I could do this via the :data array, I would prefer to do that in the body-cell slot, in order to allow for filtering and reordering of rows (sorting is enabled). I think I could do it with the rowIndex introduced in version 1.10, by looking up the previous row and comparing with it.

    But I have two questions:

    • Would you see an obvious problem with this approach, e.g. in terms of performance? It also feels strange to get the current cell neatly unwrapped and presented by QTable, but to then add lengthy code querying the cell value from above.
    • Also, it could be a feature request for quasar / QTable itself, to suppress repeated values?


  • If you really don’t want to use body slots, you could potentially accomplish this with the format function in the column definition. It might be slower than a body slot because I couldn’t find a reference to rowIndex in that function (it would be a nice feature if it was there) but it would be a cleaner template. I probably wouldn’t worry about speed unless it is actually slow. Note that I’m assuming each row has a unique id, say from a mysql db.

    columns: [
        {
            label: 'Patient',
            name: 'patient',
            field: 'patient',
            // ...
            format: (val, row) => {
                const rowIndex = this.data.findIndex(item => item.id === row.id)
                if(rowIndex === -1 || rowIndex === 0) {
                    // this row wasn't found (shouldn't happen) or it's the first row
                    return val
                }
                const prevRow = this.data[rowIndex - 1]
                if(prevRow.patient === val) {
                    return ''
                }
                return val
            },
        }
    ]
    

    Edit: I’m also not positive how it will play with filtering. Another option is to modify the :data array as you said, but set two object properties on each row: patient and patientLabel. Then use the format function like this:

    columns: [
        {
            label: 'Patient',
            name: 'patient',
            field: 'patient',
            // ...
            format: (val, row) => row.patientLabel,
        }
    ]
    

    That will preserve sorting, but override the display.



  • @beets Thanks – it’s good to know yet another option. However, I fear that an O(N²) approach (with findIndex()) is really too slow for me. To give you an impression of the size of the table – it is already kind of slow when scrolling and has a 1-2 seconds delay on sorting. (And one would also have to ensure that format() is called again when sorting / filtering, I guess?!)

    Also, it is not that I “don’t want to use body slots” – I am just wondering whether people find this use of rowIndex reasonable.



  • I just noticed that my plan does not work: Apparently, the slot only has access to the rowIndex in the sorted and filtered table data, but not to the sorted and filtered data array itself! 😞



  • Thanks to @rstoenescu himself (who replied on GitHub, pointing me to the filteredSortedRows computed property), I found a solution that works fine.

    Simplified code:

          <template>
            <div class="... q-pa-md">
              <q-scroll-area class="col row">
                <q-table
                  ref="myTable"
                  :data="tableData"
                  :columns="columns"
                  :pagination.sync="pagination"
                >
                  <template v-slot:body-cell="props">
                    <q-td :props="props">
                      {{ deduplicatedCellValue(props) }}
                    </q-td>
                  </template>
                </q-table>
              </q-scroll-area>
            </div>
          </template>
          
          <script>
          export default {
            data() {
              return {
                tableData: [
                    ...
                ],
                columns: [
                  {
                    name: "subject",
                    label: "Subject",
                    field: "subject",
                    sortable: true,
                    deduplicate: true,
                  },
                  {
                    name: "date",
                    label: "Date",
                    field: "date",
                    sortable: true,
                    deduplicate: true,
                  },
                  {
                    name: "sample",
                    label: "Sample",
                    field: "sample",
                    sortable: true,
                    deduplicate: true,
                  },
                  ...
                ],
                pagination: {
                  rowsPerPage: 0,
                  sortBy: "subject",
                },
              };
            },
            methods: {
              deduplicatedCellValue(props) {
                if (props.rowIndex > 0 && props.col.deduplicate) {
                  let table = this.$refs.myTable.filteredSortedRows;
                  if (props.value == table[props.rowIndex - 1][props.col.field]) {
                    return "";
                  }
                }
                return props.value;
              },
            },
          };
          </script>
    

    It does not seem to come with a big performance penalty, although the previous row has to be computed for every cell anew. However, I had introduced a “deduplicate” column flag anyhow, because I only wanted to do this on the header columns, but not remove identical property values from the later columns. Therefore, the dereferencing of this.$refs.myTable.filteredSortedRows[props.rowIndex - 1] has to be done only for three cells per row, which is acceptable.

    Still, I wonder if QTable should possibly introduce a props.previousRow next to props.row. To me, that would mean much less code on my side, and a possibly slightly more performant solution in the end. But I understand that every feature in Quasar is something that needs to be documented and maintained as well, so I won’t push for it.


Log in to reply