Rachel
Rachel

Reputation: 727

Selecting multiple rows with React-Table? (Shift + Click)

does anyone know a way to select multiple rows with React-Table. Say if we wish to click a cell in a row and then press SHIFT and then select another row and perhaps color code the selected rows with CSS? Is this even possible?

Upvotes: 7

Views: 10915

Answers (2)

Patrick
Patrick

Reputation: 1407

I built this out with react-table 6.x. I'm using the checkbox table HOC. My enclosing component is a functional component using hooks. It handles shift-click in both directions (top to bottom, bottom to top) and handles shift-unselect.

Basic component setup:

import checkboxHOC from 'react-table/lib/hoc/selectTable'
// see "selectTable" - https://github.com/TanStack/table/blob/v6.8.6/src/hoc/README.md
const CheckboxTable = checkboxHOC(ReactTable)
const TableElement = CheckboxTable

const selection = .... // some array of primary keys

const [lastPrimaryKeySelected, setLastPrimaryKeySelected] = React.useState(null)

return <TableElement toggleSelection={toggleSelection} ... />

Then the biz logic:

  const toggleSelection = React.useCallback(
    (id, e) => {
      const currentlyChecking = !selection.has(id)
      const previouslyChecking = selection.has(lastPrimaryKeySelected)

      if (e.shiftKey && lastPrimaryKeySelected && previouslyChecking === currentlyChecking) {
        // Support shift-click. (Check all locations from lastPrimaryKeySelected to id)
        // This ^ also avoids doing anything if they select a box then shift-unselect another box.

        toggleRange(lastPrimaryKeySelected, id, currentlyChecking)
      } else {
        toggleRange(id, id, currentlyChecking)
      }

      setLastPrimaryKeySelected(id)
    },
    [selection, lastPrimaryKeySelected, toggleRange]
  )

  // Unconditionally check or uncheck the range.
  // checkRange - when false, this will uncheck the range.
  // primaryKeyA / primaryKeyB - the order isn't important.
  const toggleRange = React.useCallback(
    (primaryKeyA, primaryKeyB, checkRange) => {
      const setCopy = new Set(selection)

      // keys are primary keys of some record
      // values are the index of internalData
      const primaryKeyToIndex = {}

      // Seems sketchy but the official examples demonstrate this: https://github.com/TanStack/table/blob/v6.8.6/docs/src/examples/selecttable/index.js#L104-L111
      // https://stackoverflow.com/a/52986052
      const internalData = reactTable.current.getWrappedInstance().getResolvedState().sortedData
      internalData?.forEach((d, index) => {
        primaryKeyToIndex[d._original.id] = index
      })
      // potential (but complicated) optimization here ^: only fill out primaryKeyToIndex for the current page being viewed.
      // It currently holds the entire list of records
      // Complicated because react-table doesn't seem to expose the current page worth of data, so we'd have to calculate offsets ourselves.

      // `sort` makes the following for-loop simpler. Handles shift-clicking from top to bottom, or bottom to top.
      const [startIndex, endIndex] = [primaryKeyToIndex[primaryKeyA], primaryKeyToIndex[primaryKeyB]].sort(
        (a, b) => a - b // JS can't sort numbers correctly without a comparator function
      )

      for (let i = startIndex; i <= endIndex; i += 1) {
        const primaryKeyForIndex = internalData[i]._original.id

        if (checkRange) {
          setCopy.add(primaryKeyForIndex) // handle shift-select
        } else {
          setCopy.delete(primaryKeyForIndex) // handle shift-unselect
        }
      }

      onSelectionChange(setCopy)
    },
    [onSelectionChange, selection]
  )

Upvotes: 0

Young Scooter
Young Scooter

Reputation: 504

I found a way to do it. Let me know if you have any questions. Basically just need to implement on your own.

   state = {
    selected: null,
    selectedRows: [],
  }

  previousRow = null;

  handleClick = (state, rowInfo) => {
    if (rowInfo) {
      return {
        onClick: (e) => {
          let selectedRows = [];
          // if shift key is being pressed upon click and there is a row that was previously selected, grab all rows inbetween including both previous and current clicked rows
          if (e.shiftKey && this.previousRow) {
            // if previous row is above current row in table
            if (this.previousRow.index < rowInfo.index) {
              for (let i = this.previousRow.index; i <= rowInfo.index; i++) {
                selectedRows.push(state.sortedData[i]);
              }
            // or the opposite
            } else {
              for (let i = rowInfo.index; i <= this.previousRow.index; i++) {
                selectedRows.push(state.sortedData[i]);
              }
            }
          } else {
            // add _index field so this entry is same as others from sortedData
            rowInfo._index = rowInfo.index;
            selectedRows.push(rowInfo);
            this.previousRow = rowInfo;
          }
          this.setState({ selected: rowInfo.index, selectedRows })
        },
        style: {
          // check index of rows in selectedRows against all rowInfo's indices, to match them up, and return true/highlight row, if there is a index from selectedRows in rowInfo/the table data
          background: this.state.selectedRows.some((e) => e._index === rowInfo.index) && '#9bdfff',
        }
      }
    } else {
      return {}
    }

Upvotes: 12

Related Questions