@tanstack/table-core
Version:
Headless UI for building powerful tables & datagrids for TS/JS.
669 lines (588 loc) • 22.4 kB
text/typescript
import {
OnChangeFn,
Table,
Row,
RowModel,
Updater,
RowData,
TableFeature,
} from '../types'
import { getMemoOptions, makeStateUpdater, memo } from '../utils'
export type RowSelectionState = Record<string, boolean>
export interface RowSelectionTableState {
rowSelection: RowSelectionState
}
export interface RowSelectionOptions<TData extends RowData> {
/**
* - Enables/disables multiple row selection for all rows in the table OR
* - A function that given a row, returns whether to enable/disable multiple row selection for that row's children/grandchildren
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#enablemultirowselection)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
enableMultiRowSelection?: boolean | ((row: Row<TData>) => boolean)
/**
* - Enables/disables row selection for all rows in the table OR
* - A function that given a row, returns whether to enable/disable row selection for that row
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#enablerowselection)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
enableRowSelection?: boolean | ((row: Row<TData>) => boolean)
/**
* Enables/disables automatic sub-row selection when a parent row is selected, or a function that enables/disables automatic sub-row selection for each row.
* (Use in combination with expanding or grouping features)
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#enablesubrowselection)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
enableSubRowSelection?: boolean | ((row: Row<TData>) => boolean)
/**
* If provided, this function will be called with an `updaterFn` when `state.rowSelection` changes. This overrides the default internal state management, so you will need to persist the state change either fully or partially outside of the table.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#onrowselectionchange)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
onRowSelectionChange?: OnChangeFn<RowSelectionState>
// enableGroupingRowSelection?:
// | boolean
// | ((
// row: Row<TData>
// ) => boolean)
// isAdditiveSelectEvent?: (e: unknown) => boolean
// isInclusiveSelectEvent?: (e: unknown) => boolean
// selectRowsFn?: (
// table: Table<TData>,
// rowModel: RowModel<TData>
// ) => RowModel<TData>
}
export interface RowSelectionRow {
/**
* Returns whether or not the row can multi-select.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getcanmultiselect)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getCanMultiSelect: () => boolean
/**
* Returns whether or not the row can be selected.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getcanselect)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getCanSelect: () => boolean
/**
* Returns whether or not the row can select sub rows automatically when the parent row is selected.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getcanselectsubrows)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getCanSelectSubRows: () => boolean
/**
* Returns whether or not all of the row's sub rows are selected.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getisallsubrowsselected)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getIsAllSubRowsSelected: () => boolean
/**
* Returns whether or not the row is selected.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getisselected)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getIsSelected: () => boolean
/**
* Returns whether or not some of the row's sub rows are selected.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getissomeselected)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getIsSomeSelected: () => boolean
/**
* Returns a handler that can be used to toggle the row.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#gettoggleselectedhandler)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getToggleSelectedHandler: () => (event: unknown) => void
/**
* Selects/deselects the row.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#toggleselected)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
toggleSelected: (value?: boolean, opts?: { selectChildren?: boolean }) => void
}
export interface RowSelectionInstance<TData extends RowData> {
/**
* Returns the row model of all rows that are selected after filtering has been applied.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getfilteredselectedrowmodel)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getFilteredSelectedRowModel: () => RowModel<TData>
/**
* Returns the row model of all rows that are selected after grouping has been applied.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getgroupedselectedrowmodel)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getGroupedSelectedRowModel: () => RowModel<TData>
/**
* Returns whether or not all rows on the current page are selected.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getisallpagerowsselected)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getIsAllPageRowsSelected: () => boolean
/**
* Returns whether or not all rows in the table are selected.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getisallrowsselected)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getIsAllRowsSelected: () => boolean
/**
* Returns whether or not any rows on the current page are selected.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getissomepagerowsselected)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getIsSomePageRowsSelected: () => boolean
/**
* Returns whether or not any rows in the table are selected.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getissomerowsselected)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getIsSomeRowsSelected: () => boolean
/**
* Returns the core row model of all rows before row selection has been applied.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getpreselectedrowmodel)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getPreSelectedRowModel: () => RowModel<TData>
/**
* Returns the row model of all rows that are selected.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getselectedrowmodel)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getSelectedRowModel: () => RowModel<TData>
/**
* Returns a handler that can be used to toggle all rows on the current page.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#gettoggleallpagerowsselectedhandler)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getToggleAllPageRowsSelectedHandler: () => (event: unknown) => void
/**
* Returns a handler that can be used to toggle all rows in the table.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#gettoggleallrowsselectedhandler)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
getToggleAllRowsSelectedHandler: () => (event: unknown) => void
/**
* Resets the **rowSelection** state to the `initialState.rowSelection`, or `true` can be passed to force a default blank state reset to `{}`.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#resetrowselection)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
resetRowSelection: (defaultState?: boolean) => void
/**
* Sets or updates the `state.rowSelection` state.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#setrowselection)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
setRowSelection: (updater: Updater<RowSelectionState>) => void
/**
* Selects/deselects all rows on the current page.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#toggleallpagerowsselected)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
toggleAllPageRowsSelected: (value?: boolean) => void
/**
* Selects/deselects all rows in the table.
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#toggleallrowsselected)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
*/
toggleAllRowsSelected: (value?: boolean) => void
}
//
export const RowSelection: TableFeature = {
getInitialState: (state): RowSelectionTableState => {
return {
rowSelection: {},
...state,
}
},
getDefaultOptions: <TData extends RowData>(
table: Table<TData>
): RowSelectionOptions<TData> => {
return {
onRowSelectionChange: makeStateUpdater('rowSelection', table),
enableRowSelection: true,
enableMultiRowSelection: true,
enableSubRowSelection: true,
// enableGroupingRowSelection: false,
// isAdditiveSelectEvent: (e: unknown) => !!e.metaKey,
// isInclusiveSelectEvent: (e: unknown) => !!e.shiftKey,
}
},
createTable: <TData extends RowData>(table: Table<TData>): void => {
table.setRowSelection = updater =>
table.options.onRowSelectionChange?.(updater)
table.resetRowSelection = defaultState =>
table.setRowSelection(
defaultState ? {} : table.initialState.rowSelection ?? {}
)
table.toggleAllRowsSelected = value => {
table.setRowSelection(old => {
value =
typeof value !== 'undefined' ? value : !table.getIsAllRowsSelected()
const rowSelection = { ...old }
const preGroupedFlatRows = table.getPreGroupedRowModel().flatRows
// We don't use `mutateRowIsSelected` here for performance reasons.
// All of the rows are flat already, so it wouldn't be worth it
if (value) {
preGroupedFlatRows.forEach(row => {
if (!row.getCanSelect()) {
return
}
rowSelection[row.id] = true
})
} else {
preGroupedFlatRows.forEach(row => {
delete rowSelection[row.id]
})
}
return rowSelection
})
}
table.toggleAllPageRowsSelected = value =>
table.setRowSelection(old => {
const resolvedValue =
typeof value !== 'undefined'
? value
: !table.getIsAllPageRowsSelected()
const rowSelection: RowSelectionState = { ...old }
table.getRowModel().rows.forEach(row => {
mutateRowIsSelected(rowSelection, row.id, resolvedValue, true, table)
})
return rowSelection
})
// addRowSelectionRange: rowId => {
// const {
// rows,
// rowsById,
// options: { selectGroupingRows, selectSubRows },
// } = table
// const findSelectedRow = (rows: Row[]) => {
// let found
// rows.find(d => {
// if (d.getIsSelected()) {
// found = d
// return true
// }
// const subFound = findSelectedRow(d.subRows || [])
// if (subFound) {
// found = subFound
// return true
// }
// return false
// })
// return found
// }
// const firstRow = findSelectedRow(rows) || rows[0]
// const lastRow = rowsById[rowId]
// let include = false
// const selectedRowIds = {}
// const addRow = (row: Row) => {
// mutateRowIsSelected(selectedRowIds, row.id, true, {
// rowsById,
// selectGroupingRows: selectGroupingRows!,
// selectSubRows: selectSubRows!,
// })
// }
// table.rows.forEach(row => {
// const isFirstRow = row.id === firstRow.id
// const isLastRow = row.id === lastRow.id
// if (isFirstRow || isLastRow) {
// if (!include) {
// include = true
// } else if (include) {
// addRow(row)
// include = false
// }
// }
// if (include) {
// addRow(row)
// }
// })
// table.setRowSelection(selectedRowIds)
// },
table.getPreSelectedRowModel = () => table.getCoreRowModel()
table.getSelectedRowModel = memo(
() => [table.getState().rowSelection, table.getCoreRowModel()],
(rowSelection, rowModel) => {
if (!Object.keys(rowSelection).length) {
return {
rows: [],
flatRows: [],
rowsById: {},
}
}
return selectRowsFn(table, rowModel)
},
getMemoOptions(table.options, 'debugTable', 'getSelectedRowModel')
)
table.getFilteredSelectedRowModel = memo(
() => [table.getState().rowSelection, table.getFilteredRowModel()],
(rowSelection, rowModel) => {
if (!Object.keys(rowSelection).length) {
return {
rows: [],
flatRows: [],
rowsById: {},
}
}
return selectRowsFn(table, rowModel)
},
getMemoOptions(table.options, 'debugTable', 'getFilteredSelectedRowModel')
)
table.getGroupedSelectedRowModel = memo(
() => [table.getState().rowSelection, table.getSortedRowModel()],
(rowSelection, rowModel) => {
if (!Object.keys(rowSelection).length) {
return {
rows: [],
flatRows: [],
rowsById: {},
}
}
return selectRowsFn(table, rowModel)
},
getMemoOptions(table.options, 'debugTable', 'getGroupedSelectedRowModel')
)
///
// getGroupingRowCanSelect: rowId => {
// const row = table.getRow(rowId)
// if (!row) {
// throw new Error()
// }
// if (typeof table.options.enableGroupingRowSelection === 'function') {
// return table.options.enableGroupingRowSelection(row)
// }
// return table.options.enableGroupingRowSelection ?? false
// },
table.getIsAllRowsSelected = () => {
const preGroupedFlatRows = table.getFilteredRowModel().flatRows
const { rowSelection } = table.getState()
let isAllRowsSelected = Boolean(
preGroupedFlatRows.length && Object.keys(rowSelection).length
)
if (isAllRowsSelected) {
if (
preGroupedFlatRows.some(
row => row.getCanSelect() && !rowSelection[row.id]
)
) {
isAllRowsSelected = false
}
}
return isAllRowsSelected
}
table.getIsAllPageRowsSelected = () => {
const paginationFlatRows = table
.getPaginationRowModel()
.flatRows.filter(row => row.getCanSelect())
const { rowSelection } = table.getState()
let isAllPageRowsSelected = !!paginationFlatRows.length
if (
isAllPageRowsSelected &&
paginationFlatRows.some(row => !rowSelection[row.id])
) {
isAllPageRowsSelected = false
}
return isAllPageRowsSelected
}
table.getIsSomeRowsSelected = () => {
const totalSelected = Object.keys(
table.getState().rowSelection ?? {}
).length
return (
totalSelected > 0 &&
totalSelected < table.getFilteredRowModel().flatRows.length
)
}
table.getIsSomePageRowsSelected = () => {
const paginationFlatRows = table.getPaginationRowModel().flatRows
return table.getIsAllPageRowsSelected()
? false
: paginationFlatRows
.filter(row => row.getCanSelect())
.some(d => d.getIsSelected() || d.getIsSomeSelected())
}
table.getToggleAllRowsSelectedHandler = () => {
return (e: unknown) => {
table.toggleAllRowsSelected(
((e as MouseEvent).target as HTMLInputElement).checked
)
}
}
table.getToggleAllPageRowsSelectedHandler = () => {
return (e: unknown) => {
table.toggleAllPageRowsSelected(
((e as MouseEvent).target as HTMLInputElement).checked
)
}
}
},
createRow: <TData extends RowData>(
row: Row<TData>,
table: Table<TData>
): void => {
row.toggleSelected = (value, opts) => {
const isSelected = row.getIsSelected()
table.setRowSelection(old => {
value = typeof value !== 'undefined' ? value : !isSelected
if (row.getCanSelect() && isSelected === value) {
return old
}
const selectedRowIds = { ...old }
mutateRowIsSelected(
selectedRowIds,
row.id,
value,
opts?.selectChildren ?? true,
table
)
return selectedRowIds
})
}
row.getIsSelected = () => {
const { rowSelection } = table.getState()
return isRowSelected(row, rowSelection)
}
row.getIsSomeSelected = () => {
const { rowSelection } = table.getState()
return isSubRowSelected(row, rowSelection, table) === 'some'
}
row.getIsAllSubRowsSelected = () => {
const { rowSelection } = table.getState()
return isSubRowSelected(row, rowSelection, table) === 'all'
}
row.getCanSelect = () => {
if (typeof table.options.enableRowSelection === 'function') {
return table.options.enableRowSelection(row)
}
return table.options.enableRowSelection ?? true
}
row.getCanSelectSubRows = () => {
if (typeof table.options.enableSubRowSelection === 'function') {
return table.options.enableSubRowSelection(row)
}
return table.options.enableSubRowSelection ?? true
}
row.getCanMultiSelect = () => {
if (typeof table.options.enableMultiRowSelection === 'function') {
return table.options.enableMultiRowSelection(row)
}
return table.options.enableMultiRowSelection ?? true
}
row.getToggleSelectedHandler = () => {
const canSelect = row.getCanSelect()
return (e: unknown) => {
if (!canSelect) return
row.toggleSelected(
((e as MouseEvent).target as HTMLInputElement)?.checked
)
}
}
},
}
const mutateRowIsSelected = <TData extends RowData>(
selectedRowIds: Record<string, boolean>,
id: string,
value: boolean,
includeChildren: boolean,
table: Table<TData>
) => {
const row = table.getRow(id, true)
// const isGrouped = row.getIsGrouped()
// if ( // TODO: enforce grouping row selection rules
// !isGrouped ||
// (isGrouped && table.options.enableGroupingRowSelection)
// ) {
if (value) {
if (!row.getCanMultiSelect()) {
Object.keys(selectedRowIds).forEach(key => delete selectedRowIds[key])
}
if (row.getCanSelect()) {
selectedRowIds[id] = true
}
} else {
delete selectedRowIds[id]
}
// }
if (includeChildren && row.subRows?.length && row.getCanSelectSubRows()) {
row.subRows.forEach(row =>
mutateRowIsSelected(selectedRowIds, row.id, value, includeChildren, table)
)
}
}
export function selectRowsFn<TData extends RowData>(
table: Table<TData>,
rowModel: RowModel<TData>
): RowModel<TData> {
const rowSelection = table.getState().rowSelection
const newSelectedFlatRows: Row<TData>[] = []
const newSelectedRowsById: Record<string, Row<TData>> = {}
// Filters top level and nested rows
const recurseRows = (rows: Row<TData>[], depth = 0): Row<TData>[] => {
return rows
.map(row => {
const isSelected = isRowSelected(row, rowSelection)
if (isSelected) {
newSelectedFlatRows.push(row)
newSelectedRowsById[row.id] = row
}
if (row.subRows?.length) {
row = {
...row,
subRows: recurseRows(row.subRows, depth + 1),
}
}
if (isSelected) {
return row
}
})
.filter(Boolean) as Row<TData>[]
}
return {
rows: recurseRows(rowModel.rows),
flatRows: newSelectedFlatRows,
rowsById: newSelectedRowsById,
}
}
export function isRowSelected<TData extends RowData>(
row: Row<TData>,
selection: Record<string, boolean>
): boolean {
return selection[row.id] ?? false
}
export function isSubRowSelected<TData extends RowData>(
row: Row<TData>,
selection: Record<string, boolean>,
table: Table<TData>
): boolean | 'some' | 'all' {
if (!row.subRows?.length) return false
let allChildrenSelected = true
let someSelected = false
row.subRows.forEach(subRow => {
// Bail out early if we know both of these
if (someSelected && !allChildrenSelected) {
return
}
if (subRow.getCanSelect()) {
if (isRowSelected(subRow, selection)) {
someSelected = true
} else {
allChildrenSelected = false
}
}
// Check row selection of nested subrows
if (subRow.subRows && subRow.subRows.length) {
const subRowChildrenSelected = isSubRowSelected(subRow, selection, table)
if (subRowChildrenSelected === 'all') {
someSelected = true
} else if (subRowChildrenSelected === 'some') {
someSelected = true
allChildrenSelected = false
} else {
allChildrenSelected = false
}
}
})
return allChildrenSelected ? 'all' : someSelected ? 'some' : false
}