v7.0.0-beta.20

This commit is contained in:
Tanner Linsley 2019-12-02 01:28:28 -07:00
parent dac4744727
commit 127a7fca87
27 changed files with 905 additions and 664 deletions

View File

@ -1,20 +1,20 @@
{
"dist/index.js": {
"bundled": 90287,
"minified": 43056,
"gzipped": 11505
"bundled": 96409,
"minified": 46532,
"gzipped": 12274
},
"dist/index.es.js": {
"bundled": 89704,
"minified": 42548,
"gzipped": 11385,
"bundled": 95844,
"minified": 46040,
"gzipped": 12164,
"treeshaked": {
"rollup": {
"code": 450,
"code": 78,
"import_statements": 21
},
"webpack": {
"code": 3712
"code": 11105
}
}
}

View File

@ -1,3 +1,13 @@
## 7.0.0-beta.20
- Internals have been reworked to use `useReducer` instead of `useState` for stability and architecture
- The `state` option has been removed in favor of using a custom reducer
- The `reducer` option has been changed to a new function signature: `function (newState, action, oldState) => newState`
- The `setState` table instance method is no longer supported
- The `dispatch` table instanced method was added
- The `ReactTable.actions` export is now a plain object of action types mapped to identically named action strings
- The `ReactTable.reducerHandlers` export was added, which is a plain object of plugin hook names mapped to their respective reducer functions
## 7.0.0-beta.19
- Added an `isAggregated` boolean parameter to the `aggregate` function signature

View File

@ -87,16 +87,11 @@ The following options are supported via the main options object passed to `useTa
- Optional
- The initial state object for the table.
- Upon table initialization, this object is **merged over the table's `defaultState` object** (eg. `{...defaultState, ...initialState}`) that React Table and its hooks use to register default state to produce the final initial state object passed to the `React.useState` hook internally.
- `state: Object`
- `reducer: Function(newState, action, prevState) => newState`
- Optional
- Must be **memoized**
- When either the internal `state` or this `state` object change, this object is **always merged over the internal table state** (eg. `{...state, ...overrides}`) to produce the final state object that is then passed to the `useTable` options.
- `reducer: Function(oldState, newState) => finalState`
- Optional
- Must be **memoized**
- Inspired by Kent C. Dodd's [State Reducer Pattern](https://kentcdodds.com/blog/the-state-reducer-pattern-with-react-hooks)
- With every `setState` call to the table's internal `React.useState` instance, this reducer is called and is allowed to modify the final state object for updating.
- It is passed the `oldState`, the `newState`, and when provided, an optional action `type`.
- With every action that is dispatched to the table's internal `React.useReducer` instance, this reducer is called and is allowed to modify the final state object for updating.
- It is passed the `newState`, `action`, and `prevState` and is expected to either return the `newState` or a modified version of the `newState`
- May also be used to "control" the state of the table, by overriding certain pieces of state regardless of the action.
- `defaultColumn: Object`
- Optional
- Defaults to `{}`
@ -114,7 +109,7 @@ The following options are supported via the main options object passed to `useTa
- Defaults to `(row) => row.subRows || []`
- Use this function to change how React Table detects subrows. You could even use this function to generate sub rows if you want.
- By default, it will attempt to return the `subRows` property on the row, or an empty array if that is not found.
- `getRowID: Function(row, relativeIndex) => string`
- `getRowId: Function(row, relativeIndex) => string`
- Use this function to change how React Table detects unique rows and also how it constructs each row's underlying `path` property.
- Optional
- Must be **memoized**
@ -182,18 +177,14 @@ The following options are supported on any column object you can pass to `column
The following properties are available on the table instance returned from `useTable`
- `state: Object`
- **Memoized** - This object reference will not change unless either the internal state or the `state` overrides option provided change.
- This is the final state object of the table, which is the product of the `initialState`, internal state, optional `state` overrides option and the `reducer` option (if applicable).
- `setState: Function(updater, type) => void`
- **Memoized** - This function reference will not change unless the internal state `reducer` is changed
- **Memoized** - This object reference will not change unless the internal table state is modified.
- This is the final state object of the table, which is the product of the `initialState`, internal table reducer and (optionally) a custom `reducer` supplied by the user.
- `dispatch: Function({ type: Actions[type], ...payload }) => void`
- This function is used both internally by React Table, and optionally by you (the developer) to update the table state programmatically.
- `updater: Function`
- This parameter is identical to the `setState` API exposed by `React.useState`.
- If a function is passed, that function will be called with the previous state and is expected to return a new version of the state.
- If a value is passed, it will replace the state entirely.
- `type: String`
- Optional
- `type: Actions[type] | String`
- The action type corresponding to what action being taken against the state.
- `...payload`
- Any other action data that is associated with the action
- `columns: Array<Column>`
- A **nested** array of final column objects, **similar in structure to the original columns configuration option**.
- See [Column Properties](#column-properties) for more information
@ -237,9 +228,9 @@ The following properties are available on the table instance returned from `useT
- This function can be used to update the internal state for any row.
- Pass it a valid `rowPath` array and `updater`. The `updater` may be a value or function, similar to `React.useState`'s usage.
- If `updater` is a function, it will be passed the previous value
- `setCellState: Function(rowPath, columnID, updater: Function | any) => void`
- `setCellState: Function(rowPath, columnId, updater: Function | any) => void`
- This function can be used to update the internal state for any cell.
- Pass it a valid `rowPath` array, `columnID` and `updater`. The `updater` may be a value or function, similar to `React.useState`'s usage.
- Pass it a valid `rowPath` array, `columnId` and `updater`. The `updater` may be a value or function, similar to `React.useState`'s usage.
- If `updater` is a function, it will be passed the previous value
### HeaderGroup Properties
@ -287,8 +278,8 @@ The following additional properties are available on every `row` object returned
- `cells: Array<Cell>`
- An array of `Cell` objects containing properties and functions specific to the row and column it belongs to.
- See [Cell Properties](#cell-properties) for more information
- `values: Object<columnID: any>`
- A map of this row's **resolved** values by columnID, eg. `{ firstName: 'Tanner', lastName: 'Linsley' }`
- `values: Object<columnId: any>`
- A map of this row's **resolved** values by columnId, eg. `{ firstName: 'Tanner', lastName: 'Linsley' }`
- `getRowProps: Function(?props)`
- **Required**
- This function is used to resolve any props needed for this row.
@ -352,7 +343,7 @@ The following additional properties are available on every `Cell` object returne
The following options are supported via the main options object passed to `useTable(options)`
- `state.sortBy: Array<Object<id: columnID, desc: Bool>>`
- `state.sortBy: Array<Object<id: columnId, desc: Bool>>`
- Must be **memoized**
- An array of sorting objects. If there is more than one object in the array, multi-sorting will be enabled. Each sorting object should contain an `id` key with the corresponding column ID to sort by. An optional `desc` key may be set to true or false to indicated ascending or descending sorting for that column. This information is stored in state since the table is allowed to manipulate the filter through user interaction.
- `initialState.sortBy`
@ -428,7 +419,7 @@ The following values are provided to the table `instance`:
- An array of **sorted** rows.
- `preSortedRows: Array<Row>`
- The array of rows that were originally sorted.
- `toggleSortBy: Function(ColumnID: String, descending: Bool, isMulti: Bool) => void`
- `toggleSortBy: Function(ColumnId: String, descending: Bool, isMulti: Bool) => void`
- This function can be used to programmatically toggle the sorting for any specific column
### Column Properties
@ -439,7 +430,7 @@ The following properties are available on every `Column` object returned by the
- Denotes whether a column is sortable or not depending on if it has a valid accessor/data model or is manually disabled via an option.
- `toggleSortBy: Function(descending, multi) => void`
- This function can be used to programmatically toggle the sorting for this column.
- This function is similar to the `instance`-level `toggleSortBy`, however, passing a columnID is not required since it is located on a `Column` object already.
- This function is similar to the `instance`-level `toggleSortBy`, however, passing a columnId is not required since it is located on a `Column` object already.
- `getSortByToggleProps: Function(props) => props`
- **Required**
- This function is used to resolve any props needed for this column's UI that is responsible for toggling the sort direction when the user clicks it.
@ -474,9 +465,9 @@ The following properties are available on every `Column` object returned by the
The following options are supported via the main options object passed to `useTable(options)`
- `state.filters: Object<columnID: filterValue>`
- `state.filters: Object<columnId: filterValue>`
- Must be **memoized**
- An object of columnID's and their corresponding filter values. This information is stored in state since the table is allowed to manipulate the filter through user interaction.
- An object of columnId's and their corresponding filter values. This information is stored in state since the table is allowed to manipulate the filter through user interaction.
- `initialState.filters`
- Identical to the `state.filters` option above
- `manualFilters: Bool`
@ -532,7 +523,7 @@ The following values are provided to the table `instance`:
- `preFilteredRows: Array<Row>`
- The array of rows **used right before filtering**.
- Among many other use-cases, these rows are directly useful for building option lists in filters, since the resulting filtered `rows` do not contain every possible option.
- `setFilter: Function(columnID, filterValue) => void`
- `setFilter: Function(columnId, filterValue) => void`
- An instance-level function used to update the filter value for a specific column.
- `setAllFilters: Function(filtersObject) => void`
- An instance-level function used to update the values for **all** filters on the table, all at once.
@ -619,7 +610,7 @@ The following values are provided to the table `instance`:
- An array of **grouped and aggregated** rows.
- `preGroupedRows: Array<Row>`
- The array of rows originally used to create the grouped rows.
- `toggleGroupBy: Function(columnID: String, ?set: Bool) => void`
- `toggleGroupBy: Function(columnId: String, ?set: Bool) => void`
- This function can be used to programmatically set or toggle the groupBy state for a specific column.
### Column Properties
@ -645,7 +636,7 @@ The following properties are available on every `Column` object returned by the
The following properties are available on every `Row` object returned by the table instance.
- `groupByID: String`
- `groupById: String`
- The column ID for which this row is being grouped.
- Will be `undefined` if the row is an original row from `data` and not a materialized one from the grouping.
- `groupByVal: any`
@ -924,7 +915,7 @@ The following additional properties are available on every **prepared** `row` ob
The following options are supported via the main options object passed to `useTable(options)`
- `state.rowState: Object<RowPathKey:Object<any, cellState: {columnID: Object}>>`
- `state.rowState: Object<RowPathKey:Object<any, cellState: {columnId: Object}>>`
- Optional
- Defaults to `{}`
- If a row's path key (eg. a row path of `[1, 3, 2]` would have a path key of `1.3.2`) is found in this array, it will have the state of the value corresponding to that key.
@ -936,6 +927,14 @@ The following options are supported via the main options object passed to `useTa
- Optional
- This function may optionally return the initial state for a row.
- If this function is defined, it will be passed a `Row` object, from which you can return a value to use as the initial state, eg. `row => row.original.initialState`
- `getResetRowStateDeps: Function(instance) => [...useEffectDependencies]`
- Optional
- Defaults to resetting the `rowState` state to `{}` when the dependencies below change
- ```js
const getResetRowStateDeps = ({ data }) => [data]
```
- If set, the dependencies returned from this function will be used to determine when the effect to reset the `rowState` state is fired.
- To disable, set to `false`
### Instance Properties
@ -944,7 +943,7 @@ The following values are provided to the table `instance`:
- `setRowState: Function(rowPath: Array<string>, updater: Function | Any) => void`
- Use this function to programmatically update the state of a row.
- `updater` can be a function or value. If a `function` is passed, it will receive the current value and expect a new one to be returned.
- `setCellState: Function(rowPath: Array<string>, columnID: String, updater: Function | Any) => void`
- `setCellState: Function(rowPath: Array<string>, columnId: String, updater: Function | Any) => void`
- Use this function to programmatically update the cell of a row.
- `updater` can be a function or value. If a `function` is passed, it will receive the current value and expect a new one to be returned.
@ -964,7 +963,7 @@ The following additional properties are available on every **prepared** `row` ob
The following additional properties are available on every `Cell` object returned in an array of `cells` on every row object.
- `state: Object`
- This is the state object for each cell, pre-mapped to the cell from the table state's `rowState` object via `rowState[row.path.join('.')].cellState[columnID]`
- This is the state object for each cell, pre-mapped to the cell from the table state's `rowState` object via `rowState[row.path.join('.')].cellState[columnId]`
- `setState: Function(updater: Function | any)`
- Use this function to programmatically update the state of a cell.
- `updater` can be a function or value. If a `function` is passed, it will receive the current value and expect a new one to be returned.
@ -1086,7 +1085,7 @@ The core column options `width`, `minWidth` and `maxWidth` are used to calculate
The following options are supported via the main options object passed to `useTable(options)`
- `state.columnOrder: Array<ColumnID>`
- `state.columnOrder: Array<ColumnId>`
- Optional
- Defaults to `[]`
- Any column ID's not represented in this array will be naturally ordered based on their position in the original table's `column` structure
@ -1097,7 +1096,7 @@ The following options are supported via the main options object passed to `useTa
The following values are provided to the table `instance`:
- `setColumnOrder: Function(updater: Function | Array<ColumnID>) => void`
- `setColumnOrder: Function(updater: Function | Array<ColumnId>) => void`
- Use this function to programmatically update the columnOrder.
- `updater` can be a function or value. If a `function` is passed, it will receive the current value and expect a new one to be returned.

View File

@ -236,9 +236,9 @@ function App() {
// Update data. So we can keep track of that flag with a ref.
// When our cell renderer calls updateMyData, we'll use
// the rowIndex, columnID and new value to update the
// the rowIndex, columnId and new value to update the
// original data
const updateMyData = (rowIndex, columnID, value) => {
const updateMyData = (rowIndex, columnId, value) => {
// We also turn on the flag to not reset the page
setSkipPageReset(true)
setData(old =>
@ -246,7 +246,7 @@ function App() {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnID]: value,
[columnId]: value,
}
}
return row

View File

@ -582,9 +582,9 @@ function App() {
const skipPageResetRef = React.useRef(false)
// When our cell renderer calls updateMyData, we'll use
// the rowIndex, columnID and new value to update the
// the rowIndex, columnId and new value to update the
// original data
const updateMyData = (rowIndex, columnID, value) => {
const updateMyData = (rowIndex, columnId, value) => {
// We also turn on the flag to not reset the page
skipPageResetRef.current = true
setData(old =>
@ -592,7 +592,7 @@ function App() {
if (index === rowIndex) {
return {
...row,
[columnID]: value,
[columnId]: value,
}
}
return row

View File

@ -581,9 +581,9 @@ function App() {
const skipResetRef = React.useRef(false)
// When our cell renderer calls updateMyData, we'll use
// the rowIndex, columnID and new value to update the
// the rowIndex, columnId and new value to update the
// original data
const updateMyData = (rowIndex, columnID, value) => {
const updateMyData = (rowIndex, columnId, value) => {
// We also turn on the flag to not reset the page
skipResetRef.current = true
setData(old =>
@ -591,7 +591,7 @@ function App() {
if (index === rowIndex) {
return {
...row,
[columnID]: value,
[columnId]: value,
}
}
return row

View File

@ -918,7 +918,7 @@
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.5.5":
version "7.7.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.4.tgz#b23a856751e4bf099262f867767889c0e3fe175b"
integrity sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw==
@ -8218,10 +8218,10 @@ react-scripts@3.0.1:
optionalDependencies:
fsevents "2.0.6"
react-table@^7.0.0-beta.15:
version "7.0.0-beta.15"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-beta.15.tgz#66dc4c71f9e77f5c92ac65a307341d6d3e045d33"
integrity sha512-Qr0HjVkYIPLi7BJiHm6ZfRIH00Ziu/bdonwouwKY10hsaqQi9AyH9kF4cXJChBQuspfjsnqnP9kQAjsEIoAeUQ==
react-table@next:
version "7.0.0-beta.19"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-beta.19.tgz#8563ae56f693cbffa10060772ba1f1cd4e926bb2"
integrity sha512-BRt7zW7PGyg67fR2CK9M2fwP6BocjQN870w3P+K+vDJMkpewGjSPDscCU5sJMBqCEjKWg85k2pVkiwQPg9ofgQ==
react@^16.8.6:
version "16.12.0"
@ -9093,13 +9093,6 @@ stealthy-require@^1.1.1:
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
stop-runaway-react-effects@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/stop-runaway-react-effects/-/stop-runaway-react-effects-1.2.1.tgz#ffc9df565021c69cd2c04171e7f0e4a6c7eb2b21"
integrity sha512-56AK/rkH+/Y1ZUF+QYsl/7Z/caSnF46RmkbF6AemYWue2tZpAlOOj+VdcLdhFGo5Vg7ajDP2Lqq+3UhbdWQRmQ==
dependencies:
"@babel/runtime" "^7.4.4"
stream-browserify@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"

View File

@ -61,7 +61,6 @@ function Table({
nextPage,
previousPage,
setPageSize,
rows,
// Get the state from the instance
state: { pageIndex, pageSize },
} = useTable(
@ -245,7 +244,7 @@ function App() {
// even a server. But for this example, we'll just fake it.
// Give this fetch an ID
const fetchID = ++fetchIdRef.current
const fetchId = ++fetchIdRef.current
// Set the loading state
setLoading(true)
@ -253,7 +252,7 @@ function App() {
// We'll even set a delay to simulate a server here
setTimeout(() => {
// Only update the data if this is the latest fetch
if (fetchID === fetchIdRef.current) {
if (fetchId === fetchIdRef.current) {
const startRow = pageSize * pageIndex
const endRow = startRow + pageSize
setData(serverData.slice(startRow, endRow))

View File

@ -62,6 +62,7 @@ function Table({ columns, data }) {
columns,
data,
initialState: { pageIndex: 2 },
debug: true,
},
usePagination
)
@ -95,19 +96,16 @@ function Table({ columns, data }) {
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map(
(row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
)
})}
</tr>
)}
)}
{page.map((row, i) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
{/*

View File

@ -39,7 +39,7 @@ const Styles = styled.div`
const Table = ({ columns, data }) => {
const [records, setRecords] = React.useState(data)
const getRowID = React.useCallback(row => {
const getRowId = React.useCallback(row => {
return row.id
}, [])
@ -52,7 +52,7 @@ const Table = ({ columns, data }) => {
} = useTable({
data: records,
columns,
getRowID,
getRowId,
})
const moveRow = (dragIndex, hoverIndex) => {

View File

@ -47,6 +47,7 @@ function Table({ columns, data }) {
{
columns,
data,
debug: true,
},
useRowSelect
)
@ -65,19 +66,16 @@ function Table({ columns, data }) {
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(
(row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
)
})}
</tr>
)}
)}
{rows.map((row, i) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
<p>Selected Rows: {selectedRowPaths.length}</p>

2
index.d.ts vendored
View File

@ -484,7 +484,7 @@ export interface UseRowStateInstanceProps<D extends object> {
setRowState: (rowPath: string[], updater: UseRowUpdater) => void // Purposely not exposing action
setCellState: (
rowPath: string[],
columnID: IdType<D>,
columnId: IdType<D>,
updater: UseRowUpdater
) => void
}

View File

@ -1,6 +1,6 @@
{
"name": "react-table",
"version": "7.0.0-beta.19",
"version": "7.0.0-beta.20",
"description": "A fast, lightweight, opinionated table and datagrid built on React",
"license": "MIT",
"homepage": "https://github.com/tannerlinsley/react-table#readme",

View File

@ -1,15 +0,0 @@
const actions = {}
const types = {}
export { actions, types }
export const addActions = (...acts) => {
acts.forEach(action => {
// Action values are formatted this way to discourage
// you (the dev) from interacting with them in any way
// other than importing `{ actions } from 'react-table'`
// and referencing an action via `actions[actionName]`
actions[action] = `React Table Action: ${action}`
types[actions[action]] = true
})
}

View File

@ -15,13 +15,17 @@ import {
const renderErr =
'You must specify a valid render component. This could be "column.Cell", "column.Header", "column.Filter", "column.Aggregated" or any other custom renderer component.'
export const actions = {
init: 'init',
}
export const defaultState = {}
export const reducerHandlers = {}
const defaultInitialState = {}
const defaultColumnInstance = {}
const defaultReducer = (old, newState) => newState
const defaultReducer = (state, action, prevState) => state
const defaultGetSubRows = (row, index) => row.subRows || []
const defaultGetRowID = (row, index) => index
const defaultGetRowId = (row, index) => index
export const useTable = (props, ...plugins) => {
// Destructure props
@ -29,53 +33,54 @@ export const useTable = (props, ...plugins) => {
data,
columns: userColumns,
initialState = defaultInitialState,
state: userState,
defaultColumn = defaultColumnInstance,
getSubRows = defaultGetSubRows,
getRowID = defaultGetRowID,
reducer = defaultReducer,
getRowId = defaultGetRowId,
reducer: userReducer = defaultReducer,
debug,
} = props
debug = process.env.NODE_ENV === 'production' ? false : debug
// But use the users table state if provided
let [originalState, originalSetState] = React.useState({
...defaultState,
...initialState,
})
const reducer = (state, action) => {
let nextState = Object.keys(reducerHandlers)
.map(key => reducerHandlers[key])
.reduce((state, handler) => handler(state, action) || state, state)
const state = React.useMemo(() => {
if (userState) {
const newState = {
...originalState,
}
Object.keys(userState).forEach(key => {
newState[key] = userState[key]
})
return newState
nextState = userReducer(nextState, action, state)
if (process.env.NODE_ENV !== 'production' && debug) {
console.log('')
console.log('React Table Action: ', action)
console.log('New State: ', nextState)
}
return originalState
}, [originalState, userState])
return nextState
}
const setState = React.useCallback(
(updater, type) => {
return originalSetState(old => {
const newState = typeof updater === 'function' ? updater(old) : updater
return reducer(old, newState, type)
})
},
[reducer]
// But use the users table state if provided
const [state, originalDispatch] = React.useReducer(reducer, undefined, () =>
reducer(initialState, { type: actions.init })
)
// The table instance ref
let instanceRef = React.useRef({})
const dispatch = React.useCallback(action => {
if (!action.type) {
if (process.env.NODE_ENV !== 'production') {
console.info({ action })
throw new Error('Unknown Action Type! 👆')
}
throw new Error()
}
originalDispatch({ ...action, instanceRef })
}, [])
Object.assign(instanceRef.current, {
...props,
data, // The raw data
state,
setState, // The resolved table state
state, // The state dispatcher
dispatch, // The resolved table state
plugins, // All resolved plugins
hooks: {
columnsBeforeHeaderGroups: [],
@ -94,14 +99,13 @@ export const useTable = (props, ...plugins) => {
})
// Allow plugins to register hooks
if (process.env.NODE_ENV === 'development' && debug) console.time('plugins')
if (process.env.NODE_ENV !== 'production' && debug) console.time('plugins')
plugins.filter(Boolean).forEach(plugin => {
plugin(instanceRef.current.hooks)
})
if (process.env.NODE_ENV === 'development' && debug)
console.timeEnd('plugins')
if (process.env.NODE_ENV !== 'production' && debug) console.timeEnd('plugins')
// Decorate All the columns
let columns = React.useMemo(
@ -112,7 +116,7 @@ export const useTable = (props, ...plugins) => {
// Get the flat list of all columns and allow hooks to decorate
// those columns (and trigger this memoization via deps)
let flatColumns = React.useMemo(() => {
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.time('hooks.columnsBeforeHeaderGroups')
let newColumns = applyHooks(
@ -121,7 +125,7 @@ export const useTable = (props, ...plugins) => {
instanceRef.current
)
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.timeEnd('hooks.columnsBeforeHeaderGroups')
return newColumns
}, [
@ -152,7 +156,7 @@ export const useTable = (props, ...plugins) => {
// Access the row model
const [rows, flatRows] = React.useMemo(() => {
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.time('getAccessedRows')
let flatRows = []
@ -162,10 +166,10 @@ export const useTable = (props, ...plugins) => {
// Keep the original reference around
const original = originalRow
const rowID = getRowID(originalRow, i)
const rowId = getRowId(originalRow, i)
// Make the new path for the row
const path = [...parentPath, rowID]
const path = [...parentPath, rowId]
const row = {
original,
@ -209,10 +213,10 @@ export const useTable = (props, ...plugins) => {
// Use the resolved data
const accessedData = data.map((d, i) => accessRow(d, i))
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.timeEnd('getAccessedRows')
return [accessedData, flatRows]
}, [debug, data, getRowID, getSubRows, flatColumns])
}, [debug, data, getRowId, getSubRows, flatColumns])
instanceRef.current.rows = rows
instanceRef.current.flatRows = flatRows
@ -226,24 +230,24 @@ export const useTable = (props, ...plugins) => {
[]
)
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.time('hooks.useBeforeDimensions')
instanceRef.current = applyHooks(
instanceRef.current.hooks.useBeforeDimensions,
instanceRef.current
)
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.timeEnd('hooks.useBeforeDimensions')
calculateDimensions(instanceRef.current)
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.time('hooks.useMain')
instanceRef.current = applyHooks(
instanceRef.current.hooks.useMain,
instanceRef.current
)
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.timeEnd('hooks.useMain')
// Each materialized header needs to be assigned a render function and other
@ -316,14 +320,14 @@ export const useTable = (props, ...plugins) => {
})
// Run the rows (this could be a dangerous hook with a ton of data)
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.time('hooks.useRows')
instanceRef.current.rows = applyHooks(
instanceRef.current.hooks.useRows,
instanceRef.current.rows,
instanceRef.current
)
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.timeEnd('hooks.useRows')
// The prepareRow function is absolutely necessary and MUST be called on

View File

@ -1,7 +1,7 @@
import * as utils from './utils'
export { utils }
export { defaultColumn } from './utils'
export { useTable, defaultState } from './hooks/useTable'
export { useTable, actions, reducerHandlers } from './hooks/useTable'
export { useExpanded } from './plugin-hooks/useExpanded'
export { useFilters } from './plugin-hooks/useFilters'
export { useGroupBy } from './plugin-hooks/useGroupBy'
@ -13,4 +13,3 @@ export { useColumnOrder } from './plugin-hooks/useColumnOrder'
export { useResizeColumns } from './plugin-hooks/useResizeColumns'
export { useAbsoluteLayout } from './plugin-hooks/useAbsoluteLayout'
export { useBlockLayout } from './plugin-hooks/useBlockLayout'
export { actions, addActions } from './actions'

View File

@ -1,11 +1,37 @@
import React from 'react'
import { addActions, actions } from '../actions'
import { defaultState } from '../hooks/useTable'
import { actions, reducerHandlers } from '../hooks/useTable'
import { functionalUpdate } from '../utils'
defaultState.columnOrder = []
const pluginName = 'useColumnOrder'
addActions('setColumnOrder')
// Actions
actions.resetColumnOrder = 'resetColumnOrder'
actions.setColumnOrder = 'setColumnOrder'
// Reducer
reducerHandlers[pluginName] = (state, action) => {
if (action.type === actions.init) {
return {
columnOrder: [],
...state,
}
}
if (action.type === actions.resetColumnOrder) {
return {
...state,
columnOrder: [],
}
}
if (action.type === actions.setColumnOrder) {
return {
...state,
columnOrder: functionalUpdate(action.columnOrder, state.columnOrder),
}
}
}
export const useColumnOrder = hooks => {
hooks.columnsBeforeHeaderGroupsDeps.push((deps, instance) => {
@ -15,7 +41,7 @@ export const useColumnOrder = hooks => {
hooks.useMain.push(useMain)
}
useColumnOrder.pluginName = 'useColumnOrder'
useColumnOrder.pluginName = pluginName
function columnsBeforeHeaderGroups(columns, instance) {
const {
@ -37,8 +63,8 @@ function columnsBeforeHeaderGroups(columns, instance) {
// Loop over the columns and place them in order into the new array
while (columnsCopy.length && columnOrderCopy.length) {
const targetColumnID = columnOrderCopy.shift()
const foundIndex = columnsCopy.findIndex(d => d.id === targetColumnID)
const targetColumnId = columnOrderCopy.shift()
const foundIndex = columnsCopy.findIndex(d => d.id === targetColumnId)
if (foundIndex > -1) {
columnsInOrder.push(columnsCopy.splice(foundIndex, 1)[0])
}
@ -49,19 +75,13 @@ function columnsBeforeHeaderGroups(columns, instance) {
}
function useMain(instance) {
const { setState } = instance
const { dispatch } = instance
const setColumnOrder = React.useCallback(
updater => {
return setState(old => {
return {
...old,
columnOrder:
typeof updater === 'function' ? updater(old.columnOrder) : updater,
}
}, actions.setColumnOrder)
columnOrder => {
return dispatch({ type: actions.setColumnOrder, columnOrder })
},
[setState]
[dispatch]
)
return {

View File

@ -6,19 +6,58 @@ import {
expandRows,
safeUseLayoutEffect,
} from '../utils'
import { addActions, actions } from '../actions'
import { defaultState } from '../hooks/useTable'
import { actions, reducerHandlers } from '../hooks/useTable'
defaultState.expanded = []
const pluginName = 'useExpanded'
addActions('toggleExpanded', 'setExpanded')
// Actions
actions.toggleExpandedByPath = 'toggleExpandedByPath'
actions.resetExpanded = 'resetExpanded'
// Reducer
reducerHandlers[pluginName] = (state, action) => {
if (action.type === actions.init) {
return {
expanded: [],
...state,
}
}
if (action.type === actions.resetExpanded) {
return {
...state,
expanded: [],
}
}
if (action.type === actions.toggleExpandedByPath) {
const { path, expanded } = action
const key = path.join('.')
const exists = state.expanded.includes(key)
const shouldExist = typeof set !== 'undefined' ? expanded : !exists
let newExpanded = new Set(state.expanded)
if (!exists && shouldExist) {
newExpanded.add(key)
} else if (exists && !shouldExist) {
newExpanded.delete(key)
} else {
return state
}
return {
...state,
expanded: [...newExpanded.values()],
}
}
}
export const useExpanded = hooks => {
hooks.getExpandedToggleProps = []
hooks.useMain.push(useMain)
}
useExpanded.pluginName = 'useExpanded'
useExpanded.pluginName = pluginName
const defaultGetResetExpandedDeps = ({ data }) => [data]
@ -31,7 +70,7 @@ function useMain(instance) {
expandSubRows = true,
hooks,
state: { expanded },
setState,
dispatch,
getResetExpandedDeps = defaultGetResetExpandedDeps,
} = instance
@ -39,41 +78,16 @@ function useMain(instance) {
const isMountedRef = React.useRef()
safeUseLayoutEffect(() => {
if (isMountedRef.current) {
setState(
old => ({
...old,
expanded: [],
}),
actions.setExpanded
)
dispatch({ type: actions.resetExpanded })
}
isMountedRef.current = true
}, [
setState,
dispatch,
...(getResetExpandedDeps ? getResetExpandedDeps(instance) : []),
])
const toggleExpandedByPath = (path, set) => {
const key = path.join('.')
return setState(old => {
const exists = old.expanded.includes(key)
const shouldExist = typeof set !== 'undefined' ? set : !exists
let newExpanded = new Set(old.expanded)
if (!exists && shouldExist) {
newExpanded.add(key)
} else if (exists && !shouldExist) {
newExpanded.delete(key)
} else {
return old
}
return {
...old,
expanded: [...newExpanded.values()],
}
}, actions.toggleExpanded)
const toggleExpandedByPath = (path, expanded) => {
dispatch({ type: actions.toggleExpandedByPath, path, expanded })
}
// use reference to avoid memory leak in #1608
@ -106,7 +120,7 @@ function useMain(instance) {
})
const expandedRows = React.useMemo(() => {
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.info('getExpandedRows')
if (paginateExpandedRows) {

View File

@ -1,19 +1,118 @@
import React from 'react'
import { getFirstDefined, isFunction, safeUseLayoutEffect } from '../utils'
import {
getFirstDefined,
isFunction,
safeUseLayoutEffect,
functionalUpdate,
} from '../utils'
import * as filterTypes from '../filterTypes'
import { addActions, actions } from '../actions'
import { defaultState } from '../hooks/useTable'
import { actions, reducerHandlers } from '../hooks/useTable'
defaultState.filters = {}
const pluginName = 'useFilters'
addActions('setFilter', 'setAllFilters')
// Actions
actions.resetFilters = 'resetFilters'
actions.setFilter = 'setFilter'
actions.setAllFilters = 'setAllFilters'
// Reducer
reducerHandlers[pluginName] = (state, action) => {
if (action.type === actions.init) {
return {
filters: {},
...state,
}
}
if (action.type === actions.resetFilters) {
return {
...state,
filters: {},
}
}
if (action.type === actions.setFilter) {
const {
columnId,
filterValue,
instanceRef: {
current: { flatColumns, userFilterTypes },
},
} = action
const column = flatColumns.find(d => d.id === columnId)
if (!column) {
throw new Error(
`React-Table: Could not find a column with id: ${columnId}`
)
}
const filterMethod = getFilterMethod(
column.filter,
userFilterTypes || {},
filterTypes
)
const newFilter = functionalUpdate(filterValue, state.filters[columnId])
//
if (shouldAutoRemove(filterMethod.autoRemove, newFilter)) {
const { [columnId]: remove, ...newFilters } = state.filters
return {
...state,
filters: newFilters,
}
}
return {
...state,
filters: {
...state.filters,
[columnId]: newFilter,
},
}
}
if (action.type === actions.setAllFilters) {
const {
filters,
instanceRef: {
current: { flatColumns, filterTypes: userFilterTypes },
},
} = action
const newFilters = functionalUpdate(filters, state.filters)
// Filter out undefined values
Object.keys(newFilters).forEach(id => {
const newFilter = newFilters[id]
const column = flatColumns.find(d => d.id === id)
const filterMethod = getFilterMethod(
column.filter,
userFilterTypes || {},
filterTypes
)
if (shouldAutoRemove(filterMethod.autoRemove, newFilter)) {
delete newFilters[id]
}
})
return {
...state,
filters: newFilters,
}
}
}
export const useFilters = hooks => {
hooks.useMain.push(useMain)
}
useFilters.pluginName = 'useFilters'
useFilters.pluginName = pluginName
function useMain(instance) {
const {
@ -26,7 +125,7 @@ function useMain(instance) {
defaultCanFilter = false,
disableFilters,
state: { filters },
setState,
dispatch,
getResetFiltersDeps = false,
} = instance
@ -37,77 +136,20 @@ function useMain(instance) {
const isMountedRef = React.useRef()
safeUseLayoutEffect(() => {
if (isMountedRef.current) {
setState(
old => ({
...old,
filters: {},
}),
actions.setAllFilters
)
dispatch({ type: actions.resetFilters })
}
isMountedRef.current = true
}, [setState, ...(getResetFiltersDeps ? getResetFiltersDeps(instance) : [])])
}, [dispatch, ...(getResetFiltersDeps ? getResetFiltersDeps(instance) : [])])
const setFilter = (id, updater) => {
const column = flatColumns.find(d => d.id === id)
if (!column) {
throw new Error(`React-Table: Could not find a column with id: ${id}`)
}
const filterMethod = getFilterMethod(
column.filter,
userFilterTypes || {},
filterTypes
)
return setState(old => {
const newFilter =
typeof updater === 'function' ? updater(old.filters[id]) : updater
//
if (shouldAutoRemove(filterMethod.autoRemove, newFilter)) {
const { [id]: remove, ...newFilters } = old.filters
return {
...old,
filters: newFilters,
}
}
return {
...old,
filters: {
...old.filters,
[id]: newFilter,
},
}
}, actions.setFilter)
const setFilter = (columnId, filterValue) => {
dispatch({ type: actions.setFilter, columnId, filterValue })
}
const setAllFilters = updater => {
return setState(old => {
const newFilters = typeof updater === 'function' ? updater(old) : updater
// Filter out undefined values
Object.keys(newFilters).forEach(id => {
const newFilter = newFilters[id]
const column = flatColumns.find(d => d.id === id)
const filterMethod = getFilterMethod(
column.filter,
userFilterTypes || {},
filterTypes
)
if (shouldAutoRemove(filterMethod.autoRemove, newFilter)) {
delete newFilters[id]
}
})
return {
...old,
filters: newFilters,
}
}, actions.setAllFilters)
const setAllFilters = filters => {
dispatch({
type: actions.setAllFilters,
filters,
})
}
flatColumns.forEach(column => {
@ -150,7 +192,7 @@ function useMain(instance) {
const filteredFlatRows = []
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.info('getFilteredRows')
// Filters top level and nested rows
@ -158,9 +200,9 @@ function useMain(instance) {
let filteredRows = rows
filteredRows = Object.entries(filters).reduce(
(filteredSoFar, [columnID, filterValue]) => {
(filteredSoFar, [columnId, filterValue]) => {
// Find the filters column
const column = flatColumns.find(d => d.id === columnID)
const column = flatColumns.find(d => d.id === columnId)
if (!column) {
return filteredSoFar
@ -187,7 +229,7 @@ function useMain(instance) {
// to get the filtered rows back
column.filteredRows = filterMethod(
filteredSoFar,
columnID,
columnId,
filterValue,
column
)

View File

@ -1,8 +1,7 @@
import React from 'react'
import * as aggregations from '../aggregations'
import { addActions, actions } from '../actions'
import { defaultState } from '../hooks/useTable'
import { actions, reducerHandlers } from '../hooks/useTable'
import {
mergeProps,
applyPropHooks,
@ -11,9 +10,47 @@ import {
ensurePluginOrder,
} from '../utils'
defaultState.groupBy = []
const pluginName = 'useGroupBy'
addActions('toggleGroupBy')
// Actions
actions.resetGroupBy = 'resetGroupBy'
actions.toggleGroupBy = 'toggleGroupBy'
// Reducer
reducerHandlers[pluginName] = (state, action) => {
if (action.type === actions.init) {
return {
groupBy: [],
...state,
}
}
if (action.type === actions.resetGroupBy) {
return {
...state,
groupBy: [],
}
}
if (action.type === actions.toggleGroupBy) {
const { columnId, toggle } = action
const resolvedToggle =
typeof toggle !== 'undefined' ? toggle : !state.groupBy.includes(columnId)
if (resolvedToggle) {
return {
...state,
groupBy: [...state.groupBy, columnId],
}
}
return {
...state,
groupBy: state.groupBy.filter(d => d !== columnId),
}
}
}
export const useGroupBy = hooks => {
hooks.columnsBeforeHeaderGroups.push(columnsBeforeHeaderGroups)
@ -24,7 +61,7 @@ export const useGroupBy = hooks => {
hooks.useMain.push(useMain)
}
useGroupBy.pluginName = 'useGroupBy'
useGroupBy.pluginName = pluginName
function columnsBeforeHeaderGroups(flatColumns, { state: { groupBy } }) {
// Sort grouped columns to the start of the column list
@ -61,7 +98,7 @@ function useMain(instance) {
hooks,
plugins,
state: { groupBy },
setState,
dispatch,
} = instance
ensurePluginOrder(plugins, [], 'useGroupBy', ['useSortBy', 'useExpanded'])
@ -91,21 +128,8 @@ function useMain(instance) {
column.Aggregated = column.Aggregated || column.Cell
})
const toggleGroupBy = (id, toggle) => {
return setState(old => {
const resolvedToggle =
typeof toggle !== 'undefined' ? toggle : !groupBy.includes(id)
if (resolvedToggle) {
return {
...old,
groupBy: [...groupBy, id],
}
}
return {
...old,
groupBy: groupBy.filter(d => d !== id),
}
}, actions.toggleGroupBy)
const toggleGroupBy = (columnId, toggle) => {
dispatch({ type: actions.toggleGroupBy, columnId, toggle })
}
hooks.getGroupByToggleProps = []
@ -158,7 +182,7 @@ function useMain(instance) {
return [rows, flatRows]
}
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.info('getGroupedRows')
// Find the columns that can or are aggregating
@ -223,15 +247,15 @@ function useMain(instance) {
return rows
}
const columnID = groupBy[depth]
const columnId = groupBy[depth]
// Group the rows together for this level
let groupedRows = groupByFn(rows, columnID)
let groupedRows = groupByFn(rows, columnId)
// Recurse to sub rows before aggregation
groupedRows = Object.entries(groupedRows).map(
([groupByVal, subRows], index) => {
const path = [...parentPath, `${columnID}:${groupByVal}`]
const path = [...parentPath, `${columnId}:${groupByVal}`]
subRows = groupRecursively(subRows, depth + 1, path)
@ -239,7 +263,7 @@ function useMain(instance) {
const row = {
isAggregated: true,
groupByID: columnID,
groupByID: columnId,
groupByVal,
values,
subRows,

View File

@ -1,20 +1,70 @@
import React from 'react'
//
import { addActions, actions } from '../actions'
import { defaultState } from '../hooks/useTable'
import { ensurePluginOrder, safeUseLayoutEffect, expandRows } from '../utils'
defaultState.pageSize = 10
defaultState.pageIndex = 0
import { actions, reducerHandlers } from '../hooks/useTable'
import {
ensurePluginOrder,
safeUseLayoutEffect,
expandRows,
functionalUpdate,
} from '../utils'
addActions('pageChange', 'pageSizeChange')
const pluginName = 'usePagination'
// Actions
actions.resetPage = 'resetPage'
actions.gotoPage = 'gotoPage'
actions.setPageSize = 'setPageSize'
// Reducer
reducerHandlers[pluginName] = (state, action) => {
if (action.type === actions.init) {
return {
pageSize: 10,
pageIndex: 0,
...state,
}
}
if (action.type === actions.resetPage) {
return {
...state,
pageIndex: 0,
}
}
if (action.type === actions.gotoPage) {
const { pageCount } = action.instanceRef.current
const newPageIndex = functionalUpdate(action.pageIndex, state.pageIndex)
if (newPageIndex < 0 || newPageIndex > pageCount - 1) {
return state
}
return {
...state,
pageIndex: newPageIndex,
}
}
if (action.type === actions.setPageSize) {
const { pageSize } = action
const topRowIndex = state.pageSize * state.pageIndex
const pageIndex = Math.floor(topRowIndex / pageSize)
return {
...state,
pageIndex,
pageSize,
}
}
}
export const usePagination = hooks => {
hooks.useMain.push(useMain)
}
usePagination.pluginName = 'usePagination'
usePagination.pluginName = pluginName
const defaultGetResetPageDeps = ({
data,
@ -34,7 +84,7 @@ function useMain(instance) {
paginateExpandedRows = true,
expandSubRows = true,
state: { pageSize, pageIndex, expanded },
setState,
dispatch,
} = instance
ensurePluginOrder(
@ -48,16 +98,10 @@ function useMain(instance) {
const isMountedRef = React.useRef()
safeUseLayoutEffect(() => {
if (isMountedRef.current) {
setState(
old => ({
...old,
pageIndex: 0,
}),
actions.pageChange
)
dispatch({ type: actions.resetPage })
}
isMountedRef.current = true
}, [setState, ...(getResetPageDeps ? getResetPageDeps(instance) : [])])
}, [dispatch, ...(getResetPageDeps ? getResetPageDeps(instance) : [])])
const pageCount = manualPagination
? userPageCount
@ -74,7 +118,7 @@ function useMain(instance) {
if (manualPagination) {
page = rows
} else {
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.info('getPage')
const pageStart = pageSize * pageIndex
@ -104,23 +148,10 @@ function useMain(instance) {
const canNextPage = pageCount === -1 || pageIndex < pageCount - 1
const gotoPage = React.useCallback(
updater => {
if (process.env.NODE_ENV === 'development' && debug)
console.info('gotoPage')
return setState(old => {
const newPageIndex =
typeof updater === 'function' ? updater(old.pageIndex) : updater
if (newPageIndex < 0 || newPageIndex > pageCount - 1) {
return old
}
return {
...old,
pageIndex: newPageIndex,
}
}, actions.pageChange)
pageIndex => {
dispatch({ type: actions.gotoPage, pageIndex })
},
[debug, pageCount, setState]
[dispatch]
)
const previousPage = React.useCallback(() => {
@ -133,17 +164,9 @@ function useMain(instance) {
const setPageSize = React.useCallback(
pageSize => {
setState(old => {
const topRowIndex = old.pageSize * old.pageIndex
const pageIndex = Math.floor(topRowIndex / pageSize)
return {
...old,
pageIndex,
pageSize,
}
}, actions.pageSizeChange)
dispatch({ type: actions.setPageSize, pageSize })
},
[setState]
[dispatch]
)
return {

View File

@ -1,20 +1,89 @@
import React from 'react'
import { defaultState } from '../hooks/useTable'
import { defaultColumn, getFirstDefined } from '../utils'
import { mergeProps, applyPropHooks } from '../utils'
import { actions, reducerHandlers } from '../hooks/useTable'
import {
defaultColumn,
getFirstDefined,
mergeProps,
applyPropHooks,
} from '../utils'
defaultState.columnResizing = {
columnWidths: {},
}
const pluginName = 'useResizeColumns'
// Default Column
defaultColumn.canResize = true
// Actions
actions.columnStartResizing = 'columnStartResizing'
actions.columnResizing = 'columnResizing'
actions.columnDoneResizing = 'columnDoneResizing'
// Reducer
reducerHandlers[pluginName] = (state, action) => {
if (action.type === actions.init) {
return {
columnResizing: {
columnWidths: {},
},
...state,
}
}
if (action.type === actions.columnStartResizing) {
const { startX, columnId, headerIdWidths } = action
return {
...state,
columnResizing: {
...state.columnResizing,
startX,
headerIdWidths,
isResizingColumn: columnId,
},
}
}
if (action.type === actions.columnResizing) {
const { clientX } = action
const { startX, headerIdWidths } = state.columnResizing
const deltaX = clientX - startX
const percentageDeltaX = deltaX / headerIdWidths.length
const newColumnWidths = {}
headerIdWidths.forEach(([headerId, headerWidth], index) => {
newColumnWidths[headerId] = Math.max(headerWidth + percentageDeltaX, 0)
})
return {
...state,
columnResizing: {
...state.columnResizing,
columnWidths: {
...state.columnResizing.columnWidths,
...action.columnWidths,
},
},
}
}
if (action.type === actions.columnDoneResizing) {
return {
...state,
columnResizing: {
...state.columnResizing,
startX: null,
isResizingColumn: null,
},
}
}
}
export const useResizeColumns = hooks => {
hooks.useBeforeDimensions.push(useBeforeDimensions)
}
useResizeColumns.pluginName = 'useResizeColumns'
useResizeColumns.pluginName = pluginName
const useBeforeDimensions = instance => {
instance.hooks.getResizerProps = []
@ -24,7 +93,7 @@ const useBeforeDimensions = instance => {
disableResizing,
hooks: { getHeaderProps },
state: { columnResizing },
setState,
dispatch,
} = instance
getHeaderProps.push(() => {
@ -37,60 +106,32 @@ const useBeforeDimensions = instance => {
const onMouseDown = (e, header) => {
const headersToResize = getLeafHeaders(header)
const startWidths = headersToResize.map(header => header.totalWidth)
const startX = e.clientX
const headerIdWidths = headersToResize.map(d => [d.id, d.totalWidth])
const clientX = e.clientX
const onMouseMove = e => {
const currentX = e.clientX
const deltaX = currentX - startX
const clientX = e.clientX
const percentageDeltaX = deltaX / headersToResize.length
const newColumnWidths = {}
headersToResize.forEach((header, index) => {
newColumnWidths[header.id] = Math.max(
startWidths[index] + percentageDeltaX,
0
)
})
setState(old => ({
...old,
columnResizing: {
...old.columnResizing,
columnWidths: {
...old.columnResizing.columnWidths,
...newColumnWidths,
},
},
}))
dispatch({ type: actions.columnResizing, clientX })
}
const onMouseUp = e => {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
setState(old => ({
...old,
columnResizing: {
...old.columnResizing,
startX: null,
isResizingColumn: null,
},
}))
dispatch({ type: actions.columnDoneResizing })
}
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
setState(old => ({
...old,
columnResizing: {
...old.columnResizing,
startX,
isResizingColumn: header.id,
},
}))
dispatch({
type: actions.columnStartResizing,
columnId: header.id,
headerIdWidths,
clientX,
})
}
// use reference to avoid memory leak in #1608

View File

@ -6,12 +6,113 @@ import {
ensurePluginOrder,
safeUseLayoutEffect,
} from '../utils'
import { addActions, actions } from '../actions'
import { defaultState } from '../hooks/useTable'
import { actions, reducerHandlers } from '../hooks/useTable'
defaultState.selectedRowPaths = []
const pluginName = 'useRowSelect'
addActions('toggleRowSelected', 'toggleRowSelectedAll', 'setSelectedRowPaths')
// Actions
actions.resetSelectedRows = 'resetSelectedRows'
actions.toggleRowSelectedAll = 'toggleRowSelectedAll'
actions.toggleRowSelected = 'toggleRowSelected'
// Reducer
reducerHandlers[pluginName] = (state, action) => {
if (action.type === actions.init) {
return {
selectedRowPaths: [],
...state,
}
}
if (action.type === actions.resetSelectedRows) {
return {
...state,
selectedRowPaths: [],
}
}
if (action.type === actions.toggleRowSelectedAll) {
const {
selected,
instanceRef: {
current: { isAllRowsSelected, flatRowPaths },
},
} = action
const selectAll =
typeof selected !== 'undefined' ? selected : !isAllRowsSelected
return {
...state,
selectedRowPaths: selectAll ? flatRowPaths : [],
}
}
if (action.type === actions.toggleRowSelected) {
const {
path,
selected,
instanceRef: {
current: { flatRowPaths },
},
} = action
const key = path.join('.')
const childRowPrefixKey = [key, '.'].join('')
// Join the paths of deep rows
// to make a key, then manage all of the keys
// in a flat object
const exists = state.selectedRowPaths.includes(key)
const shouldExist = typeof set !== 'undefined' ? selected : !exists
let newSelectedRows = new Set(state.selectedRowPaths)
if (!exists && shouldExist) {
flatRowPaths.forEach(rowPath => {
if (rowPath === key || rowPath.startsWith(childRowPrefixKey)) {
newSelectedRows.add(rowPath)
}
})
} else if (exists && !shouldExist) {
flatRowPaths.forEach(rowPath => {
if (rowPath === key || rowPath.startsWith(childRowPrefixKey)) {
newSelectedRows.delete(rowPath)
}
})
} else {
return state
}
const updateParentRow = (selectedRowPaths, path) => {
const parentPath = path.slice(0, path.length - 1)
const parentKey = parentPath.join('.')
const selected =
flatRowPaths.filter(rowPath => {
const path = rowPath
return (
path !== parentKey &&
path.startsWith(parentKey) &&
!selectedRowPaths.has(path)
)
}).length === 0
if (selected) {
selectedRowPaths.add(parentKey)
} else {
selectedRowPaths.delete(parentKey)
}
if (parentPath.length > 1) updateParentRow(selectedRowPaths, parentPath)
}
// If the row is a subRow update
// its parent row to reflect changes
if (path.length > 1) updateParentRow(newSelectedRows, path)
return {
...state,
selectedRowPaths: [...newSelectedRows.values()],
}
}
}
export const useRowSelect = hooks => {
hooks.getToggleRowSelectedProps = []
@ -20,7 +121,7 @@ export const useRowSelect = hooks => {
hooks.useMain.push(useMain)
}
useRowSelect.pluginName = 'useRowSelect'
useRowSelect.pluginName = pluginName
function useRows(rows, instance) {
const {
@ -54,7 +155,7 @@ function useMain(instance) {
flatRows,
getResetSelectedRowPathsDeps = defaultGetResetSelectedRowPathsDeps,
state: { selectedRowPaths },
setState,
dispatch,
} = instance
ensurePluginOrder(
@ -78,90 +179,21 @@ function useMain(instance) {
const isMountedRef = React.useRef()
safeUseLayoutEffect(() => {
if (isMountedRef.current) {
setState(
old => ({
...old,
selectedRowPaths: [],
}),
actions.setSelectedRowPaths
)
dispatch({ type: actions.resetSelectedRows })
}
isMountedRef.current = true
}, [
setState,
dispatch,
...(getResetSelectedRowPathsDeps
? getResetSelectedRowPathsDeps(instance)
: []),
])
const toggleRowSelectedAll = set => {
setState(old => {
const selectAll = typeof set !== 'undefined' ? set : !isAllRowsSelected
return {
...old,
selectedRowPaths: selectAll ? flatRowPaths : [],
}
}, actions.toggleRowSelectedAll)
}
const toggleRowSelectedAll = selected =>
dispatch({ type: actions.toggleRowSelectedAll, selected })
const updateParentRow = (selectedRowPaths, path) => {
const parentPath = path.slice(0, path.length - 1)
const parentKey = parentPath.join('.')
const selected =
flatRowPaths.filter(rowPath => {
const path = rowPath
return (
path !== parentKey &&
path.startsWith(parentKey) &&
!selectedRowPaths.has(path)
)
}).length === 0
if (selected) {
selectedRowPaths.add(parentKey)
} else {
selectedRowPaths.delete(parentKey)
}
if (parentPath.length > 1) updateParentRow(selectedRowPaths, parentPath)
}
const toggleRowSelected = (path, set) => {
const key = path.join('.')
const childRowPrefixKey = [key, '.'].join('')
return setState(old => {
// Join the paths of deep rows
// to make a key, then manage all of the keys
// in a flat object
const exists = old.selectedRowPaths.includes(key)
const shouldExist = typeof set !== 'undefined' ? set : !exists
let newSelectedRows = new Set(old.selectedRowPaths)
if (!exists && shouldExist) {
flatRowPaths.forEach(rowPath => {
if (rowPath === key || rowPath.startsWith(childRowPrefixKey)) {
newSelectedRows.add(rowPath)
}
})
} else if (exists && !shouldExist) {
flatRowPaths.forEach(rowPath => {
if (rowPath === key || rowPath.startsWith(childRowPrefixKey)) {
newSelectedRows.delete(rowPath)
}
})
} else {
return old
}
// If the row is a subRow update
// its parent row to reflect changes
if (path.length > 1) updateParentRow(newSelectedRows, path)
return {
...old,
selectedRowPaths: [...newSelectedRows.values()],
}
}, actions.toggleRowSelected)
}
const toggleRowSelected = (path, selected) =>
dispatch({ type: actions.toggleRowSelected, path, selected })
// use reference to avoid memory leak in #1608
const instanceRef = React.useRef()
@ -217,13 +249,13 @@ function useMain(instance) {
props
)
}
// }
return row
})
return {
...instance,
flatRowPaths,
toggleRowSelected,
toggleRowSelectedAll,
getToggleAllRowsSelectedProps,

View File

@ -1,48 +1,77 @@
import React from 'react'
import { addActions, actions } from '../actions'
import { defaultState } from '../hooks/useTable'
//
defaultState.rowState = {}
import { actions, reducerHandlers } from '../hooks/useTable'
import { functionalUpdate, safeUseLayoutEffect } from '../utils'
addActions('setRowState', 'setCellState')
const pluginName = 'useRowState'
// Actions
actions.setRowState = 'setRowState'
actions.resetRowState = 'resetRowState'
// Reducer
reducerHandlers[pluginName] = (state, action) => {
if (action.type === actions.init) {
return {
rowState: {},
...state,
}
}
if (action.type === actions.resetRowState) {
return {
...state,
rowState: {},
}
}
if (action.type === actions.setRowState) {
const { path, value } = action
const pathKey = path.join('.')
return {
...state,
rowState: {
...state.rowState,
[pathKey]: functionalUpdate(value, state.rowState[pathKey]),
},
}
}
}
export const useRowState = hooks => {
hooks.useMain.push(useMain)
}
useRowState.pluginName = 'useRowState'
useRowState.pluginName = pluginName
const defaultGetResetRowStateDeps = ({ data }) => [data]
function useMain(instance) {
const {
hooks,
rows,
initialRowStateAccessor,
getResetRowStateDeps = defaultGetResetRowStateDeps,
state: { rowState },
setState,
dispatch,
} = instance
const setRowState = React.useCallback(
(path, updater, action = actions.setRowState) => {
const pathKey = path.join('.')
return setState(old => {
return {
...old,
rowState: {
...old.rowState,
[pathKey]:
typeof updater === 'function'
? updater(old.rowState[pathKey])
: updater,
},
}
}, action)
},
[setState]
(path, value, columnId) =>
dispatch({
type: actions.setRowState,
path,
value,
columnId,
}),
[dispatch]
)
const setCellState = React.useCallback(
(rowPath, columnID, updater) => {
(rowPath, columnId, updater) => {
return setRowState(
rowPath,
old => {
@ -50,14 +79,14 @@ function useMain(instance) {
...old,
cellState: {
...old.cellState,
[columnID]:
[columnId]:
typeof updater === 'function'
? updater(old.cellState[columnID])
? updater(old.cellState[columnId])
: updater,
},
}
},
actions.setCellState
columnId
)
},
[setRowState]
@ -66,18 +95,16 @@ function useMain(instance) {
const rowsMountedRef = React.useRef()
// When data changes, reset row and cell state
React.useEffect(() => {
safeUseLayoutEffect(() => {
if (rowsMountedRef.current) {
setState(old => {
return {
...old,
rowState: {},
}
}, actions.setRowState)
dispatch({ type: actions.resetRowState })
}
rowsMountedRef.current = true
}, [rows, setState])
}, [
dispatch,
...(getResetRowStateDeps ? getResetRowStateDeps(instance) : []),
])
hooks.prepareRow.push(row => {
const pathKey = row.path.join('.')

View File

@ -1,8 +1,7 @@
import React from 'react'
import { ensurePluginOrder, defaultColumn, safeUseLayoutEffect } from '../utils'
import { addActions, actions } from '../actions'
import { defaultState } from '../hooks/useTable'
import { actions, reducerHandlers } from '../hooks/useTable'
import * as sortTypes from '../sortTypes'
import {
mergeProps,
@ -12,17 +11,148 @@ import {
isFunction,
} from '../utils'
defaultState.sortBy = []
const pluginName = 'useSortBy'
// Actions
actions.resetSortBy = 'resetSortBy'
actions.toggleSortBy = 'toggleSortBy'
actions.clearSortBy = 'clearSortBy'
// Reducer
reducerHandlers[pluginName] = (state, action) => {
if (action.type === actions.init) {
return {
sortBy: [],
...state,
}
}
if (action.type === actions.resetSortBy) {
return {
...state,
sortBy: {},
}
}
if (action.type === actions.clearSortBy) {
const { sortBy } = state
const newSortBy = sortBy.filter(d => d.id !== action.columnID)
return {
...state,
sortBy: newSortBy,
}
}
if (action.type === actions.toggleSortBy) {
const {
columnId,
desc,
multi,
instanceRef: {
current: {
flatColumns,
disableMultiSort,
disableSortRemove,
disableMultiRemove,
maxMultiSortColCount = Number.MAX_SAFE_INTEGER,
},
},
} = action
const { sortBy } = state
// Find the column for this columnId
const column = flatColumns.find(d => d.id === columnId)
const { sortDescFirst } = column
// Find any existing sortBy for this column
const existingSortBy = sortBy.find(d => d.id === columnId)
const existingIndex = sortBy.findIndex(d => d.id === columnId)
const hasDescDefined = typeof desc !== 'undefined' && desc !== null
let newSortBy = []
// What should we do with this sort action?
let sortAction
if (!disableMultiSort && multi) {
if (existingSortBy) {
sortAction = 'toggle'
} else {
sortAction = 'add'
}
} else {
// Normal mode
if (existingIndex !== sortBy.length - 1) {
sortAction = 'replace'
} else if (existingSortBy) {
sortAction = 'toggle'
} else {
sortAction = 'replace'
}
}
// Handle toggle states that will remove the sortBy
if (
sortAction === 'toggle' && // Must be toggling
!disableSortRemove && // If disableSortRemove, disable in general
!hasDescDefined && // Must not be setting desc
(multi ? !disableMultiRemove : true) && // If multi, don't allow if disableMultiRemove
((existingSortBy && // Finally, detect if it should indeed be removed
existingSortBy.desc &&
!sortDescFirst) ||
(!existingSortBy.desc && sortDescFirst))
) {
sortAction = 'remove'
}
if (sortAction === 'replace') {
newSortBy = [
{
id: columnId,
desc: hasDescDefined ? desc : sortDescFirst,
},
]
} else if (sortAction === 'add') {
newSortBy = [
...sortBy,
{
id: columnId,
desc: hasDescDefined ? desc : sortDescFirst,
},
]
// Take latest n columns
newSortBy.splice(0, newSortBy.length - maxMultiSortColCount)
} else if (sortAction === 'toggle') {
// This flips (or sets) the
newSortBy = sortBy.map(d => {
if (d.id === columnId) {
return {
...d,
desc: hasDescDefined ? desc : !existingSortBy.desc,
}
}
return d
})
} else if (sortAction === 'remove') {
newSortBy = sortBy.filter(d => d.id !== columnId)
}
return {
...state,
sortBy: newSortBy,
}
}
}
defaultColumn.sortType = 'alphanumeric'
defaultColumn.sortDescFirst = false
addActions('sortByChange')
export const useSortBy = hooks => {
hooks.useMain.push(useMain)
}
useSortBy.pluginName = 'useSortBy'
useSortBy.pluginName = pluginName
function useMain(instance) {
const {
@ -34,15 +164,11 @@ function useMain(instance) {
manualSorting,
defaultCanSort,
disableSortBy,
disableSortRemove,
disableMultiRemove,
disableMultiSort,
isMultiSortEvent = e => e.shiftKey,
maxMultiSortColCount = Number.MAX_SAFE_INTEGER,
flatHeaders,
hooks,
state: { sortBy },
setState,
dispatch,
plugins,
getResetSortByDeps = false,
} = instance
@ -55,104 +181,14 @@ function useMain(instance) {
const isMountedRef = React.useRef()
safeUseLayoutEffect(() => {
if (isMountedRef.current) {
setState(
old => ({
...old,
sortBy: [],
}),
actions.sortByChange
)
dispatch({ type: actions.resetSortBy })
}
isMountedRef.current = true
}, [setState, ...(getResetSortByDeps ? getResetSortByDeps(instance) : [])])
}, [dispatch, ...(getResetSortByDeps ? getResetSortByDeps(instance) : [])])
// Updates sorting based on a columnID, desc flag and multi flag
const toggleSortBy = (columnID, desc, multi) => {
return setState(old => {
const { sortBy } = old
// Find the column for this columnID
const column = flatColumns.find(d => d.id === columnID)
const { sortDescFirst } = column
// Find any existing sortBy for this column
const existingSortBy = sortBy.find(d => d.id === columnID)
const existingIndex = sortBy.findIndex(d => d.id === columnID)
const hasDescDefined = typeof desc !== 'undefined' && desc !== null
let newSortBy = []
// What should we do with this sort action?
let action
if (!disableMultiSort && multi) {
if (existingSortBy) {
action = 'toggle'
} else {
action = 'add'
}
} else {
// Normal mode
if (existingIndex !== sortBy.length - 1) {
action = 'replace'
} else if (existingSortBy) {
action = 'toggle'
} else {
action = 'replace'
}
}
// Handle toggle states that will remove the sortBy
if (
action === 'toggle' && // Must be toggling
!disableSortRemove && // If disableSortRemove, disable in general
!hasDescDefined && // Must not be setting desc
(multi ? !disableMultiRemove : true) && // If multi, don't allow if disableMultiRemove
((existingSortBy && // Finally, detect if it should indeed be removed
existingSortBy.desc &&
!sortDescFirst) ||
(!existingSortBy.desc && sortDescFirst))
) {
action = 'remove'
}
if (action === 'replace') {
newSortBy = [
{
id: columnID,
desc: hasDescDefined ? desc : sortDescFirst,
},
]
} else if (action === 'add') {
newSortBy = [
...sortBy,
{
id: columnID,
desc: hasDescDefined ? desc : sortDescFirst,
},
]
// Take latest n columns
newSortBy.splice(0, newSortBy.length - maxMultiSortColCount)
} else if (action === 'toggle') {
// This flips (or sets) the
newSortBy = sortBy.map(d => {
if (d.id === columnID) {
return {
...d,
desc: hasDescDefined ? desc : !existingSortBy.desc,
}
}
return d
})
} else if (action === 'remove') {
newSortBy = sortBy.filter(d => d.id !== columnID)
}
return {
...old,
sortBy: newSortBy,
}
}, actions.sortByChange)
// Updates sorting based on a columnId, desc flag and multi flag
const toggleSortBy = (columnId, desc, multi) => {
dispatch({ type: actions.toggleSortBy, columnId, desc, multi })
}
// use reference to avoid memory leak in #1608
@ -182,15 +218,8 @@ function useMain(instance) {
column.toggleSortBy = (desc, multi) =>
toggleSortBy(column.id, desc, multi)
column.clearSorting = () => {
return setState(old => {
const { sortBy } = old
const newSortBy = sortBy.filter(d => d.id !== column.id)
return {
...old,
sortBy: newSortBy,
}
}, actions.sortByChange)
column.clearSortBy = () => {
dispatch({ type: actions.clearSortBy, columnId: column.id })
}
}
@ -230,7 +259,7 @@ function useMain(instance) {
if (manualSorting || !sortBy.length) {
return rows
}
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.time('getSortedRows')
// Filter out sortBys that correspond to non existing columns
@ -302,7 +331,7 @@ function useMain(instance) {
return sortedData
}
if (process.env.NODE_ENV === 'development' && debug)
if (process.env.NODE_ENV !== 'production' && debug)
console.timeEnd('getSortedRows')
return sortData(rows)

View File

@ -3,9 +3,9 @@ const reSplitAlphaNumeric = /([0-9]+)/gm
// Mixed sorting is slow, but very inclusive of many edge cases.
// It handles numbers, mixed alphanumeric combinations, and even
// null, undefined, and Infinity
export const alphanumeric = (rowA, rowB, columnID) => {
let a = getRowValueByColumnID(rowA, columnID)
let b = getRowValueByColumnID(rowB, columnID)
export const alphanumeric = (rowA, rowB, columnId) => {
let a = getRowValueByColumnID(rowA, columnId)
let b = getRowValueByColumnID(rowB, columnId)
// Force to strings (or "" for unsupported types)
a = toString(a)
b = toString(b)
@ -53,9 +53,9 @@ export const alphanumeric = (rowA, rowB, columnID) => {
return a.length - b.length
}
export function datetime(rowA, rowB, columnID) {
let a = getRowValueByColumnID(rowA, columnID)
let b = getRowValueByColumnID(rowB, columnID)
export function datetime(rowA, rowB, columnId) {
let a = getRowValueByColumnID(rowA, columnId)
let b = getRowValueByColumnID(rowB, columnId)
a = a.getTime()
b = b.getTime()
@ -63,9 +63,9 @@ export function datetime(rowA, rowB, columnID) {
return compareBasic(a, b)
}
export function basic(rowA, rowB, columnID) {
let a = getRowValueByColumnID(rowA, columnID)
let b = getRowValueByColumnID(rowB, columnID)
export function basic(rowA, rowB, columnId) {
let a = getRowValueByColumnID(rowA, columnId)
let b = getRowValueByColumnID(rowB, columnId)
return compareBasic(a, b)
}
@ -76,8 +76,8 @@ function compareBasic(a, b) {
return a === b ? 0 : a > b ? 1 : -1
}
function getRowValueByColumnID(row, columnID) {
return row.values[columnID]
function getRowValueByColumnID(row, columnId) {
return row.values[columnId]
}
function toString(a) {

View File

@ -113,24 +113,24 @@ export function makeHeaderGroups(flatColumns, defaultColumn) {
// If the column has a parent, add it if necessary
if (column.parent) {
const similarParentColumns = parentColumns.filter(
d => d.originalID === column.parent.id
d => d.originalId === column.parent.id
)
if (isFirst || latestParentColumn.originalID !== column.parent.id) {
if (isFirst || latestParentColumn.originalId !== column.parent.id) {
parentColumns.push({
...column.parent,
originalID: column.parent.id,
originalId: column.parent.id,
id: [column.parent.id, similarParentColumns.length].join('_'),
})
}
} else if (hasParents) {
// If other columns have parents, we'll need to add a place holder if necessary
const originalID = [column.id, 'placeholder'].join('_')
const originalId = [column.id, 'placeholder'].join('_')
const similarParentColumns = parentColumns.filter(
d => d.originalID === originalID
d => d.originalId === originalId
)
const placeholderColumn = decorateColumn(
{
originalID,
originalId,
id: [column.id, 'placeholder', similarParentColumns.length].join(
'_'
),
@ -140,7 +140,7 @@ export function makeHeaderGroups(flatColumns, defaultColumn) {
)
if (
isFirst ||
latestParentColumn.originalID !== placeholderColumn.originalID
latestParentColumn.originalId !== placeholderColumn.originalId
) {
parentColumns.push(placeholderColumn)
}
@ -245,11 +245,11 @@ export function getFirstDefined(...args) {
}
}
export function defaultGroupByFn(rows, columnID) {
export function defaultGroupByFn(rows, columnId) {
return rows.reduce((prev, row, i) => {
// TODO: Might want to implement a key serializer here so
// irregular column values can still be grouped if needed?
const resKey = `${row.values[columnID]}`
const resKey = `${row.values[columnId]}`
prev[resKey] = Array.isArray(prev[resKey]) ? prev[resKey] : []
prev[resKey].push(row)
return prev
@ -438,6 +438,10 @@ export function expandRows(
return expandedRows
}
export function functionalUpdate(updater, old) {
return typeof updater === 'function' ? updater(old) : updater
}
//
function makePathArray(obj) {