Changed: Tests, aggregation, hooks, columnVisibility, docs

This commit is contained in:
Tanner Linsley 2020-02-14 11:23:05 -07:00
parent 94a9b49187
commit b989a8fa76
83 changed files with 15083 additions and 7991 deletions

View File

@ -11,7 +11,8 @@
],
"env": {
"test": {
"presets": ["@babel/preset-env", "@babel/react"]
"presets": ["@babel/preset-env", "@babel/react"],
"plugins": ["@babel/plugin-transform-runtime"]
}
}
}

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ react-table.css
.DS_Store
.history
package-lock.json
coverage

View File

@ -1,20 +1,20 @@
{
"dist/index.js": {
"bundled": 112721,
"minified": 52301,
"gzipped": 13754
"bundled": 126199,
"minified": 59470,
"gzipped": 15298
},
"dist/index.es.js": {
"bundled": 111784,
"minified": 51465,
"gzipped": 13587,
"bundled": 125286,
"minified": 58658,
"gzipped": 15131,
"treeshaked": {
"rollup": {
"code": 80,
"import_statements": 21
},
"webpack": {
"code": 8461
"code": 8471
}
}
},

View File

@ -1,3 +1,24 @@
## 7.0.0-rc.16
- Moved away from snapshot tests. No more testing implementation details.
- Added `visibleColumns` and `visibleColumnsDeps` hooks to manipulate columns after all data is processed. Further visibility processing may result in these columns not being visible, such as `hiddenColumn` state
- The `useRows` hook has been deprecated due to its dangerous nature 💀
- Added the `instance.rowsById` object
- Renamed `instance.flatColumns` to `instance.allColumns` which now accumulates ALL columns created for the table, visible or not.
- Added the `instance.visibleColumns` object
- Fix an issue where `useAsyncDebounce` would crash when passed arguments
- Started development on the `usePivotColumns` plugin, which can be tested currently using the `_UNSTABLE_usePivoteColumns` export.
- Renamed `cell.isRepeatedValue` to `cell.isPlaceholder`
- Removed `useConsumeHookGetter` as it was inefficient most of the time and noisy
- All hooks are now "consumed" right after main plugin functions are run. This means that any attempt to add a plugin after that will result in a runtime error (for good reason, since using hook points should not be a conditional or async operation)
- Added `instance.getHooks` for getting the list of hooks that was captured after plugins are run
- Normalized all "toggle" actions to use an optional `value` property to set the value instead of toggle. Previously properties like `selected`, `groupBy`, etc. were used, but not any more!
- Undocument `instance.dispatch`. Both plugins and users should be interacting with the table via methods assigned to the instance and other structures on the table. This should both reduce the surface API that React Table needs to expose and also the amount of documentation that is needed to understand how to use the API.
- `useRowState`'s `initialRowStateAccessor` and `initialCellStateAccessor` options now have a default of `row => ({})` and `cell => ({})` respectively.
- Removed the concept of complex aggregations (eg. `column.aggregate = ['sum', 'count']`). Instead, a better aggregation function signature is now used to allow for leaf node aggregation when needed.
- Added the `column.aggregateValue` option which allows resolving (or pre-aggregating) a cell's value before it is grouped and aggregated across rows. This is useful for cell values that are not primitive, eg. an array of values that you may want to unique and count before summing that count across your groupings
- The function signature for aggregation functions has changed to be `(leafValues, aggregatedValues) => aggregatedValue` where `leafValues` is a flat array containing all leaf rows currently grouped at the aggregation level and `aggregatedValues` is an array containing the aggregated values from the immediate child sub rows. Each has purpose in the types of aggregations they power where optimizations are made for either accuracy or performance.
## 7.0.0-rc.15
- Added `useGlobalFilter` hook for performing table-wide filtering
@ -9,7 +30,7 @@
- Changed the function signature for all propGetter hooks to accept a single object of named meta properties instead of a variable length of meta arguments. The user props object has also been added as a property to all prop getters. For example, `hooks.getRowProps.push((props, instance, row) => [...])` is now written `hooks.getRowProps.push((props, { instance, row, userProps }) => [...])`
- Changed the function signature for all reduceHooks accept a single object of named meta properties instead of a variable length of meta arguments. For example, `hooks.flatColumns.push((flatColumns, instance) => flatColumns)` is now written `hooks.flatColumns.push((flatColumns, { instance }) => flatColumns)`
- Changed the function signature for all loopHooks accept a single object of named meta properties instead of a variable length of meta arguments. For example, `hooks.prepareRow.push((row, instance) => void)` is now written `hooks.prepareRow.push((row { instance }) => void)`
- Changed the function signature for all loopHooks accept a single object of named meta properties instead of a variable length of meta arguments. For example, `hooks.prepareRow.push((row, instance) => void)` is now written `hooks.prepareRow.push((row, { instance }) => void)`
## 7.0.0-rc.13

View File

@ -13,5 +13,5 @@ module.exports = {
rootDir: path.resolve(__dirname, '../../'),
roots: ['<rootDir>/src', __dirname],
transformIgnorePatterns: ['node_modules'],
snapshotSerializers: [require.resolve('snapshot-diff/serializer.js')],
collectCoverageFrom: ['src/**/*.js', '!**/*.test.js'],
}

View File

@ -1,2 +1 @@
import '@testing-library/jest-dom/extend-expect'
import 'snapshot-diff/extend-expect'

View File

@ -38,12 +38,18 @@ The following options are supported on any `Column` object passed to the `column
- Receives the table instance and cell model as props
- Must return valid JSX
- This function (or component) formats this column's value when it is being grouped and aggregated, eg. If this column was showing the number of visits for a user to a website and it was currently being grouped to show an **average** of the values, the `Aggregated` function for this column could format that value to `1,000 Avg. Visits`
- `aggregate: String | [String, String] | Function(values, rows, isAggregated: Bool) => any`
- `aggregate: String | Function(leafValues, aggregatedValues) => any`
- Optional
- Used to aggregate values across rows, eg. `average`-ing the ages of many cells in a table"
- If a single `String` is passed, it must be the key of either a user defined or predefined `aggregations` function.
- If a tuple array of `[String, String]` is passed, both must be a key of either a user defined or predefined `aggregations` function.
- The first is used to aggregate raw values, eg. `sum`-ing raw values together
- The second is used to aggregate values that have already been aggregated, eg. `average`-ing the sums produced by the raw aggregation level
- If a `Function` is passed, this function will receive the `values`, original `rows` of those values, and an `isAggregated` `Bool` of whether or not the values and rows have already been aggregated.
- If a `Function` is passed, this function will receive both the leaf-row values and (if the rows have already been aggregated, the previously aggregated values) to be aggregated into a single value.
- The function signature for all aggregation functions is `(leafValues, aggregatedValues) => aggregatedValue` where `leafValues` is a flat array containing all leaf rows currently grouped at the aggregation level and `aggregatedValues` is an array containing the aggregated values from the immediate child sub rows. Each has purpose in the types of aggregations they power where optimizations are made for either accuracy or performance.
- For examples on how an aggregation functions work, see the source code for the built in aggregations in the [src/aggregations.js](../../src/aggregations.js) file.
- `aggregateValue: String | Function(values, row, column) => any`
- Optional
- When attempting to group/aggregate non primitive cell values (eg. arrays of items) you will likely need to resolve a stable primitive value like a number or string to use in normal row aggregations. This property can be used to aggregate or simply access the value to be used in aggregations eg. `count`-ing the unique number of items in a cell's array value before `sum`-ing that count across the table.
- If a single `String` is passed, it must be the key of either a user defined or predefined `aggregations` function.
- If a `Function` is passed, this function will receive the cell's accessed value, the original `row` object and the `column` associated with the cell
- `disableGroupBy: Boolean`
- Defaults to `false`
- If `true`, will disable grouping for this column.
@ -92,6 +98,8 @@ The following properties are available on every `Row` object returned by the tab
- This object contains the **aggregated** values for this row's sub rows
- `subRows: Array<Row>`
- If the row is a materialized group row, this property is the array of materialized subRows that were grouped inside of this row.
- `leafRows: Array<Row>`
- If the row is a materialized group row, this property is an array containing all leaf node rows aggregated into this row.
- `depth: Int`
- If the row is a materialized group row, this is the grouping depth at which this row was created.
- `id: String`
@ -108,7 +116,7 @@ The following additional properties are available on every `Cell` object returne
- `isGrouped: Bool`
- If `true`, this cell is a grouped cell, meaning it contains a grouping value and should usually display and expander.
- `isRepeatedValue: Bool`
- `isPlaceholder: Bool`
- If `true`, this cell is a repeated value cell, meaning it contains a value that is already being displayed elsewhere (usually by a parent row's cell).
- Most of the time, this cell is not required to be displayed and can safely be hidden during rendering
- `isAggregated: Bool`

View File

@ -15,10 +15,16 @@ The following options are supported via the main options object passed to `useTa
- If a row's ID is found in this array, it will have the state of the value corresponding to that key.
- Individual row states can contain anything, but they also contain a `cellState` key, which provides cell-level state based on column ID's to every
**prepared** cell in the table.
- `initialRowStateAccessor: Function`
- `initialRowStateAccessor: Function(originalRow) => Object<any>`
- Optional
- This function may optionally return the initial state for a row.
- Defaults to: `row => ({})`
- This function should 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`
- `initialCellStateAccessor: Function(originalRow) => Object<any>`
- **Optional**
- Defaults to: `cell => ({})`
- This function should return the initial state for a cell.
- If this function is defined, it will be passed a `Cell` object, from which you can return a value to use as the initial state, eg. `cell => cell.row.original.initialCellState[cell.column.id]`
- `autoResetRowState: Boolean`
- Defaults to `true`
- When `true`, the `rowState` state will automatically reset if any of the following conditions are met:

View File

@ -67,7 +67,7 @@ The following options are supported via the main options object passed to `useTa
The following options are supported on any column object you can pass to `columns`.
- `accessor: String | Function`
- `accessor: String | Function(originalRow, rowIndex) => any`
- **Required**
- This string/function is used to build the data model for your column.
- The data returned by an accessor should be **primitive** and sortable.
@ -120,18 +120,15 @@ The following properties are available on the table instance returned from `useT
- `state: Object`
- **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.
- `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
- `flatColumns: Array<Column>`
- `allColumns: Array<Column>`
- A **flat** array of all final column objects.
- See [Column Properties](#column-properties) for more information
- `visibleColumns: Array<Column>`
- A **flat** array of all visible column objects derived from `allColumns`.
- See [Column Properties](#column-properties) for more information
- `headerGroups: Array<HeaderGroup>`
- An array of normalized header groups, each containing a flattened array of final column objects for that row.
- **Some of these headers may be materialized as placeholders**

View File

@ -205,7 +205,7 @@ function Table({ columns, data }) {
getTableBodyProps,
headerGroups,
rows,
flatColumns,
visibleColumns,
prepareRow,
setColumnOrder,
state,
@ -230,7 +230,7 @@ function Table({ columns, data }) {
)
const randomizeColumns = () => {
setColumnOrder(shuffle(flatColumns.map(d => d.id)))
setColumnOrder(shuffle(visibleColumns.map(d => d.id)))
}
return (
@ -267,30 +267,29 @@ function Table({ columns, data }) {
</thead>
<tbody {...getTableBodyProps()}>
<AnimatePresence>
{rows.slice(0, 10).map(
(row, i) => {
prepareRow(row);
return (
<motion.tr
{...row.getRowProps({
layoutTransition: spring,
exit: { opacity: 0, maxHeight: 0 },
})}
>
{row.cells.map((cell, i) => {
return (
<motion.td
{...cell.getCellProps({
layoutTransition: spring,
})}
>
{cell.render('Cell')}
</motion.td>
)
})}
</motion.tr>
)}
)}
{rows.slice(0, 10).map((row, i) => {
prepareRow(row)
return (
<motion.tr
{...row.getRowProps({
layoutTransition: spring,
exit: { opacity: 0, maxHeight: 0 },
})}
>
{row.cells.map((cell, i) => {
return (
<motion.td
{...cell.getCellProps({
layoutTransition: spring,
})}
>
{cell.render('Cell')}
</motion.td>
)
})}
</motion.tr>
)
})}
</AnimatePresence>
</tbody>
</table>

View File

@ -59,17 +59,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>
)

View File

@ -53,7 +53,7 @@ function Table({ columns, data }) {
headerGroups,
rows,
prepareRow,
flatColumns,
allColumns,
getToggleHideAllColumnsProps,
state,
} = useTable({
@ -69,7 +69,7 @@ function Table({ columns, data }) {
<IndeterminateCheckbox {...getToggleHideAllColumnsProps()} /> Toggle
All
</div>
{flatColumns.map(column => (
{allColumns.map(column => (
<div key={column.id}>
<label>
<input type="checkbox" {...column.getToggleHiddenProps()} />{' '}

View File

@ -50,7 +50,7 @@ function Table({ columns, data }) {
getTableBodyProps,
headerGroups,
rows,
flatColumns,
visibleColumns,
prepareRow,
setColumnOrder,
state,
@ -63,7 +63,7 @@ function Table({ columns, data }) {
)
const randomizeColumns = () => {
setColumnOrder(shuffle(flatColumns.map(d => d.id)))
setColumnOrder(shuffle(visibleColumns.map(d => d.id)))
}
return (
@ -80,19 +80,16 @@ function Table({ columns, data }) {
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.slice(0, 10).map(
(row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell, i) => {
return (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
)
})}
</tr>
)}
)}
{rows.slice(0, 10).map((row, i) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell, i) => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
<pre>

View File

@ -8087,10 +8087,10 @@ react-scripts@3.0.1:
optionalDependencies:
fsevents "2.0.6"
react-table@next:
version "7.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-alpha.7.tgz#0cb6da6f32adb397e68505b7cdd4880d15d73017"
integrity sha512-oXE9RRkE2CFk1OloNCSTPQ9qxOdujgkCoW5b/srbJsBog/ySkWuozBTQkxH1wGNmnSxGyTrTxJqXdXPQam7VAw==
react-table@latest:
version "7.0.0-rc.15"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.15.tgz#bb855e4e2abbb4aaf0ed2334404a41f3ada8e13a"
integrity sha512-ofMOlgrioHhhvHjvjsQkxvfQzU98cqwy6BjPGNwhLN1vhgXeWi0mUGreaCPvRenEbTiXsQbMl4k3Xmx3Mut8Rw==
react@^16.8.6:
version "16.8.6"

View File

@ -243,7 +243,7 @@ function Table({ columns, data }) {
rows,
prepareRow,
state,
flatColumns,
visibleColumns,
preGlobalFilteredRows,
setGlobalFilter,
} = useTable(
@ -278,7 +278,7 @@ function Table({ columns, data }) {
))}
<tr>
<th
colSpan={flatColumns.length}
colSpan={visibleColumns.length}
style={{
textAlign: 'left',
}}

View File

@ -65,7 +65,7 @@ function Table({ columns, data }) {
// Our custom plugin to add the expander column
hooks => {
hooks.useControlledState.push(useControlledState)
hooks.flatColumns.push((columns, { instance }) => {
hooks.visibleColumns.push((columns, { instance }) => {
if (!instance.state.groupBy.length) {
return columns
}
@ -74,9 +74,9 @@ function Table({ columns, data }) {
{
id: 'expander', // Make sure it has an ID
// Build our expander column
Header: ({ flatColumns, state: { groupBy } }) => {
Header: ({ allColumns, state: { groupBy } }) => {
return groupBy.map(columnId => {
const column = flatColumns.find(d => d.id === columnId)
const column = allColumns.find(d => d.id === columnId)
return (
<span {...column.getHeaderProps()}>
@ -166,7 +166,7 @@ function Table({ columns, data }) {
? '#0aff0082'
: cell.isAggregated
? '#ffa50078'
: cell.isRepeatedValue
: cell.isPlaceholder
? '#ff000042'
: 'white',
}}
@ -175,7 +175,7 @@ function Table({ columns, data }) {
? // If the cell is aggregated, use the Aggregated
// renderer for cell
cell.render('Aggregated')
: cell.isRepeatedValue
: cell.isPlaceholder
? null // For cells with repeated values, render null
: // Otherwise, just render the regular cell
cell.render('Cell')}
@ -225,20 +225,20 @@ function Legend() {
padding: '0.5rem',
}}
>
Repeated Value
Placeholder
</span>
</div>
)
}
// This is a custom aggregator that
// takes in an array of values and
// takes in an array of leaf values and
// returns the rounded median
function roundedMedian(values) {
let min = values[0] || ''
let max = values[0] || ''
function roundedMedian(leafValues) {
let min = leafValues[0] || 0
let max = leafValues[0] || 0
values.forEach(value => {
leafValues.forEach(value => {
min = Math.min(min, value)
max = Math.max(max, value)
})
@ -259,7 +259,7 @@ function App() {
// count the total rows being aggregated,
// then sum any of those counts if they are
// aggregated further
aggregate: ['sum', 'count'],
aggregate: 'count',
Aggregated: ({ cell: { value } }) => `${value} Names`,
},
{
@ -269,7 +269,7 @@ function App() {
// first count the UNIQUE values from the rows
// being aggregated, then sum those counts if
// they are aggregated further
aggregate: ['sum', 'uniqueCount'],
aggregate: 'uniqueCount',
Aggregated: ({ cell: { value } }) => `${value} Unique Names`,
},
],

View File

@ -79,50 +79,49 @@ function Table({ columns, data }) {
))}
</thead>
<tbody {...getTableBodyProps()}>
{firstPageRows.map(
(row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td
// For educational purposes, let's color the
// cell depending on what type it is given
// from the useGroupBy hook
{...cell.getCellProps()}
style={{
background: cell.isGrouped
? '#0aff0082'
: cell.isAggregated
? '#ffa50078'
: cell.isRepeatedValue
? '#ff000042'
: 'white',
}}
>
{cell.isGrouped ? (
// If it's a grouped cell, add an expander and row count
<>
<span {...row.getExpandedToggleProps()}>
{row.isExpanded ? '👇' : '👉'}
</span>{' '}
{cell.render('Cell')} ({row.subRows.length})
</>
) : cell.isAggregated ? (
// If the cell is aggregated, use the Aggregated
// renderer for cell
cell.render('Aggregated')
) : cell.isRepeatedValue ? null : ( // For cells with repeated values, render null
// Otherwise, just render the regular cell
cell.render('Cell')
)}
</td>
)
})}
</tr>
)}
)}
{firstPageRows.map((row, i) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td
// For educational purposes, let's color the
// cell depending on what type it is given
// from the useGroupBy hook
{...cell.getCellProps()}
style={{
background: cell.isGrouped
? '#0aff0082'
: cell.isAggregated
? '#ffa50078'
: cell.isPlaceholder
? '#ff000042'
: 'white',
}}
>
{cell.isGrouped ? (
// If it's a grouped cell, add an expander and row count
<>
<span {...row.getExpandedToggleProps()}>
{row.isExpanded ? '👇' : '👉'}
</span>{' '}
{cell.render('Cell')} ({row.subRows.length})
</>
) : cell.isAggregated ? (
// If the cell is aggregated, use the Aggregated
// renderer for cell
cell.render('Aggregated')
) : cell.isPlaceholder ? null : ( // For cells with repeated values, render null
// Otherwise, just render the regular cell
cell.render('Cell')
)}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
<br />
@ -170,13 +169,13 @@ function Legend() {
}
// This is a custom aggregator that
// takes in an array of values and
// takes in an array of leaf values and
// returns the rounded median
function roundedMedian(values) {
let min = values[0] || ''
let max = values[0] || ''
function roundedMedian(leafValues) {
let min = leafValues[0] || 0
let max = leafValues[0] || 0
values.forEach(value => {
leafValues.forEach(value => {
min = Math.min(min, value)
max = Math.max(max, value)
})
@ -197,7 +196,7 @@ function App() {
// count the total rows being aggregated,
// then sum any of those counts if they are
// aggregated further
aggregate: ['sum', 'count'],
aggregate: 'count',
Aggregated: ({ cell: { value } }) => `${value} Names`,
},
{
@ -207,7 +206,7 @@ function App() {
// first count the UNIQUE values from the rows
// being aggregated, then sum those counts if
// they are aggregated further
aggregate: ['sum', 'uniqueCount'],
aggregate: 'uniqueCount',
Aggregated: ({ cell: { value } }) => `${value} Unique Names`,
},
],
@ -220,7 +219,8 @@ function App() {
accessor: 'age',
// Aggregate the average age of visitors
aggregate: 'average',
Aggregated: ({ cell: { value } }) => `${value} (avg)`,
Aggregated: ({ cell: { value } }) =>
`${Math.round(value * 100) / 100} (avg)`,
},
{
Header: 'Visits',

View File

@ -308,7 +308,7 @@ function Table({ columns, data, updateMyData, skipPageReset }) {
usePagination,
useRowSelect,
hooks => {
hooks.flatColumns.push(columns => [
hooks.visibleColumns.push(columns => [
{
id: 'selection',
// Make this column a groupByBoundary. This ensures that groupBy columns
@ -388,7 +388,7 @@ function Table({ columns, data, updateMyData, skipPageReset }) {
// If the cell is aggregated, use the Aggregated
// renderer for cell
cell.render('Aggregated')
) : cell.isRepeatedValue ? null : ( // For cells with repeated values, render null
) : cell.isPlaceholder ? null : ( // For cells with repeated values, render null
// Otherwise, just render the regular cell
cell.render('Cell', { editable: true })
)}
@ -486,13 +486,13 @@ function filterGreaterThan(rows, id, filterValue) {
filterGreaterThan.autoRemove = val => typeof val !== 'number'
// This is a custom aggregator that
// takes in an array of values and
// takes in an array of leaf values and
// returns the rounded median
function roundedMedian(values) {
let min = values[0] || ''
let max = values[0] || ''
function roundedMedian(leafValues) {
let min = leafValues[0] || 0
let max = leafValues[0] || 0
values.forEach(value => {
leafValues.forEach(value => {
min = Math.min(min, value)
max = Math.max(max, value)
})
@ -513,7 +513,7 @@ function App() {
// count the total rows being aggregated,
// then sum any of those counts if they are
// aggregated further
aggregate: ['sum', 'count'],
aggregate: 'count',
Aggregated: ({ cell: { value } }) => `${value} Names`,
},
{
@ -525,7 +525,7 @@ function App() {
// first count the UNIQUE values from the rows
// being aggregated, then sum those counts if
// they are aggregated further
aggregate: ['sum', 'uniqueCount'],
aggregate: 'uniqueCount',
Aggregated: ({ cell: { value } }) => `${value} Unique Names`,
},
],

View File

@ -308,7 +308,7 @@ function Table({ columns, data, updateMyData, skipReset }) {
useRowSelect,
// Here we will use a plugin to add our selection column
hooks => {
hooks.flatColumns.push(columns => {
hooks.visibleColumns.push(columns => {
return [
{
id: 'selection',
@ -390,7 +390,7 @@ function Table({ columns, data, updateMyData, skipReset }) {
// If the cell is aggregated, use the Aggregated
// renderer for cell
cell.render('Aggregated')
) : cell.isRepeatedValue ? null : ( // For cells with repeated values, render null
) : cell.isPlaceholder ? null : ( // For cells with repeated values, render null
// Otherwise, just render the regular cell
cell.render('Cell', { editable: true })
)}
@ -488,13 +488,13 @@ function filterGreaterThan(rows, id, filterValue) {
filterGreaterThan.autoRemove = val => typeof val !== 'number'
// This is a custom aggregator that
// takes in an array of values and
// takes in an array of leaf values and
// returns the rounded median
function roundedMedian(values) {
let min = values[0] || ''
let max = values[0] || ''
function roundedMedian(leafValues) {
let min = leafValues[0] || 0
let max = leafValues[0] || 0
values.forEach(value => {
leafValues.forEach(value => {
min = Math.min(min, value)
max = Math.max(max, value)
})
@ -532,7 +532,7 @@ function App() {
// count the total rows being aggregated,
// then sum any of those counts if they are
// aggregated further
aggregate: ['sum', 'count'],
aggregate: 'count',
Aggregated: ({ cell: { value } }) => `${value} Names`,
},
{
@ -544,7 +544,7 @@ function App() {
// first count the UNIQUE values from the rows
// being aggregated, then sum those counts if
// they are aggregated further
aggregate: ['sum', 'uniqueCount'],
aggregate: 'uniqueCount',
Aggregated: ({ cell: { value } }) => `${value} Unique Names`,
},
],

View File

@ -0,0 +1,4 @@
{
"presets": ["react-app"],
"plugins": ["styled-components"]
}

1
examples/pivoting/.env Normal file
View File

@ -0,0 +1 @@
SKIP_PREFLIGHT_CHECK=true

View File

@ -0,0 +1,7 @@
{
"extends": ["react-app", "prettier"],
"rules": {
// "eqeqeq": 0,
// "jsx-a11y/anchor-is-valid": 0
}
}

23
examples/pivoting/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,29 @@
const path = require('path')
const resolveFrom = require('resolve-from')
const fixLinkedDependencies = config => {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
react$: resolveFrom(path.resolve('node_modules'), 'react'),
'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'),
},
}
return config
}
const includeSrcDirectory = config => {
config.resolve = {
...config.resolve,
modules: [path.resolve('src'), ...config.resolve.modules],
}
return config
}
module.exports = [
['use-babel-config', '.babelrc'],
['use-eslint-config', '.eslintrc'],
fixLinkedDependencies,
// includeSrcDirectory,
]

View File

@ -0,0 +1,6 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app) and Rescripts.
You can:
- [Open this example in a new CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/pivoting)
- `yarn` and `yarn start` to run and edit the example

View File

@ -0,0 +1,36 @@
{
"private": true,
"scripts": {
"start": "rescripts start",
"build": "rescripts build",
"test": "rescripts test",
"eject": "rescripts eject"
},
"dependencies": {
"dayjs": "^1.8.18",
"namor": "^1.1.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.1",
"react-table": "latest",
"styled-components": "^4.3.2"
},
"devDependencies": {
"@rescripts/cli": "^0.0.11",
"@rescripts/rescript-use-babel-config": "^0.0.8",
"@rescripts/rescript-use-eslint-config": "^0.0.9",
"babel-eslint": "10.0.1"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,298 @@
import React from 'react'
import styled from 'styled-components'
import {
useTable,
useGroupBy,
useExpanded,
_UNSTABLE_usePivoteColumns,
} from 'react-table'
import dayjs from 'dayjs'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import makeData from './makeData'
dayjs.extend(localizedFormat)
const Styles = styled.div`
padding: 1rem;
white-space: nowrap;
table {
border-spacing: 0;
border: 1px solid black;
tr {
:last-child {
td {
border-bottom: 0;
}
}
}
th,
td {
margin: 0;
padding: 0.5rem;
border-bottom: 1px solid black;
border-right: 1px solid black;
:last-child {
border-right: 0;
}
}
}
`
const IndeterminateCheckbox = React.forwardRef(
({ indeterminate, ...rest }, ref) => {
const defaultRef = React.useRef()
const resolvedRef = ref || defaultRef
React.useEffect(() => {
resolvedRef.current.indeterminate = indeterminate
}, [resolvedRef, indeterminate])
return <input type="checkbox" ref={resolvedRef} {...rest} />
}
)
const renderHeaderToggles = headers => (
<>
{headers.map(column => (
<div key={column.id}>
<label>
<input type="checkbox" {...column.getToggleHiddenProps()} />{' '}
{column.id}
</label>
{column.headers && column.headers.length ? (
<div
style={{
paddingLeft: '2rem',
}}
>
{renderHeaderToggles(column.headers)}
</div>
) : null}
</div>
))}
</>
)
function Table({ columns, data }) {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
state,
visibleColumns,
allColumns,
toggleGroupBy,
togglePivot,
getToggleHideAllColumnsProps,
headers,
} = useTable(
{
columns,
data,
},
useGroupBy,
_UNSTABLE_usePivoteColumns,
useExpanded // useGroupBy and _UNSTABLE_usePivoteColumns would be pretty useless without useExpanded ;)
)
// We don't want to render all of the rows for this example, so cap
// it at 20 for this use case
const firstPageRows = rows.slice(0, 25)
const options = allColumns.filter(
d => !d.isGrouped && !d.isPivoted && !d.isPivotSource
)
return (
<>
<table {...getTableProps()}>
<thead>
<tr>
<td colSpan={visibleColumns.length}>
Group By:{' '}
{state.groupBy.map(columnId => {
const column = allColumns.find(d => d.id === columnId)
return (
<span key={column.id}>
<button onClick={() => column.toggleGroupBy()}>
🛑 {column.render('Header')}
</button>{' '}
</span>
)
})}
<select
onChange={e => toggleGroupBy(e.target.value, true)}
value=""
>
<option disabled selected value="">
Add column...{' '}
</option>
{options.map(column => (
<option key={column.id} value={column.id}>
{column.render('Header')}
</option>
))}
</select>
</td>
</tr>
<tr>
<td colSpan={visibleColumns.length}>
Pivot Columns:
{state.pivotColumns.map(columnId => {
const column = allColumns.find(d => d.id === columnId)
return (
<span key={column.id}>
<button onClick={() => column.togglePivot()}>
🛑 {column.render('Header')}
</button>{' '}
</span>
)
})}
<select
onChange={e => togglePivot(e.target.value, true)}
value=""
>
<option disabled selected value="">
Add column...{' '}
</option>
{options.map(column => (
<option key={column.id} value={column.id}>
{column.render('Header')}
</option>
))}
</select>
</td>
</tr>
<tr>
<td colSpan={visibleColumns.length}>
<div>
<IndeterminateCheckbox {...getToggleHideAllColumnsProps()} />{' '}
Toggle All
</div>
{renderHeaderToggles(headers)}
</td>
</tr>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{firstPageRows.map((row, i) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td {...cell.getCellProps()}>
{cell.isGrouped ? (
<>
<span {...row.getExpandedToggleProps()}>
{row.isExpanded ? '👇' : '👉'} {cell.render('Cell')}{' '}
({row.subRows.length})
</span>
</>
) : cell.isAggregated ? (
cell.render('Aggregated')
) : cell.isPlaceholder ? null : (
cell.render('Cell')
)}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
<br />
<div>Showing the first 25 results of {rows.length} rows</div>
<pre>
<code>{JSON.stringify(state, null, 2)}</code>
</pre>
</>
)
}
function App() {
const columns = React.useMemo(
() => [
{
Header: 'Order Date',
id: 'date',
accessor: d => new Date(d.date),
sortType: 'basic',
aggregate: 'count',
Cell: ({ cell: { value } }) => (value ? dayjs(value).format('l') : ''),
Aggregated: ({ cell: { value } }) => `${value} Orders`,
},
{
Header: 'Employee',
accessor: 'rep',
aggregate: 'uniqueCount',
},
{
Header: 'Region',
accessor: 'region',
aggregate: 'uniqueCount',
},
{
Header: 'Item',
accessor: 'item',
aggregate: 'count',
},
{
Header: 'Units',
accessor: 'units',
aggregate: 'sum',
},
{
Header: 'Unit Cost',
accessor: 'unitCost',
aggregate: 'average',
Cell: ({ cell: { value } }) =>
typeof value !== 'undefined' ? (
<div
style={{ textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}
>
${(Math.floor(value * 100) / 100).toLocaleString()}
</div>
) : null,
},
{
Header: 'Total',
accessor: 'total',
aggregate: 'sum',
Cell: ({ cell: { value } }) =>
typeof value !== 'undefined' ? (
<div
style={{ textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}
>
${(Math.floor(value * 100) / 100).toLocaleString()}
</div>
) : null,
},
],
[]
)
const data = React.useMemo(() => makeData(10000), [])
return (
<Styles>
<Table columns={columns} data={data} />
</Styles>
)
}
export default App

View File

@ -0,0 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
it('renders without crashing', () => {
const div = document.createElement('div')
ReactDOM.render(<App />, div)
ReactDOM.unmountComponentAtNode(div)
})

View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -0,0 +1,6 @@
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
ReactDOM.render(<App />, document.getElementById('root'))

View File

@ -0,0 +1,95 @@
const skewLow = n => (n < 0.9 ? n / 2 : n)
const sample = items => {
const length = items.length
const rand = Math.floor(skewLow(Math.random() * length))
return items[rand]
}
const reps = [
'Jones',
'Kivell',
'Jardine',
'Gill',
'Sorvino',
'Andrews',
'Thompson',
'Morgan',
'Howard',
'Parent',
'Smith',
]
const regions = ['East', 'Central', 'West', 'International']
const items = [
['Pencil', 1.99, 100],
['Binder', 19.99, 1, 50],
['Pen', 7.99, 1, 100],
['Desk', 299.99, 1, 10],
['Notebook', 10.99, 1, 30],
]
const dates = [
'1/6/2018',
'1/23/2018',
'2/9/2018',
'2/26/2018',
'3/15/2018',
'4/1/2018',
'4/18/2018',
'5/5/2018',
'5/22/2018',
'6/8/2018',
'6/25/2018',
'7/12/2018',
'7/29/2018',
'8/15/2018',
'9/1/2018',
'9/18/2018',
'10/5/2018',
'10/22/2018',
'11/8/2018',
'11/25/2018',
'12/12/2018',
'12/29/2018',
'1/15/2019',
'2/1/2019',
'2/18/2019',
'3/7/2019',
'3/24/2019',
'4/10/2019',
'4/27/2019',
'5/14/2019',
'5/31/2019',
'6/17/2019',
'7/4/2019',
'7/21/2019',
'8/7/2019',
'8/24/2019',
'9/10/2019',
'9/27/2019',
'10/14/2019',
'10/31/2019',
'11/17/2019',
'12/4/2019',
'12/21/2019',
]
export default function makeData() {
return Array.from(new Array(10000)).map(() => {
const [item, unitCost, stock] = sample(items)
const units = Math.ceil(skewLow(Math.random()) * stock)
const total = units * unitCost
return {
date: sample(dates),
rep: sample(reps),
region: sample(regions),
item,
unitCost,
units,
total,
}
})
}

10150
examples/pivoting/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -67,7 +67,7 @@ function Table({ columns, data }) {
},
useRowSelect,
hooks => {
hooks.flatColumns.push(columns => [
hooks.visibleColumns.push(columns => [
// Let's make a column for selection
{
id: 'selection',

View File

@ -43,7 +43,7 @@ function Table({ columns: userColumns, data, renderRowSubComponent }) {
headerGroups,
rows,
prepareRow,
flatColumns,
visibleColumns,
state: { expanded },
} = useTable(
{
@ -88,7 +88,7 @@ function Table({ columns: userColumns, data, renderRowSubComponent }) {
*/}
{row.isExpanded ? (
<tr>
<td colSpan={flatColumns.length}>
<td colSpan={visibleColumns.length}>
{/*
Inside it, call our renderRowSubComponent function. In reality,
you could pass whatever you want as props to

View File

@ -21,7 +21,8 @@
"test": "is-ci \"test:ci\" \"test:dev\"",
"test:dev": "jest --watch",
"test:ci": "yarn test:jest",
"test:jest": "jest",
"test:jest": "jest --coverage",
"test:coverage": "yarn test:jest; open coverage/lcov-report/index.html",
"build": "cross-env NODE_ENV=production rollup -c",
"start": "rollup -c -w",
"prepare": "yarn build",
@ -77,6 +78,7 @@
"is-ci-cli": "^2.0.0",
"jest": "^24.9.0",
"jest-cli": "^24.9.0",
"jest-diff": "^25.1.0",
"jest-runner-eslint": "^0.7.5",
"jest-watch-select-projects": "^1.0.0",
"jest-watch-typeahead": "^0.4.2",
@ -93,12 +95,15 @@
"rollup-plugin-size": "^0.2.1",
"rollup-plugin-size-snapshot": "^0.10.0",
"rollup-plugin-terser": "^5.1.2",
"snapshot-diff": "^0.6.1"
"serve": "^11.2.0"
},
"config": {
"commitizen": {
"path": "node_modules/cz-conventional-changelog"
}
},
"browserslist": "> 0.25%, not dead"
"browserslist": "> 0.25%, not dead",
"dependencies": {
"@babel/plugin-transform-runtime": "^7.8.3"
}
}

View File

@ -1,19 +1,76 @@
export function sum(values, rows) {
return values.reduce((sum, next) => sum + next, 0)
export function sum(values, aggregatedValues) {
// It's faster to just add the aggregations together instead of
// process leaf nodes individually
return aggregatedValues.reduce(
(sum, next) => sum + (typeof next === 'number' ? next : 0),
0
)
}
export function average(values, rows) {
return Math.round((sum(values, rows) / values.length) * 100) / 100
export function min(values) {
let min = 0
values.forEach(value => {
if (typeof value === 'number') {
min = Math.min(min, value)
}
})
return min
}
export function max(values) {
let max = 0
values.forEach(value => {
if (typeof value === 'number') {
max = Math.max(max, value)
}
})
return max
}
export function minMax(values) {
let min = 0
let max = 0
values.forEach(value => {
if (typeof value === 'number') {
min = Math.min(min, value)
max = Math.max(max, value)
}
})
return `${min}..${max}`
}
export function average(values) {
return sum(null, values) / values.length
}
export function median(values) {
values = values.length ? values : [0]
let min = Math.min(...values)
let max = Math.max(...values)
if (!values.length) {
return null
}
let min = 0
let max = 0
values.forEach(value => {
if (typeof value === 'number') {
min = Math.min(min, value)
max = Math.max(max, value)
}
})
return (min + max) / 2
}
export function unique(values) {
return [...new Set(values).values()]
}
export function uniqueCount(values) {
return new Set(values).size
}

View File

@ -1,116 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders a basic table 1`] = `
<DocumentFragment>
<table>
<thead>
<tr>
<th
colspan="2"
>
Name
</th>
<th
colspan="4"
>
Info
</th>
</tr>
<tr>
<th
colspan="1"
>
First Name
</th>
<th
colspan="1"
>
Last Name
</th>
<th
colspan="1"
>
Age
</th>
<th
colspan="1"
>
Visits
</th>
<th
colspan="1"
>
Status
</th>
<th
colspan="1"
>
Profile Progress
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
tanner
</td>
<td>
linsley
</td>
<td>
29
</td>
<td>
100
</td>
<td>
In Relationship
</td>
<td>
50
</td>
</tr>
<tr>
<td>
derek
</td>
<td>
perkins
</td>
<td>
40
</td>
<td>
40
</td>
<td>
Single
</td>
<td>
80
</td>
</tr>
<tr>
<td>
joe
</td>
<td>
bergevin
</td>
<td>
45
</td>
<td>
20
</td>
<td>
Complicated
</td>
<td>
10
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;

View File

@ -115,14 +115,12 @@ function App() {
}
test('renders a basic table', () => {
const { getByText, asFragment } = render(<App />)
const rtl = render(<App />)
expect(getByText('tanner')).toBeInTheDocument()
expect(getByText('linsley')).toBeInTheDocument()
expect(getByText('29')).toBeInTheDocument()
expect(getByText('100')).toBeInTheDocument()
expect(getByText('In Relationship')).toBeInTheDocument()
expect(getByText('50')).toBeInTheDocument()
expect(asFragment()).toMatchSnapshot()
expect(rtl.getByText('tanner')).toBeInTheDocument()
expect(rtl.getByText('linsley')).toBeInTheDocument()
expect(rtl.getByText('29')).toBeInTheDocument()
expect(rtl.getByText('100')).toBeInTheDocument()
expect(rtl.getByText('In Relationship')).toBeInTheDocument()
expect(rtl.getByText('50')).toBeInTheDocument()
})

View File

@ -4,9 +4,8 @@ import {
actions,
functionalUpdate,
useGetLatest,
useConsumeHookGetter,
makePropGetter,
} from '../utils'
} from '../publicUtils'
actions.resetHiddenColumns = 'resetHiddenColumns'
actions.toggleHideColumn = 'toggleHideColumn'
@ -104,7 +103,7 @@ function reducer(state, action, previousState, instance) {
return {
...state,
hiddenColumns: shouldAll ? instance.flatColumns.map(d => d.id) : [],
hiddenColumns: shouldAll ? instance.allColumns.map(d => d.id) : [],
}
}
}
@ -150,13 +149,14 @@ function useInstance(instance) {
const {
flatHeaders,
dispatch,
flatColumns,
allColumns,
getHooks,
state: { hiddenColumns },
} = instance
const getInstance = useGetLatest(instance)
const allColumnsHidden = flatColumns.length === hiddenColumns.length
const allColumnsHidden = allColumns.length === hiddenColumns.length
const toggleHideColumn = React.useCallback(
(columnId, value) =>
@ -174,23 +174,11 @@ function useInstance(instance) {
[dispatch]
)
// Snapshot hook and disallow more from being added
const getToggleHideAllColumnsPropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getToggleHideAllColumnsProps'
)
const getToggleHideAllColumnsProps = makePropGetter(
getToggleHideAllColumnsPropsHooks(),
getHooks().getToggleHideAllColumnsProps,
{ instance: getInstance() }
)
// Snapshot hook and disallow more from being added
const getToggleHiddenPropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getToggleHiddenProps'
)
flatHeaders.forEach(column => {
column.toggleHidden = value => {
dispatch({
@ -200,10 +188,13 @@ function useInstance(instance) {
})
}
column.getToggleHiddenProps = makePropGetter(getToggleHiddenPropsHooks(), {
instance: getInstance(),
column,
})
column.getToggleHiddenProps = makePropGetter(
getHooks().getToggleHiddenProps,
{
instance: getInstance(),
column,
}
)
})
Object.assign(instance, {

View File

@ -2,17 +2,23 @@ import React from 'react'
//
import {
actions,
linkColumnStructure,
flattenColumns,
assignColumnAccessor,
accessRowsForColumn,
makeHeaderGroups,
decorateColumn,
dedupeBy,
} from '../utils'
import {
useGetLatest,
reduceHooks,
actions,
loopHooks,
makePropGetter,
makeRenderer,
decorateColumnTree,
makeHeaderGroups,
flattenBy,
useGetLatest,
useConsumeHookGetter,
} from '../utils'
} from '../publicUtils'
import makeDefaultPluginHooks from '../makeDefaultPluginHooks'
@ -73,15 +79,15 @@ export const useTable = (props, ...plugins) => {
plugin(getInstance().hooks)
})
const getUseOptionsHooks = useConsumeHookGetter(
getInstance().hooks,
'useOptions'
)
// Consume all hooks and make a getter for them
const getHooks = useGetLatest(getInstance().hooks)
getInstance().getHooks = getHooks
delete getInstance().hooks
// Allow useOptions hooks to modify the options coming into the table
Object.assign(
getInstance(),
reduceHooks(getUseOptionsHooks(), applyDefaults(props))
reduceHooks(getHooks().useOptions, applyDefaults(props))
)
const {
@ -95,12 +101,6 @@ export const useTable = (props, ...plugins) => {
useControlledState,
} = getInstance()
// Snapshot hook and disallow more from being added
const getStateReducers = useConsumeHookGetter(
getInstance().hooks,
'stateReducers'
)
// Setup user reducer ref
const getStateReducer = useGetLatest(stateReducer)
@ -115,7 +115,7 @@ export const useTable = (props, ...plugins) => {
// Reduce the state from all plugin reducers
return [
...getStateReducers(),
...getHooks().stateReducers,
// Allow the user to add their own state reducer(s)
...(Array.isArray(getStateReducer())
? getStateReducer()
@ -125,7 +125,7 @@ export const useTable = (props, ...plugins) => {
state
)
},
[getStateReducers, getStateReducer, getInstance]
[getHooks, getStateReducer, getInstance]
)
// Start the reducer
@ -133,15 +133,9 @@ export const useTable = (props, ...plugins) => {
reducer(initialState, { type: actions.init })
)
// Snapshot hook and disallow more from being added
const getUseControlledStateHooks = useConsumeHookGetter(
getInstance().hooks,
'useControlledState'
)
// Allow the user to control the final state with hooks
const state = reduceHooks(
[...getUseControlledStateHooks(), useControlledState],
[...getHooks().useControlledState, useControlledState],
reducerState,
{ instance: getInstance() }
)
@ -151,170 +145,182 @@ export const useTable = (props, ...plugins) => {
dispatch,
})
// Snapshot hook and disallow more from being added
const getColumnsHooks = useConsumeHookGetter(getInstance().hooks, 'columns')
// Snapshot hook and disallow more from being added
const getColumnsDepsHooks = useConsumeHookGetter(
getInstance().hooks,
'columnsDeps'
)
// Decorate All the columns
let columns = React.useMemo(
const columns = React.useMemo(
() =>
decorateColumnTree(
reduceHooks(getColumnsHooks(), userColumns, {
linkColumnStructure(
reduceHooks(getHooks().columns, userColumns, {
instance: getInstance(),
}),
defaultColumn
})
),
[
defaultColumn,
getColumnsHooks,
getHooks,
getInstance,
userColumns,
// eslint-disable-next-line react-hooks/exhaustive-deps
...reduceHooks(getColumnsDepsHooks(), [], { instance: getInstance() }),
...reduceHooks(getHooks().columnsDeps, [], { instance: getInstance() }),
]
)
getInstance().columns = columns
// Get the flat list of all columns and allow hooks to decorate
// those columns (and trigger this memoization via deps)
let flatColumns = React.useMemo(() => flattenBy(columns, 'columns'), [
columns,
])
let allColumns = React.useMemo(
() =>
reduceHooks(getHooks().allColumns, flattenColumns(columns), {
instance: getInstance(),
}).map(assignColumnAccessor),
[
columns,
getHooks,
getInstance,
// eslint-disable-next-line react-hooks/exhaustive-deps
...reduceHooks(getHooks().allColumnsDeps, [], {
instance: getInstance(),
}),
]
)
getInstance().allColumns = allColumns
getInstance().flatColumns = flatColumns
// Access the row model
const [rows, flatRows] = React.useMemo(() => {
// Access the row model using initial columns
const coreDataModel = React.useMemo(() => {
let rows = []
let flatRows = []
const rowsById = {}
// Access the row's data
const accessRow = (originalRow, i, depth = 0, parent) => {
// Keep the original reference around
const original = originalRow
const allColumnsQueue = [...allColumns]
const id = getRowId(originalRow, i, parent)
const row = {
id,
original,
index: i,
depth,
cells: [{}], // This is a dummy cell
}
flatRows.push(row)
// Process any subRows
let subRows = getSubRows(originalRow, i)
if (subRows) {
row.subRows = subRows.map((d, i) => accessRow(d, i, depth + 1, row))
}
// Override common array functions (and the dummy cell's getCellProps function)
// to show an error if it is accessed without calling prepareRow
const unpreparedAccessWarning = () => {
throw new Error(
'React-Table: You have not called prepareRow(row) one or more rows you are attempting to render.'
)
}
row.cells.map = unpreparedAccessWarning
row.cells.filter = unpreparedAccessWarning
row.cells.forEach = unpreparedAccessWarning
row.cells[0].getCellProps = unpreparedAccessWarning
// Create the cells and values
row.values = {}
flatColumns.forEach(({ id, accessor }) => {
row.values[id] = accessor
? accessor(originalRow, i, { subRows, depth, data })
: undefined
while (allColumnsQueue.length) {
const column = allColumnsQueue.shift()
accessRowsForColumn({
data,
rows,
flatRows,
rowsById,
column,
getRowId,
getSubRows,
accessValueHooks: getHooks().accessValue,
getInstance,
})
return row
}
// Use the resolved data
const accessedData = data.map((d, i) => accessRow(d, i))
return { rows, flatRows, rowsById }
}, [allColumns, data, getRowId, getSubRows, getHooks, getInstance])
return [accessedData, flatRows]
}, [data, flatColumns, getRowId, getSubRows])
// Allow materialized columns to also access data
const [rows, flatRows, rowsById, materializedColumns] = React.useMemo(() => {
const { rows, flatRows, rowsById } = coreDataModel
const materializedColumns = reduceHooks(
getHooks().materializedColumns,
[],
{
instance: getInstance(),
}
)
getInstance().rows = rows
getInstance().flatRows = flatRows
materializedColumns.forEach(d => assignColumnAccessor(d))
// Snapshot hook and disallow more from being added
const flatColumnsHooks = useConsumeHookGetter(
getInstance().hooks,
'flatColumns'
)
// Snapshot hook and disallow more from being added
const flatColumnsDepsHooks = useConsumeHookGetter(
getInstance().hooks,
'flatColumnsDeps'
const materializedColumnsQueue = [...materializedColumns]
while (materializedColumnsQueue.length) {
const column = materializedColumnsQueue.shift()
accessRowsForColumn({
data,
rows,
flatRows,
rowsById,
column,
getRowId,
getSubRows,
accessValueHooks: getHooks().accessValue,
getInstance,
})
}
return [rows, flatRows, rowsById, materializedColumns]
}, [
coreDataModel,
getHooks,
getInstance,
data,
getRowId,
getSubRows,
// eslint-disable-next-line react-hooks/exhaustive-deps
...reduceHooks(getHooks().materializedColumnsDeps, [], {
instance: getInstance(),
}),
])
Object.assign(getInstance(), {
rows,
flatRows,
rowsById,
materializedColumns,
})
loopHooks(getHooks().useInstanceAfterData, getInstance())
// Combine new materialized columns with all columns (dedupe prefers later columns)
allColumns = React.useMemo(
() => dedupeBy([...allColumns, ...materializedColumns], d => d.id),
[allColumns, materializedColumns]
)
getInstance().allColumns = allColumns
// Get the flat list of all columns AFTER the rows
// have been access, and allow hooks to decorate
// those columns (and trigger this memoization via deps)
flatColumns = React.useMemo(
let visibleColumns = React.useMemo(
() =>
reduceHooks(flatColumnsHooks(), flatColumns, { instance: getInstance() }),
reduceHooks(getHooks().visibleColumns, allColumns, {
instance: getInstance(),
}).map(d => decorateColumn(d, defaultColumn)),
[
flatColumns,
flatColumnsHooks,
getHooks,
allColumns,
getInstance,
defaultColumn,
// eslint-disable-next-line react-hooks/exhaustive-deps
...reduceHooks(flatColumnsDepsHooks(), [], { instance: getInstance() }),
...reduceHooks(getHooks().visibleColumnsDeps, [], {
instance: getInstance(),
}),
]
)
getInstance().flatColumns = flatColumns
// Snapshot hook and disallow more from being added
const getHeaderGroups = useConsumeHookGetter(
getInstance().hooks,
'headerGroups'
)
// Snapshot hook and disallow more from being added
const getHeaderGroupsDeps = useConsumeHookGetter(
getInstance().hooks,
'headerGroupsDeps'
// Combine new visible columns with all columns (dedupe prefers later columns)
allColumns = React.useMemo(
() => dedupeBy([...allColumns, ...visibleColumns], d => d.id),
[allColumns, visibleColumns]
)
getInstance().allColumns = allColumns
// Make the headerGroups
const headerGroups = React.useMemo(
() =>
reduceHooks(
getHeaderGroups(),
makeHeaderGroups(flatColumns, defaultColumn),
getHooks().headerGroups,
makeHeaderGroups(visibleColumns, defaultColumn),
getInstance()
),
[
getHooks,
visibleColumns,
defaultColumn,
flatColumns,
getHeaderGroups,
getInstance,
// eslint-disable-next-line react-hooks/exhaustive-deps
...reduceHooks(getHeaderGroupsDeps(), [], { instance: getInstance() }),
...reduceHooks(getHooks().headerGroupsDeps, [], {
instance: getInstance(),
}),
]
)
getInstance().headerGroups = headerGroups
// Get the first level of headers
const headers = React.useMemo(
() => (headerGroups.length ? headerGroups[0].headers : []),
[headerGroups]
)
getInstance().headers = headers
// Provide a flat header list for utilities
@ -323,13 +329,21 @@ export const useTable = (props, ...plugins) => {
[]
)
// Snapshot hook and disallow more from being added
const getUseInstanceBeforeDimensions = useConsumeHookGetter(
getInstance().hooks,
'useInstanceBeforeDimensions'
)
loopHooks(getHooks().useInstanceBeforeDimensions, getInstance())
loopHooks(getUseInstanceBeforeDimensions(), getInstance())
// Filter columns down to visible ones
const visibleColumnsDep = visibleColumns
.filter(d => d.isVisible)
.map(d => d.id)
.sort()
.join('_')
visibleColumns = React.useMemo(
() => visibleColumns.filter(d => d.isVisible),
// eslint-disable-next-line react-hooks/exhaustive-deps
[visibleColumns, visibleColumnsDep]
)
getInstance().visibleColumns = visibleColumns
// Header Visibility is needed by this point
const [
@ -342,59 +356,29 @@ export const useTable = (props, ...plugins) => {
getInstance().totalColumnsWidth = totalColumnsWidth
getInstance().totalColumnsMaxWidth = totalColumnsMaxWidth
// Snapshot hook and disallow more from being added
const getUseInstance = useConsumeHookGetter(
getInstance().hooks,
'useInstance'
)
loopHooks(getUseInstance(), getInstance())
// Snapshot hook and disallow more from being added
const getHeaderPropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getHeaderProps'
)
// Snapshot hook and disallow more from being added
const getFooterPropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getFooterProps'
)
loopHooks(getHooks().useInstance, getInstance())
// Each materialized header needs to be assigned a render function and other
// prop getter properties here.
;[...getInstance().flatHeaders, ...getInstance().flatColumns].forEach(
;[...getInstance().flatHeaders, ...getInstance().allColumns].forEach(
column => {
// Give columns/headers rendering power
column.render = makeRenderer(getInstance(), column)
// Give columns/headers a default getHeaderProps
column.getHeaderProps = makePropGetter(getHeaderPropsHooks(), {
column.getHeaderProps = makePropGetter(getHooks().getHeaderProps, {
instance: getInstance(),
column,
})
// Give columns/headers a default getFooterProps
column.getFooterProps = makePropGetter(getFooterPropsHooks(), {
column.getFooterProps = makePropGetter(getHooks().getFooterProps, {
instance: getInstance(),
column,
})
}
)
// Snapshot hook and disallow more from being added
const getHeaderGroupPropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getHeaderGroupProps'
)
// Snapshot hook and disallow more from being added
const getFooterGroupPropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getFooterGroupProps'
)
getInstance().headerGroups = getInstance().headerGroups.filter(
(headerGroup, i) => {
// Filter out any headers and headerGroups that don't have visible columns
@ -415,12 +399,12 @@ export const useTable = (props, ...plugins) => {
// Give headerGroups getRowProps
if (headerGroup.headers.length) {
headerGroup.getHeaderGroupProps = makePropGetter(
getHeaderGroupPropsHooks(),
getHooks().getHeaderGroupProps,
{ instance: getInstance(), headerGroup, index: i }
)
headerGroup.getFooterGroupProps = makePropGetter(
getFooterGroupPropsHooks(),
getHooks().getFooterGroupProps,
{ instance: getInstance(), headerGroup, index: i }
)
@ -433,48 +417,18 @@ export const useTable = (props, ...plugins) => {
getInstance().footerGroups = [...getInstance().headerGroups].reverse()
// Run the rows (this could be a dangerous hook with a ton of data)
// Snapshot hook and disallow more from being added
const getUseRowsHooks = useConsumeHookGetter(getInstance().hooks, 'useRows')
getInstance().rows = reduceHooks(getUseRowsHooks(), getInstance().rows, {
instance: getInstance(),
})
// The prepareRow function is absolutely necessary and MUST be called on
// any rows the user wishes to be displayed.
// Snapshot hook and disallow more from being added
const getPrepareRowHooks = useConsumeHookGetter(
getInstance().hooks,
'prepareRow'
)
// Snapshot hook and disallow more from being added
const getRowPropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getRowProps'
)
// Snapshot hook and disallow more from being added
const getCellPropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getCellProps'
)
// Snapshot hook and disallow more from being added
const cellsHooks = useConsumeHookGetter(getInstance().hooks, 'cells')
getInstance().prepareRow = React.useCallback(
row => {
row.getRowProps = makePropGetter(getRowPropsHooks(), {
row.getRowProps = makePropGetter(getHooks().getRowProps, {
instance: getInstance(),
row,
})
// Build the visible cells for each row
row.allCells = flatColumns.map(column => {
row.allCells = allColumns.map(column => {
const cell = {
column,
row,
@ -482,7 +436,7 @@ export const useTable = (props, ...plugins) => {
}
// Give each cell a getCellProps base
cell.getCellProps = makePropGetter(getCellPropsHooks(), {
cell.getCellProps = makePropGetter(getHooks().getCellProps, {
instance: getInstance(),
cell,
})
@ -496,50 +450,28 @@ export const useTable = (props, ...plugins) => {
return cell
})
row.cells = reduceHooks(cellsHooks(), row.allCells, {
instance: getInstance(),
})
row.cells = visibleColumns.map(column =>
row.allCells.find(cell => cell.column.id === column.id)
)
// need to apply any row specific hooks (useExpanded requires this)
loopHooks(getPrepareRowHooks(), row, { instance: getInstance() })
loopHooks(getHooks().prepareRow, row, { instance: getInstance() })
},
[
getRowPropsHooks,
getInstance,
flatColumns,
cellsHooks,
getPrepareRowHooks,
getCellPropsHooks,
]
[getHooks, getInstance, allColumns, visibleColumns]
)
// Snapshot hook and disallow more from being added
const getTablePropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getTableProps'
)
getInstance().getTableProps = makePropGetter(getTablePropsHooks(), {
getInstance().getTableProps = makePropGetter(getHooks().getTableProps, {
instance: getInstance(),
})
// Snapshot hook and disallow more from being added
const getTableBodyPropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getTableBodyProps'
getInstance().getTableBodyProps = makePropGetter(
getHooks().getTableBodyProps,
{
instance: getInstance(),
}
)
getInstance().getTableBodyProps = makePropGetter(getTableBodyPropsHooks(), {
instance: getInstance(),
})
// Snapshot hook and disallow more from being added
const getUseFinalInstanceHooks = useConsumeHookGetter(
getInstance().hooks,
'useFinalInstance'
)
loopHooks(getUseFinalInstanceHooks(), getInstance())
loopHooks(getHooks().useFinalInstance, getInstance())
return getInstance()
}

View File

@ -6,6 +6,7 @@ export { useGlobalFilter } from './plugin-hooks/useGlobalFilter'
export { useGroupBy } from './plugin-hooks/useGroupBy'
export { useSortBy } from './plugin-hooks/useSortBy'
export { usePagination } from './plugin-hooks/usePagination'
export { usePivotColumns as _UNSTABLE_usePivoteColumns } from './plugin-hooks/usePivotColumns'
export { useRowSelect } from './plugin-hooks/useRowSelect'
export { useRowState } from './plugin-hooks/useRowState'
export { useColumnOrder } from './plugin-hooks/useColumnOrder'

View File

@ -1,8 +1,17 @@
const defaultCells = cell => cell.filter(d => d.column.isVisible)
const defaultGetTableProps = props => ({
role: 'table',
...props,
})
const defaultGetTableBodyProps = props => ({
role: 'rowgroup',
...props,
})
const defaultGetHeaderProps = (props, { column }) => ({
key: `header_${column.id}`,
colSpan: column.totalVisibleHeaderCount,
role: 'columnheader',
...props,
})
@ -14,6 +23,7 @@ const defaultGetFooterProps = (props, { column }) => ({
const defaultGetHeaderGroupProps = (props, { index }) => ({
key: `headerGroup_${index}`,
role: 'row',
...props,
})
@ -24,12 +34,14 @@ const defaultGetFooterGroupProps = (props, { index }) => ({
const defaultGetRowProps = (props, { row }) => ({
key: `row_${row.id}`,
role: 'row',
...props,
})
const defaultGetCellProps = (props, { cell }) => ({
...props,
key: `cell_${cell.row.id}_${cell.column.id}`,
role: 'cell',
...props,
})
export default function makeDefaultPluginHooks() {
@ -39,17 +51,21 @@ export default function makeDefaultPluginHooks() {
useControlledState: [],
columns: [],
columnsDeps: [],
flatColumns: [],
flatColumnsDeps: [],
allColumns: [],
allColumnsDeps: [],
accessValue: [],
materializedColumns: [],
materializedColumnsDeps: [],
useInstanceAfterData: [],
visibleColumns: [],
visibleColumnsDeps: [],
headerGroups: [],
headerGroupsDeps: [],
useInstanceBeforeDimensions: [],
useInstance: [],
useRows: [],
cells: [defaultCells],
prepareRow: [],
getTableProps: [],
getTableBodyProps: [],
getTableProps: [defaultGetTableProps],
getTableBodyProps: [defaultGetTableBodyProps],
getHeaderGroupProps: [defaultGetHeaderGroupProps],
getFooterGroupProps: [defaultGetFooterGroupProps],
getHeaderProps: [defaultGetHeaderProps],

View File

@ -1,205 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders a table 1`] = `
<DocumentFragment>
<div
class="table"
>
<div>
<div
class="row"
style="position: relative; width: 1400px;"
>
<div
class="cell header"
colspan="2"
style="position: absolute; top: 0px; left: 0px; width: 550px;"
>
Name
</div>
<div
class="cell header"
colspan="4"
style="position: absolute; top: 0px; left: 550px; width: 850px;"
>
Info
</div>
</div>
<div
class="row"
style="position: relative; width: 1400px;"
>
<div
class="cell header"
colspan="1"
style="position: absolute; top: 0px; left: 0px; width: 250px;"
>
First Name
</div>
<div
class="cell header"
colspan="1"
style="position: absolute; top: 0px; left: 250px; width: 300px;"
>
Last Name
</div>
<div
class="cell header"
colspan="1"
style="position: absolute; top: 0px; left: 550px; width: 300px;"
>
Age
</div>
<div
class="cell header"
colspan="1"
style="position: absolute; top: 0px; left: 850px; width: 150px;"
>
Visits
</div>
<div
class="cell header"
colspan="1"
style="position: absolute; top: 0px; left: 1000px; width: 200px;"
>
Status
</div>
<div
class="cell header"
colspan="1"
style="position: absolute; top: 0px; left: 1200px; width: 200px;"
>
Profile Progress
</div>
</div>
</div>
<div
style="position: relative; width: 1400px;"
>
<div
class="row"
style="position: relative; width: 1400px;"
>
<div
class="cell"
style="position: absolute; top: 0px; left: 0px; width: 250px;"
>
firstName: tanner
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 250px; width: 300px;"
>
lastName: linsley
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 550px; width: 300px;"
>
age: 29
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 850px; width: 150px;"
>
visits: 100
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 1000px; width: 200px;"
>
status: In Relationship
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 1200px; width: 200px;"
>
progress: 50
</div>
</div>
<div
class="row"
style="position: relative; width: 1400px;"
>
<div
class="cell"
style="position: absolute; top: 0px; left: 0px; width: 250px;"
>
firstName: derek
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 250px; width: 300px;"
>
lastName: perkins
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 550px; width: 300px;"
>
age: 30
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 850px; width: 150px;"
>
visits: 40
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 1000px; width: 200px;"
>
status: Single
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 1200px; width: 200px;"
>
progress: 80
</div>
</div>
<div
class="row"
style="position: relative; width: 1400px;"
>
<div
class="cell"
style="position: absolute; top: 0px; left: 0px; width: 250px;"
>
firstName: joe
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 250px; width: 300px;"
>
lastName: bergevin
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 550px; width: 300px;"
>
age: 45
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 850px; width: 150px;"
>
visits: 20
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 1000px; width: 200px;"
>
status: Complicated
</div>
<div
class="cell"
style="position: absolute; top: 0px; left: 1200px; width: 200px;"
>
progress: 10
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@ -1,203 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders a table 1`] = `
<DocumentFragment>
<div
class="table"
>
<div>
<div
class="row"
style="display: flex; width: 1400px;"
>
<div
class="cell header"
colspan="2"
style="display: inline-block; box-sizing: border-box; width: 550px;"
>
Name
</div>
<div
class="cell header"
colspan="4"
style="display: inline-block; box-sizing: border-box; width: 850px;"
>
Info
</div>
</div>
<div
class="row"
style="display: flex; width: 1400px;"
>
<div
class="cell header"
colspan="1"
style="display: inline-block; box-sizing: border-box; width: 250px;"
>
First Name
</div>
<div
class="cell header"
colspan="1"
style="display: inline-block; box-sizing: border-box; width: 300px;"
>
Last Name
</div>
<div
class="cell header"
colspan="1"
style="display: inline-block; box-sizing: border-box; width: 300px;"
>
Age
</div>
<div
class="cell header"
colspan="1"
style="display: inline-block; box-sizing: border-box; width: 150px;"
>
Visits
</div>
<div
class="cell header"
colspan="1"
style="display: inline-block; box-sizing: border-box; width: 200px;"
>
Status
</div>
<div
class="cell header"
colspan="1"
style="display: inline-block; box-sizing: border-box; width: 200px;"
>
Profile Progress
</div>
</div>
</div>
<div>
<div
class="row"
style="display: flex; width: 1400px;"
>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 250px;"
>
firstName: tanner
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 300px;"
>
lastName: linsley
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 300px;"
>
age: 29
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 150px;"
>
visits: 100
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 200px;"
>
status: In Relationship
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 200px;"
>
progress: 50
</div>
</div>
<div
class="row"
style="display: flex; width: 1400px;"
>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 250px;"
>
firstName: derek
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 300px;"
>
lastName: perkins
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 300px;"
>
age: 30
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 150px;"
>
visits: 40
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 200px;"
>
status: Single
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 200px;"
>
progress: 80
</div>
</div>
<div
class="row"
style="display: flex; width: 1400px;"
>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 250px;"
>
firstName: joe
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 300px;"
>
lastName: bergevin
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 300px;"
>
age: 45
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 150px;"
>
visits: 20
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 200px;"
>
status: Complicated
</div>
<div
class="cell"
style="display: inline-block; box-sizing: border-box; width: 200px;"
>
progress: 10
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@ -1,911 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders an expandable table 1`] = `
Snapshot Diff:
- First value
+ Second value
@@ -1,10 +1,12 @@
<DocumentFragment>
<pre>
<code>
{
- "expanded": {}
+ "expanded": {
+ "0": true
+ }
}
</code>
</pre>
<table>
<thead>
@@ -64,10 +66,37 @@
<tbody>
<tr>
<td>
<span
style="cursor: pointer; padding-left: 0rem;"
+ >
+ 👇
+ </span>
+ </td>
+ <td>
+ tanner
+ </td>
+ <td>
+ linsley
+ </td>
+ <td>
+ 29
+ </td>
+ <td>
+ 100
+ </td>
+ <td>
+ In Relationship
+ </td>
+ <td>
+ 50
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <span
+ style="cursor: pointer; padding-left: 2rem;"
>
👉
</span>
</td>
<td>
@@ -85,10 +114,91 @@
<td>
In Relationship
</td>
<td>
50
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <span
+ style="cursor: pointer; padding-left: 2rem;"
+ >
+ 👉
+ </span>
+ </td>
+ <td>
+ derek
+ </td>
+ <td>
+ perkins
+ </td>
+ <td>
+ 40
+ </td>
+ <td>
+ 40
+ </td>
+ <td>
+ Single
+ </td>
+ <td>
+ 80
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <span
+ style="cursor: pointer; padding-left: 2rem;"
+ >
+ 👉
+ </span>
+ </td>
+ <td>
+ joe
+ </td>
+ <td>
+ bergevin
+ </td>
+ <td>
+ 45
+ </td>
+ <td>
+ 20
+ </td>
+ <td>
+ Complicated
+ </td>
+ <td>
+ 10
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <span
+ style="cursor: pointer; padding-left: 2rem;"
+ >
+ 👉
+ </span>
+ </td>
+ <td>
+ jaylen
+ </td>
+ <td>
+ linsley
+ </td>
+ <td>
+ 26
+ </td>
+ <td>
+ 99
+ </td>
+ <td>
+ In Relationship
+ </td>
+ <td>
+ 70
</td>
</tr>
<tr>
<td>
<span
`;
exports[`renders an expandable table 2`] = `
Snapshot Diff:
- First value
+ Second value
@@ -1,11 +1,12 @@
<DocumentFragment>
<pre>
<code>
{
"expanded": {
- "0": true
+ "0": true,
+ "0.0": true
}
}
</code>
</pre>
<table>
@@ -94,10 +95,37 @@
<tr>
<td>
<span
style="cursor: pointer; padding-left: 2rem;"
>
+ 👇
+ </span>
+ </td>
+ <td>
+ tanner
+ </td>
+ <td>
+ linsley
+ </td>
+ <td>
+ 29
+ </td>
+ <td>
+ 100
+ </td>
+ <td>
+ In Relationship
+ </td>
+ <td>
+ 50
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <span
+ style="cursor: pointer; padding-left: 4rem;"
+ >
👉
</span>
</td>
<td>
tanner
@@ -114,10 +142,91 @@
<td>
In Relationship
</td>
<td>
50
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <span
+ style="cursor: pointer; padding-left: 4rem;"
+ >
+ 👉
+ </span>
+ </td>
+ <td>
+ derek
+ </td>
+ <td>
+ perkins
+ </td>
+ <td>
+ 40
+ </td>
+ <td>
+ 40
+ </td>
+ <td>
+ Single
+ </td>
+ <td>
+ 80
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <span
+ style="cursor: pointer; padding-left: 4rem;"
+ >
+ 👉
+ </span>
+ </td>
+ <td>
+ joe
+ </td>
+ <td>
+ bergevin
+ </td>
+ <td>
+ 45
+ </td>
+ <td>
+ 20
+ </td>
+ <td>
+ Complicated
+ </td>
+ <td>
+ 10
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <span
+ style="cursor: pointer; padding-left: 4rem;"
+ >
+ 👉
+ </span>
+ </td>
+ <td>
+ jaylen
+ </td>
+ <td>
+ linsley
+ </td>
+ <td>
+ 26
+ </td>
+ <td>
+ 99
+ </td>
+ <td>
+ In Relationship
+ </td>
+ <td>
+ 70
</td>
</tr>
<tr>
<td>
<span
`;
exports[`renders an expandable table 3`] = `
Snapshot Diff:
- First value
+ Second value
@@ -2,11 +2,12 @@
<pre>
<code>
{
"expanded": {
"0": true,
- "0.0": true
+ "0.0": true,
+ "0.0.0": true
}
}
</code>
</pre>
<table>
@@ -122,10 +123,37 @@
<tr>
<td>
<span
style="cursor: pointer; padding-left: 4rem;"
>
+ 👇
+ </span>
+ </td>
+ <td>
+ tanner
+ </td>
+ <td>
+ linsley
+ </td>
+ <td>
+ 29
+ </td>
+ <td>
+ 100
+ </td>
+ <td>
+ In Relationship
+ </td>
+ <td>
+ 50
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <span
+ style="cursor: pointer; padding-left: 6rem;"
+ >
👉
</span>
</td>
<td>
tanner
@@ -142,10 +170,91 @@
<td>
In Relationship
</td>
<td>
50
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <span
+ style="cursor: pointer; padding-left: 6rem;"
+ >
+ 👉
+ </span>
+ </td>
+ <td>
+ derek
+ </td>
+ <td>
+ perkins
+ </td>
+ <td>
+ 40
+ </td>
+ <td>
+ 40
+ </td>
+ <td>
+ Single
+ </td>
+ <td>
+ 80
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <span
+ style="cursor: pointer; padding-left: 6rem;"
+ >
+ 👉
+ </span>
+ </td>
+ <td>
+ joe
+ </td>
+ <td>
+ bergevin
+ </td>
+ <td>
+ 45
+ </td>
+ <td>
+ 20
+ </td>
+ <td>
+ Complicated
+ </td>
+ <td>
+ 10
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <span
+ style="cursor: pointer; padding-left: 6rem;"
+ >
+ 👉
+ </span>
+ </td>
+ <td>
+ jaylen
+ </td>
+ <td>
+ linsley
+ </td>
+ <td>
+ 26
+ </td>
+ <td>
+ 99
+ </td>
+ <td>
+ In Relationship
+ </td>
+ <td>
+ 70
</td>
</tr>
<tr>
<td>
<span
`;
exports[`renders an expandable table 4`] = `
Snapshot Diff:
- First value
+ Second value
@@ -3,11 +3,12 @@
<code>
{
"expanded": {
"0": true,
"0.0": true,
- "0.0.0": true
+ "0.0.0": true,
+ "0.0.0.0": true
}
}
</code>
</pre>
<table>
@@ -150,11 +151,11 @@
<tr>
<td>
<span
style="cursor: pointer; padding-left: 6rem;"
>
- 👉
+ 👇
</span>
</td>
<td>
tanner
</td>
@@ -170,10 +171,30 @@
<td>
In Relationship
</td>
<td>
50
+ </td>
+ </tr>
+ <tr>
+ <td
+ colspan="7"
+ >
+ <pre>
+ <code>
+ {
+ "values": {
+ "firstName": "tanner",
+ "lastName": "linsley",
+ "age": 29,
+ "visits": 100,
+ "status": "In Relationship",
+ "progress": 50
+ }
+ }
+ </code>
+ </pre>
</td>
</tr>
<tr>
<td>
<span
`;
exports[`renders an expandable table 5`] = `
Snapshot Diff:
- First value
+ Second value
@@ -1,15 +1,10 @@
<DocumentFragment>
<pre>
<code>
{
- "expanded": {
- "0": true,
- "0.0": true,
- "0.0.0": true,
- "0.0.0.0": true
- }
+ "expanded": {}
}
</code>
</pre>
<table>
<thead>
@@ -70,92 +65,11 @@
<tr>
<td>
<span
style="cursor: pointer; padding-left: 0rem;"
>
- 👇
- </span>
- </td>
- <td>
- tanner
- </td>
- <td>
- linsley
- </td>
- <td>
- 29
- </td>
- <td>
- 100
- </td>
- <td>
- In Relationship
- </td>
- <td>
- 50
- </td>
- </tr>
- <tr>
- <td>
- <span
- style="cursor: pointer; padding-left: 2rem;"
- >
- 👇
- </span>
- </td>
- <td>
- tanner
- </td>
- <td>
- linsley
- </td>
- <td>
- 29
- </td>
- <td>
- 100
- </td>
- <td>
- In Relationship
- </td>
- <td>
- 50
- </td>
- </tr>
- <tr>
- <td>
- <span
- style="cursor: pointer; padding-left: 4rem;"
- >
- 👇
- </span>
- </td>
- <td>
- tanner
- </td>
- <td>
- linsley
- </td>
- <td>
- 29
- </td>
- <td>
- 100
- </td>
- <td>
- In Relationship
- </td>
- <td>
- 50
- </td>
- </tr>
- <tr>
- <td>
- <span
- style="cursor: pointer; padding-left: 6rem;"
- >
- 👇
+ 👉
</span>
</td>
<td>
tanner
</td>
@@ -171,273 +85,10 @@
<td>
In Relationship
</td>
<td>
50
- </td>
- </tr>
- <tr>
- <td
- colspan="7"
- >
- <pre>
- <code>
- {
- "values": {
- "firstName": "tanner",
- "lastName": "linsley",
- "age": 29,
- "visits": 100,
- "status": "In Relationship",
- "progress": 50
- }
- }
- </code>
- </pre>
- </td>
- </tr>
- <tr>
- <td>
- <span
- style="cursor: pointer; padding-left: 6rem;"
- >
- 👉
- </span>
- </td>
- <td>
- derek
- </td>
- <td>
- perkins
- </td>
- <td>
- 40
- </td>
- <td>
- 40
- </td>
- <td>
- Single
- </td>
- <td>
- 80
- </td>
- </tr>
- <tr>
- <td>
- <span
- style="cursor: pointer; padding-left: 6rem;"
- >
- 👉
- </span>
- </td>
- <td>
- joe
- </td>
- <td>
- bergevin
- </td>
- <td>
- 45
- </td>
- <td>
- 20
- </td>
- <td>
- Complicated
- </td>
- <td>
- 10
- </td>
- </tr>
- <tr>
- <td>
- <span
- style="cursor: pointer; padding-left: 6rem;"
- >
- 👉
- </span>
- </td>
- <td>
- jaylen
- </td>
- <td>
- linsley
- </td>
- <td>
- 26
- </td>
- <td>
- 99
- </td>
- <td>
- In Relationship
- </td>
- <td>
- 70
- </td>
- </tr>
- <tr>
- <td>
- <span
- style="cursor: pointer; padding-left: 4rem;"
- >
- 👉
- </span>
- </td>
- <td>
- derek
- </td>
- <td>
- perkins
- </td>
- <td>
- 40
- </td>
- <td>
- 40
- </td>
- <td>
- Single
- </td>
- <td>
- 80
- </td>
- </tr>
- <tr>
- <td>
- <span
- style="cursor: pointer; padding-left: 4rem;"
- >
- 👉
- </span>
- </td>
- <td>
- joe
- </td>
- <td>
- bergevin
- </td>
- <td>
- 45
- </td>
- <td>
- 20
- </td>
- <td>
- Complicated
- </td>
- <td>
- 10
- </td>
- </tr>
- <tr>
- <td>
- <span
- style="cursor: pointer; padding-left: 4rem;"
- >
- 👉
- </span>
- </td>
- <td>
- jaylen
- </td>
- <td>
- linsley
- </td>
- <td>
- 26
- </td>
- <td>
- 99
- </td>
- <td>
- In Relationship
- </td>
- <td>
- 70
- </td>
- </tr>
- <tr>
- <td>
- <span
- style="cursor: pointer; padding-left: 2rem;"
- >
- 👉
- </span>
- </td>
- <td>
- derek
- </td>
- <td>
- perkins
- </td>
- <td>
- 40
- </td>
- <td>
- 40
- </td>
- <td>
- Single
- </td>
- <td>
- 80
- </td>
- </tr>
- <tr>
- <td>
- <span
- style="cursor: pointer; padding-left: 2rem;"
- >
- 👉
- </span>
- </td>
- <td>
- joe
- </td>
- <td>
- bergevin
- </td>
- <td>
- 45
- </td>
- <td>
- 20
- </td>
- <td>
- Complicated
- </td>
- <td>
- 10
- </td>
- </tr>
- <tr>
- <td>
- <span
- style="cursor: pointer; padding-left: 2rem;"
- >
- 👉
- </span>
- </td>
- <td>
- jaylen
- </td>
- <td>
- linsley
- </td>
- <td>
- 26
- </td>
- <td>
- 99
- </td>
- <td>
- In Relationship
- </td>
- <td>
- 70
</td>
</tr>
<tr>
<td>
<span
`;

View File

@ -1,766 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders a filterable table 1`] = `
<DocumentFragment>
<table>
<thead>
<tr>
<th
colspan="2"
>
Name
</th>
<th
colspan="4"
>
Info
</th>
</tr>
<tr>
<th
colspan="1"
>
First Name
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Last Name
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Age
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Visits
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Status
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Profile Progress
<input
placeholder="Search..."
value=""
/>
</th>
</tr>
<tr>
<th
colspan="6"
style="text-align: left;"
>
<span>
<input
placeholder="Global search..."
style="font-size: 1.1rem; border: 0px;"
value=""
/>
</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
firstName: tanner
</td>
<td>
lastName: linsley
</td>
<td>
age: 29
</td>
<td>
visits: 100
</td>
<td>
status: In Relationship
</td>
<td>
progress: 50
</td>
</tr>
<tr>
<td>
firstName: derek
</td>
<td>
lastName: perkins
</td>
<td>
age: 40
</td>
<td>
visits: 40
</td>
<td>
status: Single
</td>
<td>
progress: 80
</td>
</tr>
<tr>
<td>
firstName: joe
</td>
<td>
lastName: bergevin
</td>
<td>
age: 45
</td>
<td>
visits: 20
</td>
<td>
status: Complicated
</td>
<td>
progress: 10
</td>
</tr>
<tr>
<td>
firstName: jaylen
</td>
<td>
lastName: linsley
</td>
<td>
age: 26
</td>
<td>
visits: 99
</td>
<td>
status: In Relationship
</td>
<td>
progress: 70
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;
exports[`renders a filterable table 2`] = `
<DocumentFragment>
<table>
<thead>
<tr>
<th
colspan="2"
>
Name
</th>
<th
colspan="4"
>
Info
</th>
</tr>
<tr>
<th
colspan="1"
>
First Name
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Last Name
<input
placeholder="Search..."
value="l"
/>
</th>
<th
colspan="1"
>
Age
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Visits
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Status
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Profile Progress
<input
placeholder="Search..."
value=""
/>
</th>
</tr>
<tr>
<th
colspan="6"
style="text-align: left;"
>
<span>
<input
placeholder="Global search..."
style="font-size: 1.1rem; border: 0px;"
value=""
/>
</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
firstName: tanner
</td>
<td>
lastName: linsley
</td>
<td>
age: 29
</td>
<td>
visits: 100
</td>
<td>
status: In Relationship
</td>
<td>
progress: 50
</td>
</tr>
<tr>
<td>
firstName: jaylen
</td>
<td>
lastName: linsley
</td>
<td>
age: 26
</td>
<td>
visits: 99
</td>
<td>
status: In Relationship
</td>
<td>
progress: 70
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;
exports[`renders a filterable table 3`] = `
<DocumentFragment>
<table>
<thead>
<tr>
<th
colspan="2"
>
Name
</th>
<th
colspan="4"
>
Info
</th>
</tr>
<tr>
<th
colspan="1"
>
First Name
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Last Name
<input
placeholder="Search..."
value="er"
/>
</th>
<th
colspan="1"
>
Age
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Visits
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Status
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Profile Progress
<input
placeholder="Search..."
value=""
/>
</th>
</tr>
<tr>
<th
colspan="6"
style="text-align: left;"
>
<span>
<input
placeholder="Global search..."
style="font-size: 1.1rem; border: 0px;"
value=""
/>
</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
firstName: derek
</td>
<td>
lastName: perkins
</td>
<td>
age: 40
</td>
<td>
visits: 40
</td>
<td>
status: Single
</td>
<td>
progress: 80
</td>
</tr>
<tr>
<td>
firstName: joe
</td>
<td>
lastName: bergevin
</td>
<td>
age: 45
</td>
<td>
visits: 20
</td>
<td>
status: Complicated
</td>
<td>
progress: 10
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;
exports[`renders a filterable table 4`] = `
<DocumentFragment>
<table>
<thead>
<tr>
<th
colspan="2"
>
Name
</th>
<th
colspan="4"
>
Info
</th>
</tr>
<tr>
<th
colspan="1"
>
First Name
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Last Name
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Age
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Visits
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Status
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Profile Progress
<input
placeholder="Search..."
value=""
/>
</th>
</tr>
<tr>
<th
colspan="6"
style="text-align: left;"
>
<span>
<input
placeholder="Global search..."
style="font-size: 1.1rem; border: 0px;"
value=""
/>
</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
firstName: tanner
</td>
<td>
lastName: linsley
</td>
<td>
age: 29
</td>
<td>
visits: 100
</td>
<td>
status: In Relationship
</td>
<td>
progress: 50
</td>
</tr>
<tr>
<td>
firstName: derek
</td>
<td>
lastName: perkins
</td>
<td>
age: 40
</td>
<td>
visits: 40
</td>
<td>
status: Single
</td>
<td>
progress: 80
</td>
</tr>
<tr>
<td>
firstName: joe
</td>
<td>
lastName: bergevin
</td>
<td>
age: 45
</td>
<td>
visits: 20
</td>
<td>
status: Complicated
</td>
<td>
progress: 10
</td>
</tr>
<tr>
<td>
firstName: jaylen
</td>
<td>
lastName: linsley
</td>
<td>
age: 26
</td>
<td>
visits: 99
</td>
<td>
status: In Relationship
</td>
<td>
progress: 70
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;
exports[`renders a filterable table 5`] = `
<DocumentFragment>
<table>
<thead>
<tr>
<th
colspan="2"
>
Name
</th>
<th
colspan="4"
>
Info
</th>
</tr>
<tr>
<th
colspan="1"
>
First Name
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Last Name
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Age
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Visits
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Status
<input
placeholder="Search..."
value=""
/>
</th>
<th
colspan="1"
>
Profile Progress
<input
placeholder="Search..."
value=""
/>
</th>
</tr>
<tr>
<th
colspan="6"
style="text-align: left;"
>
<span>
<input
placeholder="Global search..."
style="font-size: 1.1rem; border: 0px;"
value="li"
/>
</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
firstName: tanner
</td>
<td>
lastName: linsley
</td>
<td>
age: 29
</td>
<td>
visits: 100
</td>
<td>
status: In Relationship
</td>
<td>
progress: 50
</td>
</tr>
<tr>
<td>
firstName: joe
</td>
<td>
lastName: bergevin
</td>
<td>
age: 45
</td>
<td>
visits: 20
</td>
<td>
status: Complicated
</td>
<td>
progress: 10
</td>
</tr>
<tr>
<td>
firstName: jaylen
</td>
<td>
lastName: linsley
</td>
<td>
age: 26
</td>
<td>
visits: 99
</td>
<td>
status: In Relationship
</td>
<td>
progress: 70
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;

View File

@ -1,294 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders a groupable table 1`] = `
Snapshot Diff:
- First value
+ Second value
@@ -19,24 +19,24 @@
>
<span
style="cursor: pointer;"
title="Toggle GroupBy"
>
- 👊
+ 🛑
</span>
- First Name
+ Last Name
</th>
<th
colspan="1"
>
<span
style="cursor: pointer;"
title="Toggle GroupBy"
>
👊
</span>
- Last Name
+ First Name
</th>
<th
colspan="1"
>
<span
@@ -83,86 +83,81 @@
</tr>
</thead>
<tbody>
<tr>
<td>
- firstName: tanner
+ <span
+ style="cursor: pointer;"
+ >
+ 👉
+ </span>
+ lastName: linsley (2)
</td>
<td>
- lastName: linsley
+ 2 Names
</td>
<td>
- age: 29
+ 27.5 (avg)
</td>
<td>
- visits: 100
- </td>
- <td>
- status: In Relationship
- </td>
- <td>
- progress: 50
- </td>
- </tr>
- <tr>
- <td>
- firstName: derek
+ 199 (total)
</td>
<td>
- lastName: perkins
- </td>
- <td>
- age: 40
- </td>
- <td>
- visits: 40
- </td>
- <td>
- status: Single
+ status: null
</td>
<td>
- progress: 80
+ 60 (med)
</td>
</tr>
<tr>
<td>
- firstName: joe
+ <span
+ style="cursor: pointer;"
+ >
+ 👉
+ </span>
+ lastName: perkins (1)
</td>
<td>
- lastName: bergevin
+ 1 Names
</td>
<td>
- age: 45
+ 40 (avg)
</td>
<td>
- visits: 20
+ 40 (total)
</td>
<td>
- status: Complicated
+ status: null
</td>
<td>
- progress: 10
+ 80 (med)
</td>
</tr>
<tr>
<td>
- firstName: jaylen
+ <span
+ style="cursor: pointer;"
+ >
+ 👉
+ </span>
+ lastName: bergevin (1)
</td>
<td>
- lastName: linsley
+ 1 Names
</td>
<td>
- age: 26
+ 45 (avg)
</td>
<td>
- visits: 99
+ 20 (total)
</td>
<td>
- status: In Relationship
+ status: null
</td>
<td>
- progress: 70
+ 10 (med)
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;
exports[`renders a groupable table 2`] = `
Snapshot Diff:
- First value
+ Second value
@@ -1,16 +1,26 @@
<DocumentFragment>
<table>
<thead>
<tr>
<th
- colspan="2"
+ colspan="1"
+ >
+ Name
+ </th>
+ <th
+ colspan="1"
+ >
+ Info
+ </th>
+ <th
+ colspan="1"
>
Name
</th>
<th
- colspan="4"
+ colspan="3"
>
Info
</th>
</tr>
<tr>
@@ -30,35 +40,35 @@
>
<span
style="cursor: pointer;"
title="Toggle GroupBy"
>
- 👊
+ 🛑
</span>
- First Name
+ Visits
</th>
<th
colspan="1"
>
<span
style="cursor: pointer;"
title="Toggle GroupBy"
>
👊
</span>
- Age
+ First Name
</th>
<th
colspan="1"
>
<span
style="cursor: pointer;"
title="Toggle GroupBy"
>
👊
</span>
- Visits
+ Age
</th>
<th
colspan="1"
>
<span
@@ -90,18 +100,16 @@
>
👉
</span>
lastName: linsley (2)
</td>
+ <td />
<td>
2 Names
</td>
<td>
27.5 (avg)
- </td>
- <td>
- 199 (total)
</td>
<td>
status: null
</td>
<td>
@@ -115,20 +123,18 @@
>
👉
</span>
lastName: perkins (1)
</td>
+ <td />
<td>
1 Names
</td>
<td>
40 (avg)
</td>
<td>
- 40 (total)
- </td>
- <td>
status: null
</td>
<td>
80 (med)
</td>
@@ -140,18 +146,16 @@
>
👉
</span>
lastName: bergevin (1)
</td>
+ <td />
<td>
1 Names
</td>
<td>
45 (avg)
- </td>
- <td>
- 20 (total)
</td>
<td>
status: null
</td>
<td>
`;

View File

@ -1,498 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders a paginated table 1`] = `
Snapshot Diff:
- First value
+ Second value
@@ -47,11 +47,11 @@
</tr>
</thead>
<tbody>
<tr>
<td>
- tanner 21
+ tanner 31
</td>
<td>
linsley
</td>
<td>
@@ -67,11 +67,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 22
+ tanner 32
</td>
<td>
linsley
</td>
<td>
@@ -87,11 +87,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 23
+ tanner 33
</td>
<td>
linsley
</td>
<td>
@@ -107,11 +107,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 24
+ tanner 34
</td>
<td>
linsley
</td>
<td>
@@ -127,11 +127,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 25
+ tanner 35
</td>
<td>
linsley
</td>
<td>
@@ -147,11 +147,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 26
+ tanner 36
</td>
<td>
linsley
</td>
<td>
@@ -167,11 +167,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 27
+ tanner 37
</td>
<td>
linsley
</td>
<td>
@@ -187,11 +187,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 28
+ tanner 38
</td>
<td>
linsley
</td>
<td>
@@ -207,11 +207,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 29
+ tanner 39
</td>
<td>
linsley
</td>
<td>
@@ -227,11 +227,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 30
+ tanner 40
</td>
<td>
linsley
</td>
<td>
@@ -269,20 +269,20 @@
</button>
<span>
Page
<strong>
- 3 of 10
+ 4 of 10
</strong>
</span>
<span>
| Go to page:
<input
style="width: 100px;"
type="number"
- value="3"
+ value="4"
/>
</span>
<select>
<option
`;
exports[`renders a paginated table 2`] = `
Snapshot Diff:
- First value
+ Second value
@@ -47,11 +47,11 @@
</tr>
</thead>
<tbody>
<tr>
<td>
- tanner 31
+ tanner 41
</td>
<td>
linsley
</td>
<td>
@@ -67,11 +67,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 32
+ tanner 42
</td>
<td>
linsley
</td>
<td>
@@ -87,11 +87,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 33
+ tanner 43
</td>
<td>
linsley
</td>
<td>
@@ -107,11 +107,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 34
+ tanner 44
</td>
<td>
linsley
</td>
<td>
@@ -127,11 +127,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 35
+ tanner 45
</td>
<td>
linsley
</td>
<td>
@@ -147,11 +147,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 36
+ tanner 46
</td>
<td>
linsley
</td>
<td>
@@ -167,11 +167,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 37
+ tanner 47
</td>
<td>
linsley
</td>
<td>
@@ -187,11 +187,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 38
+ tanner 48
</td>
<td>
linsley
</td>
<td>
@@ -207,11 +207,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 39
+ tanner 49
</td>
<td>
linsley
</td>
<td>
@@ -227,11 +227,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 40
+ tanner 50
</td>
<td>
linsley
</td>
<td>
@@ -269,20 +269,20 @@
</button>
<span>
Page
<strong>
- 4 of 10
+ 5 of 10
</strong>
</span>
<span>
| Go to page:
<input
style="width: 100px;"
type="number"
- value="4"
+ value="5"
/>
</span>
<select>
<option
`;
exports[`renders a paginated table 3`] = `
Snapshot Diff:
- First value
+ Second value
@@ -47,11 +47,11 @@
</tr>
</thead>
<tbody>
<tr>
<td>
- tanner 41
+ tanner 91
</td>
<td>
linsley
</td>
<td>
@@ -67,11 +67,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 42
+ tanner 92
</td>
<td>
linsley
</td>
<td>
@@ -87,11 +87,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 43
+ tanner 93
</td>
<td>
linsley
</td>
<td>
@@ -107,11 +107,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 44
+ tanner 94
</td>
<td>
linsley
</td>
<td>
@@ -127,11 +127,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 45
+ tanner 95
</td>
<td>
linsley
</td>
<td>
@@ -147,11 +147,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 46
+ tanner 96
</td>
<td>
linsley
</td>
<td>
@@ -167,11 +167,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 47
+ tanner 97
</td>
<td>
linsley
</td>
<td>
@@ -187,11 +187,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 48
+ tanner 98
</td>
<td>
linsley
</td>
<td>
@@ -207,11 +207,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 49
+ tanner 99
</td>
<td>
linsley
</td>
<td>
@@ -227,11 +227,11 @@
50
</td>
</tr>
<tr>
<td>
- tanner 50
+ tanner 100
</td>
<td>
linsley
</td>
<td>
@@ -258,31 +258,35 @@
<button>
&lt;
</button>
- <button>
+ <button
+ disabled=""
+ >
&gt;
</button>
- <button>
+ <button
+ disabled=""
+ >
&gt;&gt;
</button>
<span>
Page
<strong>
- 5 of 10
+ 10 of 10
</strong>
</span>
<span>
| Go to page:
<input
style="width: 100px;"
type="number"
- value="5"
+ value="10"
/>
</span>
<select>
<option
`;

File diff suppressed because it is too large Load Diff

View File

@ -1,194 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders a sortable table 1`] = `
Snapshot Diff:
- First value
+ Second value
@@ -20,11 +20,13 @@
colspan="1"
style="cursor: pointer;"
title="Toggle SortBy"
>
First Name
- <span />
+ <span>
+ 🔼
+ </span>
</th>
<th
colspan="1"
style="cursor: pointer;"
title="Toggle SortBy"
@@ -67,66 +69,66 @@
</tr>
</thead>
<tbody>
<tr>
<td>
- firstName: tanner
+ firstName: derek
</td>
<td>
- lastName: linsley
+ lastName: perkins
</td>
<td>
- age: 29
+ age: 40
</td>
<td>
- visits: 100
+ visits: 40
</td>
<td>
- status: In Relationship
+ status: Single
</td>
<td>
- progress: 50
+ progress: 80
</td>
</tr>
<tr>
<td>
- firstName: derek
+ firstName: joe
</td>
<td>
- lastName: perkins
+ lastName: bergevin
</td>
<td>
- age: 40
+ age: 45
</td>
<td>
- visits: 40
+ visits: 20
</td>
<td>
- status: Single
+ status: Complicated
</td>
<td>
- progress: 80
+ progress: 10
</td>
</tr>
<tr>
<td>
- firstName: joe
+ firstName: tanner
</td>
<td>
- lastName: bergevin
+ lastName: linsley
</td>
<td>
- age: 45
+ age: 29
</td>
<td>
- visits: 20
+ visits: 100
</td>
<td>
- status: Complicated
+ status: In Relationship
</td>
<td>
- progress: 10
+ progress: 50
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;
exports[`renders a sortable table 2`] = `
Snapshot Diff:
- First value
+ Second value
@@ -21,11 +21,11 @@
style="cursor: pointer;"
title="Toggle SortBy"
>
First Name
<span>
- 🔼
+ 🔽
</span>
</th>
<th
colspan="1"
style="cursor: pointer;"
@@ -69,26 +69,26 @@
</tr>
</thead>
<tbody>
<tr>
<td>
- firstName: derek
+ firstName: tanner
</td>
<td>
- lastName: perkins
+ lastName: linsley
</td>
<td>
- age: 40
+ age: 29
</td>
<td>
- visits: 40
+ visits: 100
</td>
<td>
- status: Single
+ status: In Relationship
</td>
<td>
- progress: 80
+ progress: 50
</td>
</tr>
<tr>
<td>
firstName: joe
@@ -109,26 +109,26 @@
progress: 10
</td>
</tr>
<tr>
<td>
- firstName: tanner
+ firstName: derek
</td>
<td>
- lastName: linsley
+ lastName: perkins
</td>
<td>
- age: 29
+ age: 40
</td>
<td>
- visits: 100
+ visits: 40
</td>
<td>
- status: In Relationship
+ status: Single
</td>
<td>
- progress: 50
+ progress: 80
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;

View File

@ -136,7 +136,22 @@ function App() {
}
test('renders a table', () => {
const { asFragment } = render(<App />)
const rtl = render(<App />)
expect(asFragment()).toMatchSnapshot()
expect(
rtl.getAllByRole('columnheader').every(d => d.style.position === 'absolute')
).toBe(true)
expect(
rtl.getAllByRole('columnheader').map(d => [d.style.left, d.style.width])
).toStrictEqual([
['0px', '550px'],
['550px', '850px'],
['0px', '250px'],
['250px', '300px'],
['550px', '300px'],
['850px', '150px'],
['1000px', '200px'],
['1200px', '200px'],
])
})

View File

@ -136,7 +136,28 @@ function App() {
}
test('renders a table', () => {
const { asFragment } = render(<App />)
const rtl = render(<App />)
expect(asFragment()).toMatchSnapshot()
expect(
rtl
.getAllByRole('columnheader')
.every(d => d.style.display === 'inline-block')
).toBe(true)
expect(rtl.getAllByRole('row').every(d => d.style.display === 'flex')).toBe(
true
)
expect(
rtl.getAllByRole('columnheader').map(d => d.style.width)
).toStrictEqual([
'550px',
'850px',
'250px',
'300px',
'300px',
'150px',
'200px',
'200px',
])
})

View File

@ -0,0 +1,186 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { useTable } from '../../hooks/useTable'
import { useColumnOrder } from '../useColumnOrder'
const data = [
{
firstName: 'tanner',
lastName: 'linsley',
age: 29,
visits: 100,
status: 'In Relationship',
progress: 50,
},
{
firstName: 'derek',
lastName: 'perkins',
age: 40,
visits: 40,
status: 'Single',
progress: 80,
},
{
firstName: 'joe',
lastName: 'bergevin',
age: 45,
visits: 20,
status: 'Complicated',
progress: 10,
},
{
firstName: 'jaylen',
lastName: 'linsley',
age: 26,
visits: 99,
status: 'In Relationship',
progress: 70,
},
]
function shuffle(arr, mapping) {
if (arr.length !== mapping.length) {
throw new Error()
}
arr = [...arr]
mapping = [...mapping]
const shuffled = []
while (arr.length) {
shuffled.push(arr.splice([mapping.shift()], 1)[0])
}
return shuffled
}
function Table({ columns, data }) {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
visibleColumns,
prepareRow,
setColumnOrder,
state,
} = useTable(
{
columns,
data,
},
useColumnOrder
)
const testColumnOrder = () => {
setColumnOrder(
shuffle(
visibleColumns.map(d => d.id),
[1, 4, 2, 0, 3, 5]
)
)
}
return (
<>
<button onClick={() => testColumnOrder({})}>Randomize Columns</button>
<table {...getTableProps()}>
<thead>
{headerGroups.map((headerGroup, i) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.slice(0, 10).map((row, i) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell, i) => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
<pre>
<code>{JSON.stringify(state, null, 2)}</code>
</pre>
</>
)
}
function App() {
const columns = React.useMemo(
() => [
{
Header: 'Name',
columns: [
{
Header: 'First Name',
accessor: 'firstName',
},
{
Header: 'Last Name',
accessor: 'lastName',
},
],
},
{
Header: 'Info',
columns: [
{
Header: 'Age',
accessor: 'age',
},
{
Header: 'Visits',
accessor: 'visits',
},
{
Header: 'Status',
accessor: 'status',
},
{
Header: 'Profile Progress',
accessor: 'progress',
},
],
},
],
[]
)
return <Table columns={columns} data={data} />
}
test('renders a column-orderable table', () => {
const rtl = render(<App />)
expect(rtl.getAllByRole('columnheader').map(d => d.textContent)).toEqual([
'Name',
'Info',
'First Name',
'Last Name',
'Age',
'Visits',
'Status',
'Profile Progress',
])
fireEvent.click(rtl.getByText('Randomize Columns'))
expect(rtl.getAllByRole('columnheader').map(d => d.textContent)).toEqual([
'Name',
'Info',
'Name',
'Info',
'Last Name',
'Profile Progress',
'Visits',
'First Name',
'Age',
'Status',
])
})

View File

@ -2,47 +2,9 @@ import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { useTable } from '../../hooks/useTable'
import { useExpanded } from '../useExpanded'
import makeTestData from '../../../test-utils/makeTestData'
const makeData = () => [
{
firstName: 'tanner',
lastName: 'linsley',
age: 29,
visits: 100,
status: 'In Relationship',
progress: 50,
},
{
firstName: 'derek',
lastName: 'perkins',
age: 40,
visits: 40,
status: 'Single',
progress: 80,
},
{
firstName: 'joe',
lastName: 'bergevin',
age: 45,
visits: 20,
status: 'Complicated',
progress: 10,
},
{
firstName: 'jaylen',
lastName: 'linsley',
age: 26,
visits: 99,
status: 'In Relationship',
progress: 70,
},
]
const data = makeData()
data[0].subRows = makeData()
data[0].subRows[0].subRows = makeData()
data[0].subRows[0].subRows[0].subRows = makeData()
const data = makeTestData(3, 3, 3)
function Table({ columns: userColumns, data, SubComponent }) {
const {
@ -51,8 +13,7 @@ function Table({ columns: userColumns, data, SubComponent }) {
headerGroups,
rows,
prepareRow,
flatColumns,
state: { expanded },
visibleColumns,
} = useTable(
{
columns: userColumns,
@ -63,9 +24,6 @@ function Table({ columns: userColumns, data, SubComponent }) {
return (
<>
<pre>
<code>{JSON.stringify({ expanded: expanded }, null, 2)}</code>
</pre>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
@ -91,7 +49,7 @@ function Table({ columns: userColumns, data, SubComponent }) {
</tr>
{!row.subRows.length && row.isExpanded ? (
<tr>
<td colSpan={flatColumns.length}>
<td colSpan={visibleColumns.length}>
{SubComponent({ row })}
</td>
</tr>
@ -119,43 +77,14 @@ function App() {
}}
onClick={() => row.toggleExpanded()}
>
{row.isExpanded ? '👇' : '👉'}
{row.isExpanded ? 'Collapse' : 'Expand'} Row {row.id}
</span>
),
},
{
Header: 'Name',
columns: [
{
Header: 'First Name',
accessor: 'firstName',
},
{
Header: 'Last Name',
accessor: 'lastName',
},
],
},
{
Header: 'Info',
columns: [
{
Header: 'Age',
accessor: 'age',
},
{
Header: 'Visits',
accessor: 'visits',
},
{
Header: 'Status',
accessor: 'status',
},
{
Header: 'Profile Progress',
accessor: 'progress',
},
],
Header: 'First Name',
accessor: 'name',
Cell: ({ row: { id } }) => `Row ${id}`,
},
],
[]
@ -165,51 +94,32 @@ function App() {
<Table
columns={columns}
data={data}
SubComponent={({ row }) => (
<pre>
<code>{JSON.stringify({ values: row.values }, null, 2)}</code>
</pre>
)}
SubComponent={({ row }) => <span>SubComponent: {row.id}</span>}
/>
)
}
test('renders an expandable table', () => {
const { getAllByText, asFragment } = render(<App />)
const rtl = render(<App />)
let expandButtons = getAllByText('👉')
rtl.getByText('Row 0')
const before = asFragment()
fireEvent.click(rtl.getByText('Expand Row 0'))
fireEvent.click(expandButtons[0])
rtl.getByText('Row 0.0')
rtl.getByText('Row 0.1')
rtl.getByText('Row 0.2')
const after1 = asFragment()
fireEvent.click(rtl.getByText('Expand Row 0.1'))
expandButtons = getAllByText('👉')
fireEvent.click(expandButtons[0])
rtl.getByText('Row 0.1.2')
const after2 = asFragment()
fireEvent.click(rtl.getByText('Expand Row 0.1.2'))
expandButtons = getAllByText('👉')
fireEvent.click(expandButtons[0])
rtl.getByText('SubComponent: 0.1.2')
const after3 = asFragment()
fireEvent.click(rtl.getByText('Collapse Row 0'))
expandButtons = getAllByText('👉')
fireEvent.click(expandButtons[0])
const after4 = asFragment()
expandButtons = getAllByText('👇')
expandButtons.reverse().forEach(button => {
fireEvent.click(button)
})
const after5 = asFragment()
expect(before).toMatchDiffSnapshot(after1)
expect(after1).toMatchDiffSnapshot(after2)
expect(after2).toMatchDiffSnapshot(after3)
expect(after3).toMatchDiffSnapshot(after4)
expect(after4).toMatchDiffSnapshot(after5)
expect(rtl.queryByText('SubComponent: 0.1.2')).toBe(null)
rtl.getByText('Expand Row 0')
})

View File

@ -1,10 +1,10 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { render, fireEvent } from '../../../test-utils/react-testing'
import { useTable } from '../../hooks/useTable'
import { useFilters } from '../useFilters'
import { useGlobalFilter } from '../useGlobalFilter'
const data = [
const makeData = () => [
{
firstName: 'tanner',
lastName: 'linsley',
@ -52,79 +52,8 @@ const defaultColumn = {
),
}
function Table({ columns, data }) {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
flatColumns,
state,
setGlobalFilter,
} = useTable(
{
columns,
data,
defaultColumn,
},
useFilters,
useGlobalFilter
)
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>
{column.render('Header')}
{column.canFilter ? column.render('Filter') : null}
</th>
))}
</tr>
))}
<tr>
<th
colSpan={flatColumns.length}
style={{
textAlign: 'left',
}}
>
<span>
<input
value={state.globalFilter || ''}
onChange={e => {
setGlobalFilter(e.target.value || undefined) // Set undefined to remove the filter entirely
}}
placeholder={`Global search...`}
style={{
fontSize: '1.1rem',
border: '0',
}}
/>
</span>
</th>
</tr>
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
)}
</tbody>
</table>
)
}
function App() {
const [data, setData] = React.useState(makeData)
const columns = React.useMemo(
() => [
{
@ -165,38 +94,152 @@ function App() {
[]
)
return <Table columns={columns} data={data} />
}
test('renders a filterable table', () => {
const { getAllByPlaceholderText, getByPlaceholderText, asFragment } = render(
<App />
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
visibleColumns,
state,
setGlobalFilter,
} = useTable(
{
columns,
data,
defaultColumn,
},
useFilters,
useGlobalFilter
)
const globalFilterInput = getByPlaceholderText('Global search...')
const filterInputs = getAllByPlaceholderText('Search...')
const reset = () => setData(makeData())
const beforeFilter = asFragment()
return (
<>
<button onClick={reset}>Reset Data</button>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>
{column.render('Header')}
{column.canFilter ? column.render('Filter') : null}
</th>
))}
</tr>
))}
<tr>
<th
colSpan={visibleColumns.length}
style={{
textAlign: 'left',
}}
>
<span>
<input
value={state.globalFilter || ''}
onChange={e => {
setGlobalFilter(e.target.value || undefined) // Set undefined to remove the filter entirely
}}
placeholder={`Global search...`}
style={{
fontSize: '1.1rem',
border: '0',
}}
/>
</span>
</th>
</tr>
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
)}
</tbody>
</table>
</>
)
}
test('renders a filterable table', async () => {
const rendered = render(<App />)
const resetButton = rendered.getByText('Reset Data')
const globalFilterInput = rendered.getByPlaceholderText('Global search...')
const filterInputs = rendered.getAllByPlaceholderText('Search...')
fireEvent.change(filterInputs[1], { target: { value: 'l' } })
const afterFilter1 = asFragment()
expect(
rendered
.queryAllByRole('row')
.slice(3)
.map(row => Array.from(row.children)[0].textContent)
).toEqual(['firstName: tanner', 'firstName: jaylen'])
fireEvent.change(filterInputs[1], { target: { value: 'er' } })
expect(
rendered
.queryAllByRole('row')
.slice(3)
.map(row => Array.from(row.children)[0].textContent)
).toEqual(['firstName: derek', 'firstName: joe'])
const afterFilter2 = asFragment()
fireEvent.change(filterInputs[2], { target: { value: 'nothing' } })
expect(
rendered
.queryAllByRole('row')
.slice(3)
.map(row => Array.from(row.children)[0].textContent)
).toEqual([])
fireEvent.change(filterInputs[1], { target: { value: '' } })
expect(
rendered
.queryAllByRole('row')
.slice(3)
.map(row => Array.from(row.children)[0].textContent)
).toEqual([])
const afterFilter3 = asFragment()
fireEvent.change(filterInputs[2], { target: { value: '' } })
expect(
rendered
.queryAllByRole('row')
.slice(3)
.map(row => Array.from(row.children)[0].textContent)
).toEqual([
'firstName: tanner',
'firstName: derek',
'firstName: joe',
'firstName: jaylen',
])
fireEvent.change(globalFilterInput, { target: { value: 'li' } })
expect(
rendered
.queryAllByRole('row')
.slice(3)
.map(row => Array.from(row.children)[0].textContent)
).toEqual(['firstName: tanner', 'firstName: joe', 'firstName: jaylen'])
const afterFilter4 = asFragment()
expect(beforeFilter).toMatchSnapshot()
expect(afterFilter1).toMatchSnapshot()
expect(afterFilter2).toMatchSnapshot()
expect(afterFilter3).toMatchSnapshot()
expect(afterFilter4).toMatchSnapshot()
fireEvent.click(resetButton)
expect(
rendered
.queryAllByRole('row')
.slice(3)
.map(row => Array.from(row.children)[0].textContent)
).toEqual([
'firstName: tanner',
'firstName: derek',
'firstName: joe',
'firstName: jaylen',
])
})

View File

@ -0,0 +1,157 @@
import React from 'react'
import { useTable } from '../../hooks/useTable'
import { useFlexLayout } from '../useFlexLayout'
import { render } from '../../../test-utils/react-testing'
const data = [
{
firstName: 'tanner',
lastName: 'linsley',
age: 29,
visits: 100,
status: 'In Relationship',
progress: 50,
},
{
firstName: 'derek',
lastName: 'perkins',
age: 30,
visits: 40,
status: 'Single',
progress: 80,
},
{
firstName: 'joe',
lastName: 'bergevin',
age: 45,
visits: 20,
status: 'Complicated',
progress: 10,
},
]
const defaultColumn = {
Cell: ({ cell: { value }, column: { id } }) => `${id}: ${value}`,
width: 200,
minWidth: 100,
maxWidth: 300,
}
function Table({ columns, data }) {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable(
{
columns,
data,
defaultColumn,
},
useFlexLayout
)
return (
<div {...getTableProps()} className="table">
<div>
{headerGroups.map(headerGroup => (
<div {...headerGroup.getHeaderGroupProps()} className="row">
{headerGroup.headers.map(column => (
<div {...column.getHeaderProps()} className="cell header">
{column.render('Header')}
</div>
))}
</div>
))}
</div>
<div {...getTableBodyProps()}>
{rows.map(
(row, i) =>
prepareRow(row) || (
<div {...row.getRowProps()} className="row">
{row.cells.map(cell => {
return (
<div {...cell.getCellProps()} className="cell">
{cell.render('Cell')}
</div>
)
})}
</div>
)
)}
</div>
</div>
)
}
function App() {
const columns = React.useMemo(
() => [
{
Header: 'Name',
columns: [
{
Header: 'First Name',
accessor: 'firstName',
width: 250,
},
{
Header: 'Last Name',
accessor: 'lastName',
width: 350,
},
],
},
{
Header: 'Info',
columns: [
{
Header: 'Age',
accessor: 'age',
minWidth: 300,
},
{
Header: 'Visits',
accessor: 'visits',
maxWidth: 150,
},
{
Header: 'Status',
accessor: 'status',
},
{
Header: 'Profile Progress',
accessor: 'progress',
},
],
},
],
[]
)
return <Table columns={columns} data={data} />
}
test('renders a table', () => {
const rendered = render(<App />)
const [headerRow, firstRow] = rendered.queryAllByRole('row')
expect(headerRow.getAttribute('style')).toEqual(
'display: flex; flex: 1 0 auto; min-width: 800px;'
)
expect(
Array.from(firstRow.children).map(d => d.getAttribute('style'))
).toEqual([
'box-sizing: border-box; flex: 250 0 auto; min-width: 100px; width: 250px;',
'box-sizing: border-box; flex: 300 0 auto; min-width: 100px; width: 300px;',
'box-sizing: border-box; flex: 300 0 auto; min-width: 300px; width: 300px;',
'box-sizing: border-box; flex: 150 0 auto; min-width: 100px; width: 150px;',
'box-sizing: border-box; flex: 200 0 auto; min-width: 100px; width: 200px;',
'box-sizing: border-box; flex: 200 0 auto; min-width: 100px; width: 200px;',
])
})

View File

@ -1,5 +1,5 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { render, fireEvent } from '../../../test-utils/react-testing'
import { useTable } from '../../hooks/useTable'
import { useGroupBy } from '../useGroupBy'
import { useExpanded } from '../useExpanded'
@ -29,6 +29,14 @@ const data = [
status: 'Complicated',
progress: 10,
},
{
firstName: 'joe',
lastName: 'dirt',
age: 20,
visits: 5,
status: 'Complicated',
progress: 97,
},
{
firstName: 'jaylen',
lastName: 'linsley',
@ -82,7 +90,7 @@ function Table({ columns, data }) {
{column.canGroupBy ? (
// If the column can be grouped, let's add a toggle
<span {...column.getGroupByToggleProps()}>
{column.isGrouped ? '🛑' : '👊'}
{column.isGrouped ? 'Ungroup' : 'Group'} {column.id}
</span>
) : null}
{column.render('Header')}
@ -113,7 +121,7 @@ function Table({ columns, data }) {
</>
) : cell.isAggregated ? (
cell.render('Aggregated')
) : cell.isRepeatedValue ? null : (
) : cell.isPlaceholder ? null : (
cell.render('Cell')
)}
</td>
@ -127,11 +135,14 @@ function Table({ columns, data }) {
)
}
function roundedMedian(values) {
let min = values[0] || ''
let max = values[0] || ''
// This is a custom aggregator that
// takes in an array of leaf values and
// returns the rounded median
function roundedMedian(leafValues) {
let min = leafValues[0] || 0
let max = leafValues[0] || 0
values.forEach(value => {
leafValues.forEach(value => {
min = Math.min(min, value)
max = Math.max(max, value)
})
@ -148,14 +159,16 @@ function App() {
{
Header: 'First Name',
accessor: 'firstName',
aggregate: ['sum', 'count'],
Aggregated: ({ cell: { value } }) => `${value} Names`,
aggregate: 'count',
Aggregated: ({ cell: { value } }) =>
`First Name Aggregated: ${value} Names`,
},
{
Header: 'Last Name',
accessor: 'lastName',
aggregate: ['sum', 'uniqueCount'],
Aggregated: ({ cell: { value } }) => `${value} Unique Names`,
aggregate: 'uniqueCount',
Aggregated: ({ cell: { value } }) =>
`Last Name Aggregated: ${value} Unique Names`,
},
],
},
@ -166,23 +179,62 @@ function App() {
Header: 'Age',
accessor: 'age',
aggregate: 'average',
Aggregated: ({ cell: { value } }) => `${value} (avg)`,
Aggregated: ({ cell: { value } }) =>
`Age Aggregated: ${value} (avg)`,
},
{
Header: 'Visits',
accessor: 'visits',
aggregate: 'sum',
Aggregated: ({ cell: { value } }) => `${value} (total)`,
Aggregated: ({ cell: { value } }) =>
`Visits Aggregated: ${value} (total)`,
},
{
Header: 'Min Visits',
id: 'minVisits',
accessor: 'visits',
aggregate: 'min',
Aggregated: ({ cell: { value } }) =>
`Visits Aggregated: ${value} (min)`,
},
{
Header: 'Max Visits',
id: 'maxVisits',
accessor: 'visits',
aggregate: 'max',
Aggregated: ({ cell: { value } }) =>
`Visits Aggregated: ${value} (max)`,
},
{
Header: 'Min/Max Visits',
id: 'minMaxVisits',
accessor: 'visits',
aggregate: 'minMax',
Aggregated: ({ cell: { value } }) =>
`Visits Aggregated: ${value} (minMax)`,
},
{
Header: 'Status',
accessor: 'status',
aggregate: 'unique',
Aggregated: ({ cell: { value } }) =>
`Visits Aggregated: ${value.join(', ')} (unique)`,
},
{
Header: 'Profile Progress',
Header: 'Profile Progress (Median)',
accessor: 'progress',
id: 'progress',
aggregate: 'median',
Aggregated: ({ cell: { value } }) =>
`Process Aggregated: ${value} (median)`,
},
{
Header: 'Profile Progress (Rounded Median)',
accessor: 'progress',
id: 'progressRounded',
aggregate: roundedMedian,
Aggregated: ({ cell: { value } }) => `${value} (med)`,
Aggregated: ({ cell: { value } }) =>
`Process Aggregated: ${value} (rounded median)`,
},
],
},
@ -194,20 +246,26 @@ function App() {
}
test('renders a groupable table', () => {
const { getAllByText, asFragment } = render(<App />)
const rendered = render(<App />)
const groupByButtons = getAllByText('👊')
fireEvent.click(rendered.getByText('Group lastName'))
const beforeGrouping = asFragment()
rendered.getByText('lastName: linsley (2)')
fireEvent.click(groupByButtons[1])
fireEvent.click(rendered.getByText('Group visits'))
const afterGrouping1 = asFragment()
fireEvent.click(rendered.getByText('Ungroup lastName'))
fireEvent.click(groupByButtons[3])
rendered.getByText('visits: 100 (1)')
const afterGrouping2 = asFragment()
fireEvent.click(rendered.getByText('Ungroup visits'))
expect(beforeGrouping).toMatchDiffSnapshot(afterGrouping1)
expect(afterGrouping1).toMatchDiffSnapshot(afterGrouping2)
fireEvent.click(rendered.getByText('Group firstName'))
rendered.getByText('firstName: tanner (1)')
rendered.debugDiff(false)
fireEvent.click(rendered.getByText('Group age'))
rendered.getByText('Last Name Aggregated: 2 Unique Names')
})

View File

@ -1,10 +1,10 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { render, fireEvent } from '../../../test-utils/react-testing'
import { useTable } from '../../hooks/useTable'
import { usePagination } from '../usePagination'
const data = [...new Array(100)].map((d, i) => ({
firstName: 'tanner ' + (i + 1),
const data = [...new Array(1000)].map((d, i) => ({
firstName: `tanner ${i + 1}`,
lastName: 'linsley',
age: 29,
visits: 100,
@ -100,6 +100,7 @@ function Table({ columns, data }) {
onChange={e => {
setPageSize(Number(e.target.value))
}}
data-testid="page-size-select"
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
@ -157,23 +158,30 @@ function App() {
}
test('renders a paginated table', () => {
const { getByText, asFragment } = render(<App />)
const rendered = render(<App />)
const fragment1 = asFragment()
expect(rendered.queryAllByRole('cell')[0].textContent).toEqual('tanner 21')
fireEvent.click(getByText('>'))
fireEvent.click(rendered.getByText('>'))
expect(rendered.queryAllByRole('cell')[0].textContent).toEqual('tanner 31')
const fragment2 = asFragment()
fireEvent.click(rendered.getByText('>'))
expect(rendered.queryAllByRole('cell')[0].textContent).toEqual('tanner 41')
fireEvent.click(getByText('>'))
fireEvent.click(rendered.getByText('>>'))
expect(rendered.queryAllByRole('cell')[0].textContent).toEqual('tanner 991')
const fragment3 = asFragment()
fireEvent.click(rendered.getByText('<<'))
expect(rendered.queryAllByRole('cell')[0].textContent).toEqual('tanner 1')
fireEvent.click(getByText('>>'))
fireEvent.change(rendered.getByTestId('page-size-select'), {
target: { value: 30 },
})
const fragment4 = asFragment()
expect(fragment1).toMatchDiffSnapshot(fragment2)
expect(fragment2).toMatchDiffSnapshot(fragment3)
expect(fragment3).toMatchDiffSnapshot(fragment4)
expect(
rendered
.queryAllByRole('row')
.slice(2)
.reverse()[0].children[0].textContent
).toEqual('tanner 30')
})

View File

@ -0,0 +1,234 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { useTable } from '../../hooks/useTable'
import { useBlockLayout } from '../useBlockLayout'
import { useResizeColumns } from '../useResizeColumns'
const data = [
{
firstName: 'tanner',
lastName: 'linsley',
age: 29,
visits: 100,
status: 'In Relationship',
progress: 50,
},
{
firstName: 'derek',
lastName: 'perkins',
age: 40,
visits: 40,
status: 'Single',
progress: 80,
},
{
firstName: 'joe',
lastName: 'bergevin',
age: 45,
visits: 20,
status: 'Complicated',
progress: 10,
},
]
function Table({ columns, data }) {
const defaultColumn = React.useMemo(
() => ({
minWidth: 30,
width: 150,
maxWidth: 400,
}),
[]
)
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable(
{
columns,
data,
defaultColumn,
},
useBlockLayout,
useResizeColumns
)
return (
<div {...getTableProps()} className="table">
<div>
{headerGroups.map(headerGroup => (
<div {...headerGroup.getHeaderGroupProps()} className="tr">
{headerGroup.headers.map(column => (
<div {...column.getHeaderProps()} className="th">
{column.render('Header')}
{/* Use column.getResizerProps to hook up the events correctly */}
<div
{...column.getResizerProps()}
className={`resizer${column.isResizing ? ' isResizing' : ''}`}
/>
</div>
))}
</div>
))}
</div>
<div {...getTableBodyProps()}>
{rows.map((row, i) => {
prepareRow(row)
return (
<div {...row.getRowProps()} className="tr">
{row.cells.map(cell => {
return (
<div {...cell.getCellProps()} className="td">
{cell.render('Cell')}
</div>
)
})}
</div>
)
})}
</div>
</div>
)
}
function App() {
const columns = React.useMemo(
() => [
{
Header: 'Name',
columns: [
{
Header: 'First Name',
accessor: 'firstName',
},
{
Header: 'Last Name',
accessor: 'lastName',
},
],
},
{
Header: 'Info',
columns: [
{
Header: 'Age',
accessor: 'age',
width: 50,
},
{
Header: 'Visits',
accessor: 'visits',
width: 60,
},
{
Header: 'Status',
accessor: 'status',
},
{
Header: 'Profile Progress',
accessor: 'progress',
},
],
},
],
[]
)
return <Table columns={columns} data={data} />
}
const start = 20
const move = 100
const end = 100
const sizesBefore = [
'300px',
'410px',
'150px',
'150px',
'50px',
'60px',
'150px',
'150px',
]
const sizesAfter = [
'300px',
'490px',
'150px',
'150px',
'59.75609756097561px',
'71.70731707317073px',
'179.26829268292684px',
'179.26829268292684px',
]
test('table can be resized by a mouse', () => {
const rtl = render(<App />)
const infoResizer = rtl
.getAllByRole('separator')
.find(d => d.previousSibling.textContent === 'Info')
expect(rtl.getAllByRole('columnheader').map(d => d.style.width)).toEqual(
sizesBefore
)
fireEvent.mouseDown(infoResizer, { clientX: start })
fireEvent.mouseMove(infoResizer, { clientX: move })
fireEvent.mouseUp(infoResizer, { clientX: end })
expect(rtl.getAllByRole('columnheader').map(d => d.style.width)).toEqual(
sizesAfter
)
})
test('table can be resized by a touch device', () => {
const rtl = render(<App />)
const infoResizer = rtl
.getAllByRole('separator')
.find(d => d.previousSibling.textContent === 'Info')
expect(rtl.getAllByRole('columnheader').map(d => d.style.width)).toEqual(
sizesBefore
)
fireEvent.touchStart(infoResizer, { touches: [{ clientX: start }] })
fireEvent.touchMove(infoResizer, { touches: [{ clientX: move }] })
fireEvent.touchEnd(infoResizer, { touches: [{ clientX: end }] })
expect(rtl.getAllByRole('columnheader').map(d => d.style.width)).toEqual(
sizesAfter
)
})
test('table can not be resized with multiple touches', () => {
const rtl = render(<App />)
const infoResizer = rtl
.getAllByRole('separator')
.find(d => d.previousSibling.textContent === 'Info')
expect(rtl.getAllByRole('columnheader').map(d => d.style.width)).toEqual(
sizesBefore
)
fireEvent.touchStart(infoResizer, {
touches: [{ clientX: start }, { clientX: start }],
})
fireEvent.touchMove(infoResizer, {
touches: [{ clientX: move }, { clientX: move }],
})
fireEvent.touchEnd(infoResizer, {
touches: [{ clientX: end }, { clientX: end }],
})
expect(rtl.getAllByRole('columnheader').map(d => d.style.width)).toEqual(
sizesBefore
)
})

View File

@ -84,7 +84,7 @@ function Table({ columns, data }) {
useRowSelect,
useExpanded,
hooks => {
hooks.flatColumns.push(columns => [
hooks.visibleColumns.push(columns => [
// Let's make a column for selection
{
id: 'selection',
@ -142,12 +142,6 @@ function Table({ columns, data }) {
)}
</tbody>
</table>
<p>Selected Rows: {Object.keys(selectedRowIds).length}</p>
<pre>
<code>
{JSON.stringify({ selectedRowIds: selectedRowIds }, null, 2)}
</code>
</pre>
</>
)
}
@ -170,11 +164,13 @@ function App() {
() => [
{
id: 'selectedStatus',
Cell: ({ row }) => (
<div>
Row {row.id} {row.isSelected ? 'Selected' : 'Not Selected'}
</div>
),
Cell: ({ row }) =>
row.isSelected ? (
<div>
<div>Selected</div>
<div>Row {row.id}</div>
</div>
) : null,
},
{
Header: 'Name',
@ -218,43 +214,43 @@ function App() {
}
test('renders a table with selectable rows', () => {
const { getByLabelText, getAllByLabelText, asFragment } = render(<App />)
const rtl = render(<App />)
const fragment1 = asFragment()
fireEvent.click(rtl.getByLabelText('Select All'))
fireEvent.click(getByLabelText('Select All'))
expect(rtl.getAllByText('Selected').length).toBe(24)
const fragment2 = asFragment()
fireEvent.click(rtl.getAllByLabelText('Select Row')[2])
fireEvent.click(getByLabelText('Select All'))
expect(rtl.queryAllByText('Selected').length).toBe(23)
const fragment3 = asFragment()
fireEvent.click(rtl.getByLabelText('Select All'))
fireEvent.click(getAllByLabelText('Select Row')[0])
fireEvent.click(getAllByLabelText('Select Row')[2])
expect(rtl.queryAllByText('Selected').length).toBe(24)
const fragment4 = asFragment()
fireEvent.click(rtl.getByLabelText('Select All'))
fireEvent.click(getAllByLabelText('Select Row')[2])
expect(rtl.queryAllByText('Selected').length).toBe(0)
const fragment5 = asFragment()
fireEvent.click(rtl.getAllByLabelText('Select Row')[0])
fireEvent.click(rtl.getAllByLabelText('Select Row')[2])
fireEvent.click(getAllByLabelText('Select Row')[3])
rtl.getByText('Row 0')
rtl.getByText('Row 2')
const fragment6 = asFragment()
fireEvent.click(getAllByLabelText('Select Row')[4])
fireEvent.click(rtl.getAllByLabelText('Select Row')[2])
const fragment7 = asFragment()
expect(rtl.queryByText('Row 2')).toBeNull()
fireEvent.click(getAllByLabelText('Select Row')[4])
fireEvent.click(rtl.getAllByLabelText('Select Row')[3])
const fragment8 = asFragment()
rtl.queryByText('Row 3')
expect(fragment1).toMatchDiffSnapshot(fragment2)
expect(fragment2).toMatchDiffSnapshot(fragment3)
expect(fragment3).toMatchDiffSnapshot(fragment4)
expect(fragment4).toMatchDiffSnapshot(fragment5)
expect(fragment5).toMatchDiffSnapshot(fragment6)
expect(fragment6).toMatchDiffSnapshot(fragment7)
expect(fragment7).toMatchDiffSnapshot(fragment8)
fireEvent.click(rtl.getAllByLabelText('Select Row')[4])
rtl.queryByText('Row 4')
fireEvent.click(rtl.getAllByLabelText('Select Row')[4])
expect(rtl.queryByText('Row 4')).toBeNull()
})

View File

@ -0,0 +1,185 @@
import React from 'react'
import { render, fireEvent } from '../../../test-utils/react-testing'
import { useTable } from '../../hooks/useTable'
import { useRowState } from '../useRowState'
import { useGlobalFilter } from '../useGlobalFilter'
const data = [
{
firstName: 'tanner',
lastName: 'linsley',
age: 29,
visits: 100,
status: 'In Relationship',
progress: 50,
},
{
firstName: 'derek',
lastName: 'perkins',
age: 40,
visits: 40,
status: 'Single',
progress: 80,
},
{
firstName: 'joe',
lastName: 'bergevin',
age: 45,
visits: 20,
status: 'Complicated',
progress: 10,
},
{
firstName: 'jaylen',
lastName: 'linsley',
age: 26,
visits: 99,
status: 'In Relationship',
progress: 70,
},
]
const defaultColumn = {
Cell: ({ column, cell, row }) => (
<div>
Row {row.id} Cell {column.id} Count {cell.state.count}{' '}
<button
onClick={() => cell.setState(old => ({ ...old, count: old.count + 1 }))}
>
Row {row.id} Cell {column.id} Toggle
</button>
</div>
),
}
function Table({ columns, data }) {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable(
{
columns,
data,
defaultColumn,
initialRowStateAccessor: () => ({ count: 0 }),
initialCellStateAccessor: () => ({ count: 0 }),
},
useRowState,
useGlobalFilter
)
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
<td>
<pre>Row Count {row.state.count}</pre>
<button
onClick={() =>
row.setState(old => ({
...old,
count: old.count + 1,
}))
}
>
Row {row.id} Toggle
</button>
</td>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
)}
</tbody>
</table>
)
}
function App() {
const columns = React.useMemo(
() => [
{
Header: 'Name',
columns: [
{
Header: 'First Name',
accessor: 'firstName',
},
{
Header: 'Last Name',
accessor: 'lastName',
},
],
},
{
Header: 'Info',
columns: [
{
Header: 'Age',
accessor: 'age',
},
{
Header: 'Visits',
accessor: 'visits',
},
{
Header: 'Status',
accessor: 'status',
},
{
Header: 'Profile Progress',
accessor: 'progress',
},
],
},
],
[]
)
return <Table columns={columns} data={data} />
}
test('renders a filterable table', () => {
const rendered = render(<App />)
fireEvent.click(rendered.getByText('Row 1 Toggle'))
fireEvent.click(rendered.getByText('Row 1 Toggle'))
rendered.getByText('Row Count 2')
fireEvent.click(rendered.getByText('Row 1 Cell firstName Toggle'))
rendered.getByText('Row 1 Cell firstName Count 1')
fireEvent.click(rendered.getByText('Row 2 Cell lastName Toggle'))
fireEvent.click(rendered.getByText('Row 2 Cell lastName Toggle'))
rendered.getByText('Row 2 Cell lastName Count 2')
fireEvent.click(rendered.getByText('Row 3 Cell age Toggle'))
fireEvent.click(rendered.getByText('Row 3 Cell age Toggle'))
fireEvent.click(rendered.getByText('Row 3 Cell age Toggle'))
rendered.getByText('Row 3 Cell age Count 3')
fireEvent.click(rendered.getByText('Row 1 Toggle'))
fireEvent.click(rendered.getByText('Row 1 Toggle'))
rendered.getByText('Row Count 4')
})

View File

@ -1,5 +1,5 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { render, fireEvent } from '../../../test-utils/react-testing'
import { useTable } from '../../hooks/useTable'
import { useSortBy } from '../useSortBy'
@ -10,7 +10,7 @@ const data = [
age: 29,
visits: 100,
status: 'In Relationship',
progress: 50,
progress: 80,
},
{
firstName: 'derek',
@ -61,9 +61,9 @@ function Table({ columns, data }) {
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
{/* Add a sort direction indicator */}
<span>
{column.isSorted ? (column.isSortedDesc ? ' 🔽' : ' 🔼') : ''}
</span>
{column.isSorted
? (column.isSortedDesc ? ' 🔽' : ' 🔼') + column.sortedIndex
: ''}
</th>
))}
</tr>
@ -130,18 +130,42 @@ function App() {
}
test('renders a sortable table', () => {
const { getByText, asFragment } = render(<App />)
const rendered = render(<App />)
const beforeSort = asFragment()
fireEvent.click(rendered.getByText('First Name'))
rendered.getByText('First Name 🔼0')
expect(
rendered
.queryAllByRole('row')
.slice(2)
.map(d => d.children[0].textContent)
).toEqual(['firstName: derek', 'firstName: joe', 'firstName: tanner'])
fireEvent.click(getByText('First Name'))
fireEvent.click(rendered.getByText('First Name 🔼0'))
rendered.getByText('First Name 🔽0')
expect(
rendered
.queryAllByRole('row')
.slice(2)
.map(d => d.children[0].textContent)
).toEqual(['firstName: tanner', 'firstName: joe', 'firstName: derek'])
const afterSort1 = asFragment()
fireEvent.click(rendered.getByText('Profile Progress'))
rendered.getByText('Profile Progress 🔼0')
expect(
rendered
.queryAllByRole('row')
.slice(2)
.map(d => d.children[0].textContent)
).toEqual(['firstName: joe', 'firstName: tanner', 'firstName: derek'])
fireEvent.click(getByText('First Name'))
const afterSort2 = asFragment()
expect(beforeSort).toMatchDiffSnapshot(afterSort1)
expect(afterSort1).toMatchDiffSnapshot(afterSort2)
fireEvent.click(rendered.getByText('First Name'), { shiftKey: true })
rendered.getByText('Profile Progress 🔼0')
rendered.getByText('First Name 🔼1')
expect(
rendered
.queryAllByRole('row')
.slice(2)
.map(d => d.children[0].textContent)
).toEqual(['firstName: joe', 'firstName: derek', 'firstName: tanner'])
})

View File

@ -1,4 +1,4 @@
import { ensurePluginOrder } from '../utils'
import { ensurePluginOrder } from '../publicUtils'
const cellStyles = {
position: 'absolute',

View File

@ -1,6 +1,6 @@
import React from 'react'
import { functionalUpdate, actions } from '../utils'
import { functionalUpdate, actions } from '../publicUtils'
// Actions
actions.resetColumnOrder = 'resetColumnOrder'
@ -8,10 +8,10 @@ actions.setColumnOrder = 'setColumnOrder'
export const useColumnOrder = hooks => {
hooks.stateReducers.push(reducer)
hooks.flatColumnsDeps.push((deps, { instance }) => {
hooks.visibleColumnsDeps.push((deps, { instance }) => {
return [...deps, instance.state.columnOrder]
})
hooks.flatColumns.push(flatColumns)
hooks.visibleColumns.push(visibleColumns)
hooks.useInstance.push(useInstance)
}
@ -40,7 +40,7 @@ function reducer(state, action, previousState, instance) {
}
}
function flatColumns(
function visibleColumns(
columns,
{
instance: {

View File

@ -1,13 +1,14 @@
import React from 'react'
import { expandRows } from '../utils'
import {
actions,
makePropGetter,
expandRows,
useMountedLayoutEffect,
useGetLatest,
} from '../utils'
import { useConsumeHookGetter, functionalUpdate } from '../publicUtils'
actions,
functionalUpdate,
useMountedLayoutEffect,
makePropGetter,
} from '../publicUtils'
// Actions
actions.toggleExpanded = 'toggleExpanded'
@ -19,6 +20,7 @@ export const useExpanded = hooks => {
hooks.getExpandedToggleProps = [defaultGetExpandedToggleProps]
hooks.stateReducers.push(reducer)
hooks.useInstance.push(useInstance)
hooks.prepareRow.push(prepareRow)
}
useExpanded.pluginName = 'useExpanded'
@ -61,7 +63,7 @@ function reducer(state, action, previousState, instance) {
}
if (action.type === actions.toggleExpanded) {
const { id, expanded: setExpanded } = action
const { id, value: setExpanded } = action
const exists = state.expanded[id]
const shouldExist =
@ -94,7 +96,6 @@ function useInstance(instance) {
manualExpandedKey = 'expanded',
paginateExpandedRows = true,
expandSubRows = true,
hooks,
autoResetExpanded = true,
state: { expanded },
dispatch,
@ -109,27 +110,10 @@ function useInstance(instance) {
}
}, [dispatch, data])
const toggleExpanded = (id, expanded) => {
dispatch({ type: actions.toggleExpanded, id, expanded })
const toggleExpanded = (id, value) => {
dispatch({ type: actions.toggleExpanded, id, value })
}
// use reference to avoid memory leak in #1608
const getInstance = useGetLatest(instance)
const getExpandedTogglePropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getExpandedToggleProps'
)
hooks.prepareRow.push(row => {
row.toggleExpanded = set => instance.toggleExpanded(row.id, set)
row.getExpandedToggleProps = makePropGetter(getExpandedTogglePropsHooks(), {
instance: getInstance(),
row,
})
})
const expandedRows = React.useMemo(() => {
if (paginateExpandedRows) {
return expandRows(rows, { manualExpandedKey, expanded, expandSubRows })
@ -151,6 +135,18 @@ function useInstance(instance) {
})
}
function prepareRow(row, { instance: { getHooks }, instance }) {
row.toggleExpanded = set => instance.toggleExpanded(row.id, set)
row.getExpandedToggleProps = makePropGetter(
getHooks().getExpandedToggleProps,
{
instance,
row,
}
)
}
function findExpandedDepth(expanded) {
let maxDepth = 0

View File

@ -1,14 +1,18 @@
import React from 'react'
import {
actions,
getFirstDefined,
getFilterMethod,
useMountedLayoutEffect,
functionalUpdate,
useGetLatest,
shouldAutoRemoveFilter,
} from '../utils'
import {
actions,
useGetLatest,
functionalUpdate,
useMountedLayoutEffect,
} from '../publicUtils'
import * as filterTypes from '../filterTypes'
// Actions
@ -40,9 +44,9 @@ function reducer(state, action, previousState, instance) {
if (action.type === actions.setFilter) {
const { columnId, filterValue } = action
const { flatColumns, userFilterTypes } = instance
const { allColumns, userFilterTypes } = instance
const column = flatColumns.find(d => d.id === columnId)
const column = allColumns.find(d => d.id === columnId)
if (!column) {
throw new Error(
@ -91,13 +95,13 @@ function reducer(state, action, previousState, instance) {
if (action.type === actions.setAllFilters) {
const { filters } = action
const { flatColumns, filterTypes: userFilterTypes } = instance
const { allColumns, filterTypes: userFilterTypes } = instance
return {
...state,
// Filter out undefined values
filters: functionalUpdate(filters, state.filters).filter(filter => {
const column = flatColumns.find(d => d.id === filter.id)
const column = allColumns.find(d => d.id === filter.id)
const filterMethod = getFilterMethod(
column.filter,
userFilterTypes || {},
@ -118,7 +122,7 @@ function useInstance(instance) {
data,
rows,
flatRows,
flatColumns,
allColumns,
filterTypes: userFilterTypes,
manualFilters,
defaultCanFilter = false,
@ -139,7 +143,7 @@ function useInstance(instance) {
})
}
flatColumns.forEach(column => {
allColumns.forEach(column => {
const {
id,
accessor,
@ -179,7 +183,7 @@ function useInstance(instance) {
filteredRows = filters.reduce(
(filteredSoFar, { id: columnId, value: filterValue }) => {
// Find the filters column
const column = flatColumns.find(d => d.id === columnId)
const column = allColumns.find(d => d.id === columnId)
if (!column) {
return filteredSoFar
@ -237,12 +241,12 @@ function useInstance(instance) {
}
return [filterRows(rows), filteredFlatRows]
}, [manualFilters, filters, rows, flatRows, flatColumns, userFilterTypes])
}, [manualFilters, filters, rows, flatRows, allColumns, userFilterTypes])
React.useMemo(() => {
// Now that each filtered column has it's partially filtered rows,
// lets assign the final filtered rows to all of the other columns
const nonFilteredColumns = flatColumns.filter(
const nonFilteredColumns = allColumns.filter(
column => !filters.find(d => d.id === column.id)
)
@ -252,7 +256,7 @@ function useInstance(instance) {
column.preFilteredRows = filteredRows
column.filteredRows = filteredRows
})
}, [filteredRows, filters, flatColumns])
}, [filteredRows, filters, allColumns])
const getAutoResetFilters = useGetLatest(autoResetFilters)

View File

@ -1,14 +1,15 @@
import React from 'react'
import { getFilterMethod, shouldAutoRemoveFilter } from '../utils'
import {
actions,
getFilterMethod,
useMountedLayoutEffect,
functionalUpdate,
useGetLatest,
shouldAutoRemoveFilter,
ensurePluginOrder,
} from '../utils'
useGetLatest,
} from '../publicUtils'
import * as filterTypes from '../filterTypes'
// Actions
@ -60,7 +61,7 @@ function useInstance(instance) {
data,
rows,
flatRows,
flatColumns,
allColumns,
filterTypes: userFilterTypes,
globalFilter,
manualGlobalFilter,
@ -106,7 +107,7 @@ function useInstance(instance) {
const filterRows = filteredRows => {
return filterMethod(
filteredRows,
flatColumns.map(d => d.id),
allColumns.map(d => d.id),
globalFilterValue
).map(row => {
filteredFlatRows.push(row)
@ -128,7 +129,7 @@ function useInstance(instance) {
userFilterTypes,
rows,
flatRows,
flatColumns,
allColumns,
globalFilterValue,
])

View File

@ -1,16 +1,17 @@
import React from 'react'
import * as aggregations from '../aggregations'
import { getFirstDefined, flattenBy } from '../utils'
import {
actions,
makePropGetter,
defaultGroupByFn,
getFirstDefined,
ensurePluginOrder,
useMountedLayoutEffect,
useGetLatest,
} from '../utils'
import { useConsumeHookGetter } from '../publicUtils'
} from '../publicUtils'
// Actions
actions.resetGroupBy = 'resetGroupBy'
@ -19,12 +20,13 @@ actions.toggleGroupBy = 'toggleGroupBy'
export const useGroupBy = hooks => {
hooks.getGroupByToggleProps = [defaultGetGroupByToggleProps]
hooks.stateReducers.push(reducer)
hooks.flatColumnsDeps.push((deps, { instance }) => [
hooks.visibleColumnsDeps.push((deps, { instance }) => [
...deps,
instance.state.groupBy,
])
hooks.flatColumns.push(flatColumns)
hooks.visibleColumns.push(visibleColumns)
hooks.useInstance.push(useInstance)
hooks.prepareRow.push(prepareRow)
}
useGroupBy.pluginName = 'useGroupBy'
@ -62,12 +64,14 @@ function reducer(state, action, previousState, instance) {
}
if (action.type === actions.toggleGroupBy) {
const { columnId, toggle } = action
const { columnId, value: setGroupBy } = action
const resolvedToggle =
typeof toggle !== 'undefined' ? toggle : !state.groupBy.includes(columnId)
const resolvedGroupBy =
typeof setGroupBy !== 'undefined'
? setGroupBy
: !state.groupBy.includes(columnId)
if (resolvedToggle) {
if (resolvedGroupBy) {
return {
...state,
groupBy: [...state.groupBy, columnId],
@ -81,8 +85,8 @@ function reducer(state, action, previousState, instance) {
}
}
function flatColumns(
flatColumns,
function visibleColumns(
columns,
{
instance: {
state: { groupBy },
@ -93,18 +97,19 @@ function flatColumns(
// before the headers are built
const groupByColumns = groupBy
.map(g => flatColumns.find(col => col.id === g))
.filter(col => !!col)
const nonGroupByColumns = flatColumns.filter(col => !groupBy.includes(col.id))
.map(g => columns.find(col => col.id === g))
.filter(Boolean)
flatColumns = [...groupByColumns, ...nonGroupByColumns]
const nonGroupByColumns = columns.filter(col => !groupBy.includes(col.id))
flatColumns.forEach(column => {
columns = [...groupByColumns, ...nonGroupByColumns]
columns.forEach(column => {
column.isGrouped = groupBy.includes(column.id)
column.groupedIndex = groupBy.indexOf(column.id)
})
return flatColumns
return columns
}
const defaultUserAggregations = {}
@ -114,12 +119,11 @@ function useInstance(instance) {
data,
rows,
flatRows,
flatColumns,
allColumns,
flatHeaders,
groupByFn = defaultGroupByFn,
manualGroupBy,
aggregations: userAggregations = defaultUserAggregations,
hooks,
plugins,
state: { groupBy },
dispatch,
@ -127,13 +131,14 @@ function useInstance(instance) {
manaulGroupBy,
disableGroupBy,
defaultCanGroupBy,
getHooks,
} = instance
ensurePluginOrder(plugins, [], 'useGroupBy', ['useSortBy', 'useExpanded'])
const getInstance = useGetLatest(instance)
flatColumns.forEach(column => {
allColumns.forEach(column => {
const {
accessor,
defaultGroupBy: defaultColumnGroupBy,
@ -142,11 +147,17 @@ function useInstance(instance) {
column.canGroupBy = accessor
? getFirstDefined(
column.canGroupBy,
columnDisableGroupBy === true ? false : undefined,
disableGroupBy === true ? false : undefined,
true
)
: getFirstDefined(defaultColumnGroupBy, defaultCanGroupBy, false)
: getFirstDefined(
column.canGroupBy,
defaultColumnGroupBy,
defaultCanGroupBy,
false
)
if (column.canGroupBy) {
column.toggleGroupBy = () => instance.toggleGroupBy(column.id)
@ -155,34 +166,17 @@ function useInstance(instance) {
column.Aggregated = column.Aggregated || column.Cell
})
const toggleGroupBy = (columnId, toggle) => {
dispatch({ type: actions.toggleGroupBy, columnId, toggle })
const toggleGroupBy = (columnId, value) => {
dispatch({ type: actions.toggleGroupBy, columnId, value })
}
const getGroupByTogglePropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getGroupByToggleProps'
)
flatHeaders.forEach(header => {
header.getGroupByToggleProps = makePropGetter(
getGroupByTogglePropsHooks(),
getHooks().getGroupByToggleProps,
{ instance: getInstance(), header }
)
})
hooks.prepareRow.push(row => {
row.allCells.forEach(cell => {
// Grouped cells are in the groupBy and the pivot cell for the row
cell.isGrouped = cell.column.isGrouped && cell.column.id === row.groupByID
// Repeated cells are any columns in the groupBy that are not grouped
cell.isRepeatedValue = !cell.isGrouped && cell.column.isGrouped
// Aggregated cells are not grouped, not repeated, but still have subRows
cell.isAggregated =
!cell.isGrouped && !cell.isRepeatedValue && row.canExpand
})
})
const [groupedRows, groupedFlatRows] = React.useMemo(() => {
if (manualGroupBy || !groupBy.length) {
return [rows, flatRows]
@ -190,62 +184,75 @@ function useInstance(instance) {
// Ensure that the list of filtered columns exist
const existingGroupBy = groupBy.filter(g =>
flatColumns.find(col => col.id === g)
allColumns.find(col => col.id === g)
)
// Find the columns that can or are aggregating
// Uses each column to aggregate rows into a single value
const aggregateRowsToValues = (rows, isAggregated) => {
const aggregateRowsToValues = (leafRows, groupedRows, depth) => {
const values = {}
flatColumns.forEach(column => {
allColumns.forEach(column => {
// Don't aggregate columns that are in the groupBy
if (existingGroupBy.includes(column.id)) {
values[column.id] = rows[0] ? rows[0].values[column.id] : null
values[column.id] = groupedRows[0]
? groupedRows[0].values[column.id]
: null
return
}
const columnValues = rows.map(d => d.values[column.id])
// Get the columnValues to aggregate
const groupedValues = groupedRows.map(row => row.values[column.id])
let aggregator = column.aggregate
// Get the columnValues to aggregate
const leafValues = leafRows.map(row => {
let columnValue = row.values[column.id]
if (Array.isArray(aggregator)) {
if (aggregator.length !== 2) {
console.info({ column })
throw new Error(
`React Table: Complex aggregators must have 2 values, eg. aggregate: ['sum', 'count']. More info above...`
)
if (!depth && column.aggregatedValue) {
const aggregateValueFn =
typeof column.aggregateValue === 'function'
? column.aggregateValue
: userAggregations[column.aggregateValue] ||
aggregations[column.aggregateValue]
if (!aggregateValueFn) {
console.info({ column })
throw new Error(
`React Table: Invalid column.aggregateValue option for column listed above`
)
}
columnValue = aggregateValueFn(columnValue, row, column)
}
if (isAggregated) {
aggregator = aggregator[1]
} else {
aggregator = aggregator[0]
}
}
return columnValue
})
// Aggregate the values
let aggregateFn =
typeof aggregator === 'function'
? aggregator
: userAggregations[aggregator] || aggregations[aggregator]
typeof column.aggregate === 'function'
? column.aggregate
: userAggregations[column.aggregate] ||
aggregations[column.aggregate]
if (aggregateFn) {
values[column.id] = aggregateFn(columnValues, rows, isAggregated)
} else if (aggregator) {
values[column.id] = aggregateFn(leafValues, groupedValues)
} else if (column.aggregate) {
console.info({ column })
throw new Error(
`React Table: Invalid aggregate option for column listed above`
`React Table: Invalid column.aggregate option for column listed above`
)
} else {
values[column.id] = null
}
})
return values
}
let groupedFlatRows = []
// Recursively group the data
const groupRecursively = (rows, depth = 0, parentId) => {
const groupUpRecursively = (rows, depth = 0, parentId) => {
// This is the last level, just return the rows
if (depth === existingGroupBy.length) {
return rows
@ -254,20 +261,23 @@ function useInstance(instance) {
const columnId = existingGroupBy[depth]
// Group the rows together for this level
let groupedRows = groupByFn(rows, columnId)
let rowGroupsMap = groupByFn(rows, columnId)
// Recurse to sub rows before aggregation
groupedRows = Object.entries(groupedRows).map(
([groupByVal, subRows], index) => {
// Peform aggregations for each group
const aggregatedGroupedRows = Object.entries(rowGroupsMap).map(
([groupByVal, groupedRows], index) => {
let id = `${columnId}:${groupByVal}`
id = parentId ? `${parentId}>${id}` : id
subRows = groupRecursively(subRows, depth + 1, id)
// First, Recurse to group sub rows before aggregation
const subRows = groupUpRecursively(groupedRows, depth + 1, id)
const values = aggregateRowsToValues(
subRows,
depth < existingGroupBy.length
)
// Flatten the leaf rows of the rows in this group
const leafRows = depth
? flattenBy(groupedRows, 'leafRows')
: groupedRows
const values = aggregateRowsToValues(leafRows, groupedRows, depth)
const row = {
id,
@ -276,6 +286,7 @@ function useInstance(instance) {
groupByVal,
values,
subRows,
leafRows,
depth,
index,
}
@ -286,10 +297,10 @@ function useInstance(instance) {
}
)
return groupedRows
return aggregatedGroupedRows
}
const groupedRows = groupRecursively(rows)
const groupedRows = groupUpRecursively(rows)
// Assign the new data
return [groupedRows, groupedFlatRows]
@ -298,7 +309,7 @@ function useInstance(instance) {
groupBy,
rows,
flatRows,
flatColumns,
allColumns,
userAggregations,
groupByFn,
])
@ -321,3 +332,14 @@ function useInstance(instance) {
toggleGroupBy,
})
}
function prepareRow(row) {
row.allCells.forEach(cell => {
// Grouped cells are in the groupBy and the pivot cell for the row
cell.isGrouped = cell.column.isGrouped && cell.column.id === row.groupByID
// Placeholder cells are any columns in the groupBy that are not grouped
cell.isPlaceholder = !cell.isGrouped && cell.column.isGrouped
// Aggregated cells are not grouped, not repeated, but still have subRows
cell.isAggregated = !cell.isGrouped && !cell.isPlaceholder && row.canExpand
})
}

View File

@ -5,11 +5,12 @@ import React from 'react'
import {
actions,
ensurePluginOrder,
expandRows,
functionalUpdate,
useMountedLayoutEffect,
useGetLatest,
} from '../utils'
} from '../publicUtils'
import { expandRows } from '../utils'
const pluginName = 'usePagination'

View File

@ -0,0 +1,297 @@
/* istanbul ignore file */
import {
actions,
makePropGetter,
ensurePluginOrder,
useMountedLayoutEffect,
useGetLatest,
} from '../publicUtils'
import { flattenColumns, getFirstDefined } from '../utils'
// Actions
actions.resetPivot = 'resetPivot'
actions.togglePivot = 'togglePivot'
export const usePivotColumns = hooks => {
hooks.getPivotToggleProps = [defaultGetPivotToggleProps]
hooks.stateReducers.push(reducer)
hooks.useInstanceAfterData.push(useInstanceAfterData)
hooks.allColumns.push(allColumns)
hooks.accessValue.push(accessValue)
hooks.materializedColumns.push(materializedColumns)
hooks.materializedColumnsDeps.push(materializedColumnsDeps)
hooks.visibleColumns.push(visibleColumns)
hooks.visibleColumnsDeps.push(visibleColumnsDeps)
hooks.useInstance.push(useInstance)
hooks.prepareRow.push(prepareRow)
}
usePivotColumns.pluginName = 'usePivotColumns'
const defaultPivotColumns = []
const defaultGetPivotToggleProps = (props, { header }) => [
props,
{
onClick: header.canPivot
? e => {
e.persist()
header.togglePivot()
}
: undefined,
style: {
cursor: header.canPivot ? 'pointer' : undefined,
},
title: 'Toggle Pivot',
},
]
// Reducer
function reducer(state, action, previousState, instance) {
if (action.type === actions.init) {
return {
pivotColumns: defaultPivotColumns,
...state,
}
}
if (action.type === actions.resetPivot) {
return {
...state,
pivotColumns: instance.initialState.pivotColumns || defaultPivotColumns,
}
}
if (action.type === actions.togglePivot) {
const { columnId, value: setPivot } = action
const resolvedPivot =
typeof setPivot !== 'undefined'
? setPivot
: !state.pivotColumns.includes(columnId)
if (resolvedPivot) {
return {
...state,
pivotColumns: [...state.pivotColumns, columnId],
}
}
return {
...state,
pivotColumns: state.pivotColumns.filter(d => d !== columnId),
}
}
}
function useInstanceAfterData(instance) {
instance.allColumns.forEach(column => {
column.isPivotSource = instance.state.pivotColumns.includes(column.id)
})
}
function allColumns(columns, { instance }) {
columns.forEach(column => {
column.isPivotSource = instance.state.pivotColumns.includes(column.id)
column.uniqueValues = new Set()
})
return columns
}
function accessValue(value, { column }) {
if (column.uniqueValues && typeof value !== 'undefined') {
column.uniqueValues.add(value)
}
return value
}
function materializedColumns(materialized, { instance }) {
const { allColumns, state } = instance
if (!state.pivotColumns.length || !state.groupBy || !state.groupBy.length) {
return materialized
}
const pivotColumns = state.pivotColumns
.map(id => allColumns.find(d => d.id === id))
.filter(Boolean)
const sourceColumns = allColumns.filter(
d =>
!d.isPivotSource &&
!state.groupBy.includes(d.id) &&
!state.pivotColumns.includes(d.id)
)
const buildPivotColumns = (depth = 0, parent, pivotFilters = []) => {
const pivotColumn = pivotColumns[depth]
if (!pivotColumn) {
return sourceColumns.map(sourceColumn => {
// TODO: We could offer support here for renesting pivoted
// columns inside copies of their header groups. For now,
// that seems like it would be (1) overkill on nesting, considering
// you already get nesting for every pivot level and (2)
// really hard. :)
return {
...sourceColumn,
canPivot: false,
isPivoted: true,
parent,
depth: depth,
id: `${parent ? `${parent.id}.${sourceColumn.id}` : sourceColumn.id}`,
accessor: (originalRow, i, row) => {
if (pivotFilters.every(filter => filter(row))) {
return row.values[sourceColumn.id]
}
},
}
})
}
const uniqueValues = Array.from(pivotColumn.uniqueValues).sort()
return uniqueValues.map(uniqueValue => {
const columnGroup = {
...pivotColumn,
Header:
pivotColumn.PivotHeader || typeof pivotColumn.header === 'string'
? `${pivotColumn.Header}: ${uniqueValue}`
: uniqueValue,
isPivotGroup: true,
parent,
depth,
id: parent
? `${parent.id}.${pivotColumn.id}.${uniqueValue}`
: `${pivotColumn.id}.${uniqueValue}`,
pivotValue: uniqueValue,
}
columnGroup.columns = buildPivotColumns(depth + 1, columnGroup, [
...pivotFilters,
row => row.values[pivotColumn.id] === uniqueValue,
])
return columnGroup
})
}
const newMaterialized = flattenColumns(buildPivotColumns())
return [...materialized, ...newMaterialized]
}
function materializedColumnsDeps(
deps,
{
instance: {
state: { pivotColumns, groupBy },
},
}
) {
return [...deps, pivotColumns, groupBy]
}
function visibleColumns(visibleColumns, { instance: { state } }) {
visibleColumns = visibleColumns.filter(d => !d.isPivotSource)
if (state.pivotColumns.length && state.groupBy && state.groupBy.length) {
visibleColumns = visibleColumns.filter(
column => column.isGrouped || column.isPivoted
)
}
return visibleColumns
}
function visibleColumnsDeps(deps, { instance }) {
return [...deps, instance.state.pivotColumns, instance.state.groupBy]
}
function useInstance(instance) {
const {
columns,
allColumns,
flatHeaders,
// pivotFn = defaultPivotFn,
// manualPivot,
getHooks,
plugins,
dispatch,
autoResetPivot = true,
manaulPivot,
disablePivot,
defaultCanPivot,
} = instance
ensurePluginOrder(plugins, ['useGroupBy'], 'usePivotColumns', [
'useSortBy',
'useExpanded',
])
const getInstance = useGetLatest(instance)
allColumns.forEach(column => {
const {
accessor,
defaultPivot: defaultColumnPivot,
disablePivot: columnDisablePivot,
} = column
column.canPivot = accessor
? getFirstDefined(
column.canPivot,
columnDisablePivot === true ? false : undefined,
disablePivot === true ? false : undefined,
true
)
: getFirstDefined(
column.canPivot,
defaultColumnPivot,
defaultCanPivot,
false
)
if (column.canPivot) {
column.togglePivot = () => instance.togglePivot(column.id)
}
column.Aggregated = column.Aggregated || column.Cell
})
const togglePivot = (columnId, value) => {
dispatch({ type: actions.togglePivot, columnId, value })
}
flatHeaders.forEach(header => {
header.getPivotToggleProps = makePropGetter(
getHooks().getPivotToggleProps,
{
instance: getInstance(),
header,
}
)
})
const getAutoResetPivot = useGetLatest(autoResetPivot)
useMountedLayoutEffect(() => {
if (getAutoResetPivot()) {
dispatch({ type: actions.resetPivot })
}
}, [dispatch, manaulPivot ? null : columns])
Object.assign(instance, {
togglePivot,
})
}
function prepareRow(row) {
row.allCells.forEach(cell => {
// Grouped cells are in the pivotColumns and the pivot cell for the row
cell.isPivoted = cell.column.isPivoted
})
}

View File

@ -1,11 +1,11 @@
import {
actions,
defaultColumn,
getFirstDefined,
makePropGetter,
useGetLatest,
} from '../utils'
import { useConsumeHookGetter } from '../publicUtils'
} from '../publicUtils'
import { getFirstDefined } from '../utils'
// Default Column
defaultColumn.canResize = true
@ -118,6 +118,7 @@ const defaultGetResizerProps = (props, { instance, header }) => {
cursor: 'ew-resize',
},
draggable: false,
role: 'separator',
},
]
}
@ -193,16 +194,12 @@ const useInstanceBeforeDimensions = instance => {
const {
flatHeaders,
disableResizing,
getHooks,
state: { columnResizing },
} = instance
const getInstance = useGetLatest(instance)
const getResizerPropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getResizerProps'
)
flatHeaders.forEach(header => {
const canResize = getFirstDefined(
header.disableResizing === true ? false : undefined,
@ -215,7 +212,7 @@ const useInstanceBeforeDimensions = instance => {
header.isResizing = columnResizing.isResizingColumn === header.id
if (canResize) {
header.getResizerProps = makePropGetter(getResizerPropsHooks(), {
header.getResizerProps = makePropGetter(getHooks().getResizerProps, {
instance: getInstance(),
header,
})

View File

@ -6,8 +6,7 @@ import {
ensurePluginOrder,
useGetLatest,
useMountedLayoutEffect,
useConsumeHookGetter,
} from '../utils'
} from '../publicUtils'
const pluginName = 'useRowSelect'
@ -20,8 +19,8 @@ export const useRowSelect = hooks => {
hooks.getToggleRowSelectedProps = [defaultGetToggleRowSelectedProps]
hooks.getToggleAllRowsSelectedProps = [defaultGetToggleAllRowsSelectedProps]
hooks.stateReducers.push(reducer)
hooks.useRows.push(useRows)
hooks.useInstance.push(useInstance)
hooks.prepareRow.push(prepareRow)
}
useRowSelect.pluginName = pluginName
@ -86,11 +85,11 @@ function reducer(state, action, previousState, instance) {
}
if (action.type === actions.toggleAllRowsSelected) {
const { selected } = action
const { value: setSelected } = action
const { isAllRowsSelected, flatRowsById } = instance
const selectAll =
typeof selected !== 'undefined' ? selected : !isAllRowsSelected
typeof setSelected !== 'undefined' ? setSelected : !isAllRowsSelected
if (selectAll) {
const selectedRowIds = {}
@ -112,7 +111,7 @@ function reducer(state, action, previousState, instance) {
}
if (action.type === actions.toggleRowSelected) {
const { id, selected } = action
const { id, value: setSelected } = action
const { flatGroupedRowsById } = instance
// Join the ids of deep rows
@ -120,7 +119,8 @@ function reducer(state, action, previousState, instance) {
// in a flat object
const row = flatGroupedRowsById[id]
const isSelected = row.isSelected
const shouldExist = typeof selected !== 'undefined' ? selected : !isSelected
const shouldExist =
typeof setSelected !== 'undefined' ? setSelected : !isSelected
if (isSelected === shouldExist) {
return state
@ -153,34 +153,11 @@ function reducer(state, action, previousState, instance) {
}
}
function useRows(rows, { instance }) {
const {
state: { selectedRowIds },
} = instance
instance.selectedFlatRows = React.useMemo(() => {
const selectedFlatRows = []
rows.forEach(row => {
const isSelected = getRowIsSelected(row, selectedRowIds)
row.isSelected = !!isSelected
row.isSomeSelected = isSelected === null
if (isSelected) {
selectedFlatRows.push(row)
}
})
return selectedFlatRows
}, [rows, selectedRowIds])
return rows
}
function useInstance(instance) {
const {
data,
hooks,
rows,
getHooks,
plugins,
flatRows,
autoResetSelectedRows = true,
@ -209,6 +186,22 @@ function useInstance(instance) {
return [all, grouped]
}, [flatRows])
const selectedFlatRows = React.useMemo(() => {
const selectedFlatRows = []
rows.forEach(row => {
const isSelected = getRowIsSelected(row, selectedRowIds)
row.isSelected = !!isSelected
row.isSomeSelected = isSelected === null
if (isSelected) {
selectedFlatRows.push(row)
}
})
return selectedFlatRows
}, [rows, selectedRowIds])
let isAllRowsSelected = Boolean(
Object.keys(flatRowsById).length && Object.keys(selectedRowIds).length
)
@ -227,41 +220,23 @@ function useInstance(instance) {
}
}, [dispatch, data])
const toggleAllRowsSelected = selected =>
dispatch({ type: actions.toggleAllRowsSelected, selected })
const toggleAllRowsSelected = value =>
dispatch({ type: actions.toggleAllRowsSelected, value })
const toggleRowSelected = (id, selected) =>
dispatch({ type: actions.toggleRowSelected, id, selected })
const toggleRowSelected = (id, value) =>
dispatch({ type: actions.toggleRowSelected, id, value })
const getInstance = useGetLatest(instance)
const getToggleAllRowsSelectedPropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getToggleAllRowsSelectedProps'
)
const getToggleAllRowsSelectedProps = makePropGetter(
getToggleAllRowsSelectedPropsHooks(),
getHooks().getToggleAllRowsSelectedProps,
{ instance: getInstance() }
)
const getToggleRowSelectedPropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getToggleRowSelectedProps'
)
hooks.prepareRow.push(row => {
row.toggleRowSelected = set => toggleRowSelected(row.id, set)
row.getToggleRowSelectedProps = makePropGetter(
getToggleRowSelectedPropsHooks(),
{ instance: getInstance(), row }
)
})
Object.assign(instance, {
flatRowsById,
flatGroupedRowsById,
selectedFlatRows,
toggleRowSelected,
toggleAllRowsSelected,
getToggleAllRowsSelectedProps,
@ -269,6 +244,15 @@ function useInstance(instance) {
})
}
function prepareRow(row, { instance }) {
row.toggleRowSelected = set => instance.toggleRowSelected(row.id, set)
row.getToggleRowSelectedProps = makePropGetter(
instance.getHooks().getToggleRowSelectedProps,
{ instance: instance, row }
)
}
function getRowIsSelected(row, selectedRowIds) {
if (selectedRowIds[row.id]) {
return true

View File

@ -5,20 +5,31 @@ import {
functionalUpdate,
useMountedLayoutEffect,
useGetLatest,
} from '../utils'
} from '../publicUtils'
const defaultInitialRowStateAccessor = originalRow => ({})
const defaultInitialCellStateAccessor = originalRow => ({})
// Actions
actions.setRowState = 'setRowState'
actions.setCellState = 'setCellState'
actions.resetRowState = 'resetRowState'
export const useRowState = hooks => {
hooks.stateReducers.push(reducer)
hooks.useInstance.push(useInstance)
hooks.prepareRow.push(prepareRow)
}
useRowState.pluginName = 'useRowState'
function reducer(state, action, previousState, instance) {
const {
initialRowStateAccessor = defaultInitialRowStateAccessor,
initialCellStateAccessor = defaultInitialCellStateAccessor,
rowsById,
} = instance
if (action.type === actions.init) {
return {
rowState: {},
@ -34,82 +45,75 @@ function reducer(state, action, previousState, instance) {
}
if (action.type === actions.setRowState) {
const { id, value } = action
const { rowId, value } = action
const oldRowState =
typeof state.rowState[rowId] !== 'undefined'
? state.rowState[rowId]
: initialRowStateAccessor(rowsById[rowId].original)
return {
...state,
rowState: {
...state.rowState,
[id]: functionalUpdate(value, state.rowState[id] || {}),
[rowId]: functionalUpdate(value, oldRowState),
},
}
}
if (action.type === actions.setCellState) {
const { rowId, columnId, value } = action
const oldRowState =
typeof state.rowState[rowId] !== 'undefined'
? state.rowState[rowId]
: initialRowStateAccessor(rowsById[rowId].original)
const oldCellState =
typeof oldRowState?.cellState?.[columnId] !== 'undefined'
? oldRowState.cellState[columnId]
: initialCellStateAccessor(rowsById[rowId].original)
return {
...state,
rowState: {
...state.rowState,
[rowId]: {
...oldRowState,
cellState: {
...(oldRowState.cellState || {}),
[columnId]: functionalUpdate(value, oldCellState),
},
},
},
}
}
}
function useInstance(instance) {
const {
hooks,
initialRowStateAccessor,
autoResetRowState = true,
state: { rowState },
data,
dispatch,
} = instance
const { autoResetRowState = true, data, dispatch } = instance
const setRowState = React.useCallback(
(id, value, columnId) =>
(rowId, value) =>
dispatch({
type: actions.setRowState,
id,
rowId,
value,
columnId,
}),
[dispatch]
)
const setCellState = React.useCallback(
(rowPath, columnId, value) => {
return setRowState(
rowPath,
old => {
return {
...old,
cellState: {
...old.cellState,
[columnId]: functionalUpdate(
value,
(old.cellState || {})[columnId] || {}
),
},
}
},
columnId
)
},
[setRowState]
(rowId, columnId, value) =>
dispatch({
type: actions.setCellState,
rowId,
columnId,
value,
}),
[dispatch]
)
hooks.prepareRow.push(row => {
if (row.original) {
row.state =
(typeof rowState[row.id] !== 'undefined'
? rowState[row.id]
: initialRowStateAccessor && initialRowStateAccessor(row)) || {}
row.setState = updater => {
return setRowState(row.id, updater)
}
row.cells.forEach(cell => {
cell.state = row.state.cellState || {}
cell.setState = updater => {
return setCellState(row.id, cell.column.id, updater)
}
})
}
})
const getAutoResetRowState = useGetLatest(autoResetRowState)
useMountedLayoutEffect(() => {
@ -123,3 +127,37 @@ function useInstance(instance) {
setCellState,
})
}
function prepareRow(row, { instance }) {
const {
initialRowStateAccessor = defaultInitialRowStateAccessor,
initialCellStateAccessor = defaultInitialCellStateAccessor,
state: { rowState },
} = instance
if (row.original) {
row.state =
typeof rowState[row.id] !== 'undefined'
? rowState[row.id]
: initialRowStateAccessor(row.original)
row.setState = updater => {
return instance.setRowState(row.id, updater)
}
row.cells.forEach(cell => {
if (!row.state.cellState) {
row.state.cellState = {}
}
cell.state =
typeof row.state.cellState[cell.column.id] !== 'undefined'
? row.state.cellState[cell.column.id]
: initialCellStateAccessor(row.original)
cell.setState = updater => {
return instance.setCellState(row.id, cell.column.id, updater)
}
})
}
}

View File

@ -5,13 +5,12 @@ import {
ensurePluginOrder,
defaultColumn,
makePropGetter,
useConsumeHookGetter,
getFirstDefined,
defaultOrderByFn,
isFunction,
useGetLatest,
useMountedLayoutEffect,
} from '../utils'
} from '../publicUtils'
import { getFirstDefined, isFunction } from '../utils'
import * as sortTypes from '../sortTypes'
@ -84,7 +83,7 @@ function reducer(state, action, previousState, instance) {
const { columnId, desc, multi } = action
const {
flatColumns,
allColumns,
disableMultiSort,
disableSortRemove,
disableMultiRemove,
@ -94,7 +93,7 @@ function reducer(state, action, previousState, instance) {
const { sortBy } = state
// Find the column for this columnId
const column = flatColumns.find(d => d.id === columnId)
const column = allColumns.find(d => d.id === columnId)
const { sortDescFirst } = column
// Find any existing sortBy for this column
@ -181,7 +180,7 @@ function useInstance(instance) {
const {
data,
rows,
flatColumns,
allColumns,
orderByFn = defaultOrderByFn,
sortTypes: userSortTypes,
manualSortBy,
@ -191,6 +190,7 @@ function useInstance(instance) {
state: { sortBy },
dispatch,
plugins,
getHooks,
autoResetSortBy = true,
} = instance
@ -204,11 +204,6 @@ function useInstance(instance) {
// use reference to avoid memory leak in #1608
const getInstance = useGetLatest(instance)
const getSortByTogglePropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getSortByToggleProps'
)
// Add the getSortByToggleProps method to columns and headers
flatHeaders.forEach(column => {
const {
@ -237,10 +232,13 @@ function useInstance(instance) {
}
}
column.getSortByToggleProps = makePropGetter(getSortByTogglePropsHooks(), {
instance: getInstance(),
column,
})
column.getSortByToggleProps = makePropGetter(
getHooks().getSortByToggleProps,
{
instance: getInstance(),
column,
}
)
const columnSort = sortBy.find(d => d.id === id)
column.isSorted = !!columnSort
@ -255,7 +253,7 @@ function useInstance(instance) {
// Filter out sortBys that correspond to non existing columns
const availableSortBy = sortBy.filter(sort =>
flatColumns.find(col => col.id === sort.id)
allColumns.find(col => col.id === sort.id)
)
const sortData = rows => {
@ -266,7 +264,7 @@ function useInstance(instance) {
rows,
availableSortBy.map(sort => {
// Support custom sorting methods for each column
const column = flatColumns.find(d => d.id === sort.id)
const column = allColumns.find(d => d.id === sort.id)
if (!column) {
throw new Error(
@ -301,7 +299,7 @@ function useInstance(instance) {
// Map the directions
availableSortBy.map(sort => {
// Detect and use the sortInverted option
const column = flatColumns.find(d => d.id === sort.id)
const column = allColumns.find(d => d.id === sort.id)
if (column && column.sortInverted) {
return sort.desc
@ -323,7 +321,7 @@ function useInstance(instance) {
}
return sortData(rows)
}, [manualSortBy, sortBy, rows, flatColumns, orderByFn, userSortTypes])
}, [manualSortBy, sortBy, rows, allColumns, orderByFn, userSortTypes])
const getAutoResetSortBy = useGetLatest(autoResetSortBy)

View File

@ -1,6 +1,6 @@
import React from 'react'
let renderErr = 'Renderer Error'
let renderErr = 'Renderer Error ☝️'
export const actions = {
init: 'init',
@ -94,11 +94,11 @@ export const makePropGetter = (hooks, meta = {}) => {
)
}
export const reduceHooks = (hooks, initial, meta = {}) =>
export const reduceHooks = (hooks, initial, meta = {}, allowUndefined) =>
hooks.reduce((prev, next) => {
const nextValue = next(prev, meta)
if (process.env.NODE_ENV !== 'production') {
if (typeof nextValue === 'undefined') {
if (!allowUndefined && typeof nextValue === 'undefined') {
console.info(next)
throw new Error(
'React Table: A reducer hook ☝️ just returned undefined! This is not allowed.'
@ -108,9 +108,9 @@ export const reduceHooks = (hooks, initial, meta = {}) =>
return nextValue
}, initial)
export const loopHooks = (hooks, meta = {}) =>
export const loopHooks = (hooks, context, meta = {}) =>
hooks.forEach(hook => {
const nextValue = hook(meta)
const nextValue = hook(context, meta)
if (process.env.NODE_ENV !== 'production') {
if (typeof nextValue !== 'undefined') {
console.info(hook, nextValue)
@ -190,14 +190,12 @@ export function useMountedLayoutEffect(fn, deps) {
export function useAsyncDebounce(defaultFn, defaultWait = 0) {
const debounceRef = React.useRef({})
debounceRef.current.defaultFn = defaultFn
debounceRef.current.defaultWait = defaultWait
const debounce = React.useCallback(
async (
fn = debounceRef.current.defaultFn,
wait = debounceRef.current.defaultWait
) => {
const getDefaultFn = useGetLatest(defaultFn)
const getDefaultWait = useGetLatest(defaultWait)
return React.useCallback(
async (...args) => {
if (!debounceRef.current.promise) {
debounceRef.current.promise = new Promise((resolve, reject) => {
debounceRef.current.resolve = resolve
@ -212,26 +210,18 @@ export function useAsyncDebounce(defaultFn, defaultWait = 0) {
debounceRef.current.timeout = setTimeout(async () => {
delete debounceRef.current.timeout
try {
debounceRef.current.resolve(await fn())
debounceRef.current.resolve(await getDefaultFn()(...args))
} catch (err) {
debounceRef.current.reject(err)
} finally {
delete debounceRef.current.promise
}
}, wait)
}, getDefaultWait())
return debounceRef.current.promise
},
[]
[getDefaultFn, getDefaultWait]
)
return debounce
}
export function useConsumeHookGetter(hooks, hookName) {
const getter = useGetLatest(hooks[hookName])
hooks[hookName] = undefined
return getter
}
export function makeRenderer(instance, column, meta = {}) {
@ -239,6 +229,7 @@ export function makeRenderer(instance, column, meta = {}) {
const Comp = typeof type === 'string' ? column[type] : type
if (typeof Comp === 'undefined') {
console.info(column)
throw new Error(renderErr)
}

View File

@ -1,3 +1,5 @@
/* istanbul ignore file */
import React from 'react'
// Token pagination behaves a bit different from

View File

@ -1,7 +1,5 @@
import React from 'react'
import { defaultColumn } from './publicUtils'
export * from './publicUtils'
import { defaultColumn, reduceHooks } from './publicUtils'
// Find the depth of the columns
export function findMaxDepth(columns, depth = 0) {
@ -13,10 +11,29 @@ export function findMaxDepth(columns, depth = 0) {
}, 0)
}
function decorateColumn(column, userDefaultColumn, parent, depth, index) {
// Apply the userDefaultColumn
column = { ...defaultColumn, ...userDefaultColumn, ...column }
// Build the visible columns, headers and flat column list
export function linkColumnStructure(columns, parent, depth = 0) {
return columns.map(column => {
column = {
...column,
parent,
depth,
}
assignColumnAccessor(column)
if (column.columns) {
column.columns = linkColumnStructure(column.columns, column, depth + 1)
}
return column
})
}
export function flattenColumns(columns) {
return flattenBy(columns, 'columns')
}
export function assignColumnAccessor(column) {
// First check for string accessor
let { id, accessor, Header } = column
@ -40,123 +57,199 @@ function decorateColumn(column, userDefaultColumn, parent, depth, index) {
throw new Error('A column ID (or string accessor) is required!')
}
column = {
// Make sure there is a fallback header, just in case
Header: () => <>&nbsp;</>,
Footer: () => <>&nbsp;</>,
...column,
// Materialize and override this stuff
Object.assign(column, {
id,
accessor,
parent,
depth,
index,
}
})
return column
}
// Build the visible columns, headers and flat column list
export function decorateColumnTree(columns, defaultColumn, parent, depth = 0) {
return columns.map((column, columnIndex) => {
column = decorateColumn(column, defaultColumn, parent, depth, columnIndex)
if (column.columns) {
column.columns = decorateColumnTree(
column.columns,
defaultColumn,
column,
depth + 1
)
}
return column
// Find the depth of the columns
export function dedupeBy(arr, fn) {
return [...arr]
.reverse()
.filter((d, i, all) => all.findIndex(dd => fn(dd) === fn(d)) === i)
.reverse()
}
export function decorateColumn(column, userDefaultColumn) {
if (!userDefaultColumn) {
throw new Error()
}
Object.assign(column, {
// Make sure there is a fallback header, just in case
Header: () => <>&nbsp;</>,
Footer: () => <>&nbsp;</>,
...defaultColumn,
...userDefaultColumn,
...column,
})
return column
}
export function accessRowsForColumn({
data,
rows,
flatRows,
rowsById,
column,
getRowId,
getSubRows,
accessValueHooks,
getInstance,
}) {
// Access the row's data column-by-column
// We do it this way so we can incrementally add materialized
// columns after the first pass and avoid excessive looping
const accessRow = (originalRow, rowIndex, depth = 0, parent, parentRows) => {
// Keep the original reference around
const original = originalRow
const id = getRowId(originalRow, rowIndex, parent)
let row = rowsById[id]
// If the row hasn't been created, let's make it
if (!row) {
row = {
id,
original,
index: rowIndex,
depth,
cells: [{}], // This is a dummy cell
}
// Override common array functions (and the dummy cell's getCellProps function)
// to show an error if it is accessed without calling prepareRow
row.cells.map = unpreparedAccessWarning
row.cells.filter = unpreparedAccessWarning
row.cells.forEach = unpreparedAccessWarning
row.cells[0].getCellProps = unpreparedAccessWarning
// Create the cells and values
row.values = {}
// Push this row into the parentRows array
parentRows.push(row)
// Keep track of every row in a flat array
flatRows.push(row)
// Also keep track of every row by its ID
rowsById[id] = row
// Get the original subrows
row.originalSubRows = getSubRows(originalRow, rowIndex)
// Then recursively access them
if (row.originalSubRows) {
const subRows = []
row.originalSubRows.forEach((d, i) =>
accessRow(d, i, depth + 1, row, subRows)
)
// Keep the new subRows array on the row
row.subRows = subRows
}
} else if (row.subRows) {
// If the row exists, then it's already been accessed
// Keep recursing, but don't worry about passing the
// accumlator array (those rows already exist)
row.originalSubRows.forEach((d, i) => accessRow(d, i, depth + 1, row))
}
// If the column has an accessor, use it to get a value
if (column.accessor) {
row.values[column.id] = column.accessor(originalRow, rowIndex, row)
}
// Allow plugins to manipulate the column value
row.values[column.id] = reduceHooks(
accessValueHooks,
row.values[column.id],
{
row,
column,
instance: getInstance(),
},
true
)
}
data.forEach((originalRow, rowIndex) =>
accessRow(originalRow, rowIndex, 0, undefined, rows)
)
}
// Build the header groups from the bottom up
export function makeHeaderGroups(flatColumns, defaultColumn) {
export function makeHeaderGroups(allColumns, defaultColumn) {
const headerGroups = []
// Build each header group from the bottom up
const buildGroup = (columns, depth) => {
let scanColumns = allColumns
let uid = 0
const getUID = () => uid++
while (scanColumns.length) {
// The header group we are creating
const headerGroup = {
headers: [],
}
// The parent columns we're going to scan next
const parentColumns = []
// Do any of these columns have parents?
const hasParents = columns.some(col => col.parent)
columns.forEach(column => {
// Are we the first column in this group?
const isFirst = !parentColumns.length
const hasParents = scanColumns.some(d => d.parent)
// Scan each column for parents
scanColumns.forEach(column => {
// What is the latest (last) parent column?
let latestParentColumn = [...parentColumns].reverse()[0]
// If the column has a parent, add it if necessary
if (column.parent) {
const similarParentColumns = parentColumns.filter(
d => d.originalId === column.parent.id
)
if (isFirst || latestParentColumn.originalId !== column.parent.id) {
parentColumns.push({
let newParent
if (hasParents) {
// If the column has a parent, add it if necessary
if (column.parent) {
newParent = {
...column.parent,
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 similarParentColumns = parentColumns.filter(
d => d.originalId === originalId
)
const placeholderColumn = decorateColumn(
{
originalId,
id: [column.id, 'placeholder', similarParentColumns.length].join(
'_'
),
placeholderOf: column,
},
defaultColumn
)
if (
isFirst ||
latestParentColumn.originalId !== placeholderColumn.originalId
) {
parentColumns.push(placeholderColumn)
}
}
// Establish the new headers[] relationship on the parent
if (column.parent || hasParents) {
latestParentColumn = [...parentColumns].reverse()[0]
latestParentColumn.headers = latestParentColumn.headers || []
if (!latestParentColumn.headers.includes(column)) {
latestParentColumn.headers.push(column)
}
}
column.totalHeaderCount = column.headers
? column.headers.reduce(
(sum, header) => sum + header.totalHeaderCount,
0
id: `${column.parent.id}_${getUID()}`,
headers: [column],
}
} else {
// If other columns have parents, we'll need to add a place holder if necessary
const originalId = `${column.id}_placeholder`
newParent = decorateColumn(
{
originalId,
id: `${column.id}_placeholder_${getUID()}`,
placeholderOf: column,
headers: [column],
},
defaultColumn
)
: 1 // Leaf node columns take up at least one count
}
// If the resulting parent columns are the same, just add
// the column and increment the header span
if (
latestParentColumn &&
latestParentColumn.originalId === newParent.originalId
) {
latestParentColumn.headers.push(column)
} else {
parentColumns.push(newParent)
}
}
headerGroup.headers.push(column)
})
headerGroups.push(headerGroup)
if (parentColumns.length) {
buildGroup(parentColumns, depth + 1)
}
// Start scanning the parent columns
scanColumns = parentColumns
}
buildGroup(flatColumns, 0)
return headerGroups.reverse()
}
@ -225,20 +318,20 @@ export function isFunction(a) {
}
}
export function flattenBy(columns, childKey) {
export function flattenBy(arr, key) {
const flatColumns = []
const recurse = columns => {
columns.forEach(d => {
if (!d[childKey]) {
const recurse = arr => {
arr.forEach(d => {
if (!d[key]) {
flatColumns.push(d)
} else {
recurse(d[childKey])
recurse(d[key])
}
})
}
recurse(columns)
recurse(arr)
return flatColumns
}
@ -280,6 +373,12 @@ export function shouldAutoRemoveFilter(autoRemove, value) {
return autoRemove ? autoRemove(value) : typeof value === 'undefined'
}
export function unpreparedAccessWarning() {
throw new Error(
'React-Table: You have not called prepareRow(row) one or more rows you are attempting to render.'
)
}
//
const reOpenBracket = /\[/g

View File

@ -0,0 +1,41 @@
const range = len => {
const arr = []
for (let i = 0; i < len; i++) {
arr.push(i)
}
return arr
}
const newPerson = (depth, index, total) => {
const age = (depth + 1) * (index + 1)
const visits = age * 10
const progress = (index + 1) / total
return {
firstName: `${depth} ${index} firstName`,
lastName: `${depth} ${index} lastName`,
age,
visits,
progress,
status:
progress > 0.66
? 'relationship'
: progress > 0.33
? 'complicated'
: 'single',
}
}
export default function makeTestData(...lens) {
const makeDataLevel = (depth = 0) => {
const len = lens[depth]
return range(len).map(d => {
return {
...newPerson(depth, d, len),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}

31
test-utils/react-testing.js vendored Normal file
View File

@ -0,0 +1,31 @@
import { render as originalRender } from '@testing-library/react'
import diff from 'jest-diff'
import chalk from 'chalk'
const render = (...args) => {
const rendered = originalRender(...args)
rendered.lastFragment = new DocumentFragment()
rendered.debugDiff = (log = true) => {
const nextFragment = rendered.asFragment()
if (log) {
console.log(
diff(rendered.lastFragment, nextFragment, {
aAnnotation: 'Previous',
bAnnotation: 'Next',
aColor: chalk.red,
bColor: chalk.green,
})
)
}
rendered.lastFragment = nextFragment
}
return rendered
}
export * from '@testing-library/react'
export { render }

4111
yarn.lock

File diff suppressed because it is too large Load Diff