Compare commits

...
Author SHA1 Message Date
tannerlinsley ab32339bfa v7.0.0-alpha.13 2019-07-30 10:23:42 -06:00
tannerlinsley 7fd8fa1dfc docs: adding grouping example 2019-07-30 10:21:05 -06:00
tannerlinsley bc89ae3027 Merge branch 'master' of https://github.com/react-tools/react-table 2019-07-30 09:57:04 -06:00
tannerlinsley af739d91d0 fix(usegroupby): fix useGroupBy, add grouping example and fix some tests 2019-07-30 09:56:44 -06:00
Cyril SimanandTanner Linsley 35f78132f6 Avoid react error if the data source doesn't have the property. (#1420)
* Fix : Avoid react error if the data source doesn't have the property.

* Update fix with tannerlinsley's better solution
2019-07-30 09:51:17 -06:00
Larry BothaandTanner Linsley d7a4db0979 chore(commitlint): add deps, config, and git hook (#1419) 2019-07-29 23:45:46 -06:00
tannerlinsley 11167e5635 Refactor useTable, sorting, and filtering to use new hook layer 2019-07-29 14:51:07 -06:00
Larry BothaandTanner Linsley 14a43b1a68 Update test configs (#1417)
* chore(package.json): use latest babel core, remove bridge

* test(.babelrc): add test environment so that Jest can use ES6 imports

* test(usetable): fix import
2019-07-29 14:34:33 -06:00
tannerlinsley 0375a7e7d5 Set up some failing tests 2019-07-29 12:05:19 -06:00
tannerlinsley 6b70309f42 Add prettier format 2019-07-29 11:33:47 -06:00
tannerlinsley 9b262676be Fix hook defaults 2019-07-29 11:14:41 -06:00
tannerlinsley 1caad4cd8c Merge branch 'master' of https://github.com/react-tools/react-table 2019-07-29 11:00:11 -06:00
tannerlinsley dc73347003 Relocate columns and row logic, fix columns and useGroupBy to be more pure
Since useColumns was relying on groupBy logic, this was code smell. I wanted useGroupBy to be able to add that logic all by itself and not have to have dependencies in the core of the table.

To fix that, I've moved the core column and row logic to the useTable hook and added a new hook 'columnsBeforeHeaderGroups' to allow useGroupBy to do what i needs in a more pure way.
2019-07-29 11:00:07 -06:00
gargrohandTanner Linsley 66bfec34a8 [minor] Readme update (#1415)
* correcting typos

* [minor ] Readme update
2019-07-29 09:37:48 -06:00
gargrohandTanner Linsley 6347157f80 correcting typos (#1413) 2019-07-29 08:46:56 -06:00
jaapjanfransandTanner Linsley b6db8775ad Small readme example update (#1412)
in commit ea79cd8388 the`getRowProps` member of the headerGroup object changed into `getHeaderGroupProps`
2019-07-29 07:51:01 -06:00
tannerlinsley cadd8bf62a Rename examples and their directories 2019-07-26 16:03:54 -06:00
tannerlinsley 9f4746a7ac Add sorting guide 2019-07-26 15:54:50 -06:00
tannerlinsley a0ca841287 Update App.js 2019-07-26 11:27:10 -06:00
tannerlinsley 52ee923675 Update package.json 2019-07-26 11:22:58 -06:00
tannerlinsley 24f6a2750d fix(usefilters): fix filter type fallback and autoRemove functionality AGAIN 2019-07-26 11:22:23 -06:00
tannerlinsley 8988660618 fix(usefilters): fixed filtering fallback logic and autoRemove bug 2019-07-26 10:58:35 -06:00
tannerlinsley a3e5cac298 fix(usefilters): fixed the default filter to be 'text' 2019-07-26 10:52:27 -06:00
tannerlinsley 47d2651b8d docs: updated contribution documentation 2019-07-26 10:45:33 -06:00
tannerlinsley 412f64cece chore: released version 7.0.0-alpha.9 2019-07-26 10:39:19 -06:00
82 changed files with 17359 additions and 4230 deletions
+7 -2
View File
@@ -1,11 +1,16 @@
{
"presets": [
[
"@babel/env",
"@babel/preset-env",
{
"modules": false
}
],
"@babel/react"
]
],
"env": {
"test": {
"presets": ["@babel/preset-env", "@babel/react"]
}
}
}
+1 -20
View File
@@ -1,22 +1,3 @@
{
"parser": "babel-eslint",
"extends": ["react-app", "prettier"],
"plugins": [
"react-hooks"
],
"env": {
"es6": true
},
"parserOptions": {
"sourceType": "module"
},
"rules": {
"space-before-function-paren": 0,
"react/jsx-boolean-value": 0
},
"settings": {
"react": {
"version": "latest"
}
}
"extends": ["react-app", "prettier"]
}
+1
View File
@@ -1,5 +1,6 @@
module.exports = {
hooks: {
'pre-commit': 'lint-staged',
'commit-msg': 'commitlint -E HUSKY_GIT_PARAMS',
},
}
+73 -73
View File
@@ -2,58 +2,58 @@
#### Fixes & Optimizations
* Since `resolveData` is now capable of materializing data on it's own, the `data` prop is no longer required as a prop-type.
- Since `resolveData` is now capable of materializing data on it's own, the `data` prop is no longer required as a prop-type.
## 6.8.4
#### Fixes & Optimizations
* Only run `resolveData` prop when `data` prop has changed, not any others.
- Only run `resolveData` prop when `data` prop has changed, not any others.
## 6.8.3
#### Fixes & Optimizations
* Allow the `resolveData` prop to alter or materialize new data when the `data` prop changes.
- Allow the `resolveData` prop to alter or materialize new data when the `data` prop changes.
## 6.8.1
#### Fixes & Optimizations
* Updated eslint and code formatting
- Updated eslint and code formatting
## 6.7.5
#### Fixes & Optimizations
* Now passes `column` to `getResizerProps` (#667)
* NOTE: `getResizerProps` is now only called if the column is resizable
* Fixes the `className` ordering in defaultProps for ThComponent (#673)
* NOTE: user supplied classNames now come at the end so they can extend the defaults
- Now passes `column` to `getResizerProps` (#667)
- NOTE: `getResizerProps` is now only called if the column is resizable
- Fixes the `className` ordering in defaultProps for ThComponent (#673)
- NOTE: user supplied classNames now come at the end so they can extend the defaults
## 6.7.4
#### Fixes & Optimizations
* Fix Prop types for columns
- Fix Prop types for columns
## 6.7.3
#### Fixes & Optimizations
* Fix the rest of the proptypes
- Fix the rest of the proptypes
## 6.7.2
#### Fixes & Optimizations
* `getPropTypes` proptype check
- `getPropTypes` proptype check
## 6.7.1
#### Fixes & Optimizations
* `eslint-config` moved to dev deps
- `eslint-config` moved to dev deps
## 6.7.0
@@ -61,113 +61,113 @@
#### New Features
* Expose page/pageSize to rows/cells
* Supply sort direction to custom sort methods
- Expose page/pageSize to rows/cells
- Supply sort direction to custom sort methods
#### Fixes & Optimizations
* README updates
* Linter cleanup
* Added PropTypes node module
* Deps, linting and style upgrades
- README updates
- Linter cleanup
- Added PropTypes node module
- Deps, linting and style upgrades
## 6.6.0
#### Fixes & Optimizations
* moved repo to react-tools
* Doc examples moved to codesandbox.io
* README updates
* CSS refacting for rt-tfoot to match rt-thead
* CSS more specific for input and select
- moved repo to react-tools
- Doc examples moved to codesandbox.io
- README updates
- CSS refacting for rt-tfoot to match rt-thead
- CSS more specific for input and select
## 6.5.3
#### Fixes & Optimizations
* `onClick` proxying and eslint
- `onClick` proxying and eslint
## 6.5.2
#### New Features
* Provide onClick handleOriginal function - #406
- Provide onClick handleOriginal function - #406
#### Fixes & Optimizations
* README updates
* `makePathArray` in utils - #326
* Various fixes: #294, #376, #398, #415,
- README updates
- `makePathArray` in utils - #326
- Various fixes: #294, #376, #398, #415,
## 6.5.1
#### Fixes & Optimizations
* `defaultExpanded` now works correctly - #372
* `column.getProps().rest` props are now applied correctly
* `makeTemplateComponent` now supports `displayName` - #289
- `defaultExpanded` now works correctly - #372
- `column.getProps().rest` props are now applied correctly
- `makeTemplateComponent` now supports `displayName` - #289
## 6.5.0
##### New Features
* `column.filterAll` - defaults to `false`, but when set to `true` will provide the entire array of rows to `filterMethod` as opposed to one row at a time. This allows for more fine-grained filtering using any method you can dream up. See the [Custom Filtering example](https://react-table.js.org/#/story/custom-filtering) for more info.
- `column.filterAll` - defaults to `false`, but when set to `true` will provide the entire array of rows to `filterMethod` as opposed to one row at a time. This allows for more fine-grained filtering using any method you can dream up. See the [Custom Filtering example](https://react-table.js.org/#/story/custom-filtering) for more info.
## 6.4.0
##### New Features
* `PadRowComponent` - the content rendered inside of a padding row. Defaults to a react component that renders ` `
- `PadRowComponent` - the content rendered inside of a padding row. Defaults to a react component that renders ` `
## 6.3.0
##### New Features
* `defaultSortDesc` - allows you to set the default sorting direction for all columns to descending.
* `column.defaultSortDesc` - allows you to set the default sorting direction for a specific column. Falls back to the global `defaultSortDesc` when not set at all.
- `defaultSortDesc` - allows you to set the default sorting direction for all columns to descending.
- `column.defaultSortDesc` - allows you to set the default sorting direction for a specific column. Falls back to the global `defaultSortDesc` when not set at all.
## 6.0.0
##### New Features
* New Renderers:
* `Aggregated` - Custom renderer for aggregated cells
* `Pivot` - Custom renderer for Pivoted Cells (utilizes `Expander` and `PivotValue`)
* `PivotValue` - Custom renderer for Pivot cell values (deprecates the undocumented `pivotRender` option)
* `Expander` - Custom renderer for Pivot cell Expander
* Added custom sorting methods per table via `defaultSortMethod` and per column via `column.sortMethod`
* Pivot columns are now visibly separate and sorted/filtered independently.
* Added `column.resizable` to override global table `resizable` option for specific columns.
* Added `column.sortable` to override global table `sortable` option for specific columns.
* Added `column.filterable` to override global table `filterable` option for specific columns.
* Added `defaultExpanded` table option.
* All callbacks can now be utilized without needing to hoist and manage the piece of state they export. That is what their prop counterparts are for, so now the corresponding prop is used instead of the callback to detect a "fully controlled" state.
* Prevent transitions while column resizing for a smoother resize effect.
* Disable text selection while resizing columns.
- New Renderers:
- `Aggregated` - Custom renderer for aggregated cells
- `Pivot` - Custom renderer for Pivoted Cells (utilizes `Expander` and `PivotValue`)
- `PivotValue` - Custom renderer for Pivot cell values (deprecates the undocumented `pivotRender` option)
- `Expander` - Custom renderer for Pivot cell Expander
- Added custom sorting methods per table via `defaultSortMethod` and per column via `column.sortMethod`
- Pivot columns are now visibly separate and sorted/filtered independently.
- Added `column.resizable` to override global table `resizable` option for specific columns.
- Added `column.sortable` to override global table `sortable` option for specific columns.
- Added `column.filterable` to override global table `filterable` option for specific columns.
- Added `defaultExpanded` table option.
- All callbacks can now be utilized without needing to hoist and manage the piece of state they export. That is what their prop counterparts are for, so now the corresponding prop is used instead of the callback to detect a "fully controlled" state.
- Prevent transitions while column resizing for a smoother resize effect.
- Disable text selection while resizing columns.
##### Breaking API Changes
* New Renderers:
* `Cell` - deprecates and replaces `render`
* `Header` - deprecates and replaces `header`
* `Footer` - deprecates and replaces `footer`
* `Filter`- deprecates and replaces `filterRender`
* Callbacks now provide the destination state as the primary parameter(s). This makes hoisting and controlling the state in redux or component state much easier. eg.
* `onSorting` no longer requires you to build your own toggle logic
* `onResize` no longer requires you to build your own resize logic
* Renamed `onChange` callback -> `onFetchData` which will always fire when a new data model needs to be fetched (or if not using `manual`, when new data is materialized internally).
* Renamed `filtering` -> `filtered`
* Renamed `sorting` -> `sorted`
* Renamed `expandedRows` -> `expanded`
* Renamed `resizing` -> `resized`
* Renamed `defaultResizing` -> `defaultResized`
* Renamed `defaultFiltering` -> `defaultFiltered`
* Renamed `defaultSorting` -> `defaultSorted`
* Renamed `onSortingChange` -> `onSortedChange`
* Renamed `onFilteringChange` -> `onFilteredChange`
* Renamed `onResize` -> `onResizedChange`
* Renamed `onExpandRow` -> `onExpandedChange`
* Renamed `showFilters` -> `filterable`
* Renamed `hideFilter` -> `filterable` (Column option. Note the true/false value is now flipped.)
* `cellInfo.row` and `rowInfo.row` now reference the materialize data for the table. To reference the original row, use `cellInfo.original` and `rowInfo.original`
* Removed `pivotRender` column option. You can now control how the value is displayed by overriding the `PivotValueComponent` or the individual column's `PivotValue` renderer. See [Pivoting Options Story](https://react-table.js.org/?selectedKind=2.%20Demos&selectedStory=Pivoting%20Options&full=0&down=1&left=1&panelRight=0&downPanel=kadirahq%2Fstorybook-addon-actions%2Factions-panel) for a reference on how to customize pivot column rendering.
- New Renderers:
- `Cell` - deprecates and replaces `render`
- `Header` - deprecates and replaces `header`
- `Footer` - deprecates and replaces `footer`
- `Filter`- deprecates and replaces `filterRender`
- Callbacks now provide the destination state as the primary parameter(s). This makes hoisting and controlling the state in redux or component state much easier. eg.
- `onSorting` no longer requires you to build your own toggle logic
- `onResize` no longer requires you to build your own resize logic
- Renamed `onChange` callback -> `onFetchData` which will always fire when a new data model needs to be fetched (or if not using `manual`, when new data is materialized internally).
- Renamed `filtering` -> `filtered`
- Renamed `sorting` -> `sorted`
- Renamed `expandedRows` -> `expanded`
- Renamed `resizing` -> `resized`
- Renamed `defaultResizing` -> `defaultResized`
- Renamed `defaultFiltering` -> `defaultFiltered`
- Renamed `defaultSorting` -> `defaultSorted`
- Renamed `onSortingChange` -> `onSortedChange`
- Renamed `onFilteringChange` -> `onFilteredChange`
- Renamed `onResize` -> `onResizedChange`
- Renamed `onExpandRow` -> `onExpandedChange`
- Renamed `showFilters` -> `filterable`
- Renamed `hideFilter` -> `filterable` (Column option. Note the true/false value is now flipped.)
- `cellInfo.row` and `rowInfo.row` now reference the materialize data for the table. To reference the original row, use `cellInfo.original` and `rowInfo.original`
- Removed `pivotRender` column option. You can now control how the value is displayed by overriding the `PivotValueComponent` or the individual column's `PivotValue` renderer. See [Pivoting Options Story](https://react-table.js.org/?selectedKind=2.%20Demos&selectedStory=Pivoting%20Options&full=0&down=1&left=1&panelRight=0&downPanel=kadirahq%2Fstorybook-addon-actions%2Factions-panel) for a reference on how to customize pivot column rendering.
+390 -98
View File
@@ -33,9 +33,7 @@ Hooks for building **lightweight, fast and extendable datagrids** for React
- Extensible via hooks
- <a href="https://medium.com/@tannerlinsley/why-i-wrote-react-table-and-the-problems-it-has-solved-for-nozzle-others-445c4e93d4a8#.axza4ixba" target="\_parent">"Why I wrote React Table and the problems it has solved for Nozzle.io"</a> by Tanner Linsley
## Demos
[React Table v7 Sandbox](https://codesandbox.io/s/m5lxzzpz69)
## [See Examples](#examples)
## Versions
@@ -166,12 +164,13 @@ import {
# Examples
- [Basic](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/basic)
- [Sorting with `useSortBy`](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/sorting-with-useSortBy)
- [Filtering with `useFilters`](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/filtering-with-useFilters)
- [Sorting - Client Side](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/sorting-client-side)
- [Filtering - Client Side](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/filtering-client-side)
- [Grouping - Client Side](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/grouping-client-side)
# Concepts
## React Table is a "headless" library
## React Table is a "headless" UI library
React Table is a headless utility, which means out of the box, it doesn't render or supply any actual UI elements. You are in charge of utilizing the state and callbacks of the hooks provided by this library to render your own table markup. [Read this article to understand why React Table is built this way.](https://medium.com/merrickchristensen/headless-user-interface-components-565b0c0f2e18). If you don't want to, then here's a quick rundown anyway:
@@ -237,12 +236,15 @@ const instance = useTable(
1. `useTable` is called. A table instance is created.
1. The `instance.state` is resolved from either a custom user state or an automatically generated one.
1. A collection of plugin points is created at `instance.hooks`. These plugin points don't run until after all of the plugins have run.
1. The instance is reduced through each plugin hook in the order they were called. Each hook receives the result of the previous hook, is able to manipulate the `instance`, use plugin points, use their own React hooks internally and eventually return a new `instance`. This happens until the last instance object is returned from the last hook.
1. Lastly, the plugin points that were registered and populated during hook reduction are run to produce the final instance object that is returned from `useTable`
1. A collection of plugin points is created at `instance.hooks`.
1. Each plugin is given the opportunity to add hooks to `instance.hook`.
1. As the `useTable` logic proceeds to run, each plugin hook type is used at a specific point in time with each individual hook function being executed the order it was registered.
1. The final instance object is returned from `useTable`, which the developer then uses to construct their table.
This multi-stage process is the secret sauce that allows React Table plugin hooks to work together and compose nicely, while not stepping on each others toes.
To dive deeper into plugins, see [Plugins](TODO) and the [Plugin Guide](TODO)
### Plugin Hook Order & Consistency
The order and usage of plugin hooks must follow [The Laws of Hooks](TODO), just like any other custom hook. They must always be unconditionally called in the same order.
@@ -286,12 +288,6 @@ The following options are supported via the main options object passed to `useTa
- The default column object for every column passed to React Table.
- Column-specific properties will override the properties in this object, eg. `{ ...defaultColumn, ...userColumn }`
- This is particularly useful for adding global column properties. For instance, when using the `useFilters` plugin hook, add a default `Filter` renderer for every column, eg.`{ Filter: MyDefaultFilterComponent }`
- `useColumns: Function`
- Optional
- This hook overrides the internal `useColumns` hooks used by `useTable`. You probably never want to override this unless you are testing or developing new features for React Table
- `useRows: Function`
- Optional
- This hook overrides the internal `useRows` hooks used by `useTable`. You probably never want to override this unless you are testing or developing new features for React Table
- `debug: Bool`
- Optional
- A flag to turn on debug mode.
@@ -340,7 +336,7 @@ The following properties are available on the table instance returned from `useT
- `headerGroups: Array<HeaderGroup>`
- An array of normalized header groups, each containing a flattened array of final column objects for that row.
- See [Header Group Properties](#header-group-properties) for more information
- See [Header Group Properties](#headergroup-properties) for more information
- `columns: Array<Column>`
- A **flat** array of all final column objects computed from the original columns configuration option.
- See [Column Properties](#column-properties) for more information
@@ -348,7 +344,7 @@ The following properties are available on the table instance returned from `useT
- A **nested** array of final column objects, similar in structure to the original columns configuration option.
- See [Column Properties](#column-properties) for more information
- `rows: Array<Row>`
- An array of **materialized row objects** from theriginal `dat oa` array and `columns` passed into the table options
- An array of **materialized row objects** from the original `data` array and `columns` passed into the table options
- See [Row Properties](#row-properties) for more information
- `getTableProps: Function(?props)`
- **Required**
@@ -361,7 +357,7 @@ The following properties are available on the table instance returned from `useT
### `HeaderGroup` Properties
Header Groups are The following additional properties are available on every `headerGroup` object returned by the table instance.
The following additional properties are available on every `headerGroup` object returned by the table instance.
- `headers: Array<Column>`
- **Required**
@@ -481,7 +477,7 @@ function MyTable({ columns, data }) {
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getRowProps()}>
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
@@ -505,58 +501,158 @@ function MyTable({ columns, data }) {
}
```
## `useGroupBy`
## `useSortBy`
- Plugin Hook
- Optional
`useGroupBy` is the hook that implements **row grouping and aggregation**.
`useSortBy` is the hook that implements **row sorting**. It also support multi-sort (keyboard required).
- Multi-sort is enabled by default
- To sort the table via UI, attach the props generated from each column's `getSortByToggleProps()`, then click any of those elements.
- To multi-sort the table via UI, hold `shift` while clicking on any of those same elements that have the props from `getSortByToggleProps()` attached.
- To programmatically sort (or multi-sort) any column, use the `toggleSortBy` method located on the instance or each individual column.
### Table Options
The following options are supported via the main options object passed to `useTable(options)`
- `state[0].groupBy: Array<String>`
- `state[0].sortBy: Array<Object<id: columnID, desc: Bool>>`
- Must be **memoized**
- An array of groupBy ID strings, controlling which columns are used to calculate row grouping and aggregation. This information is stored in state since the table is allowed to manipulate the groupBy through user interaction.
- `groupByFn: Function`
- An array of sorting objects. If there is more than one object in the array, multi-sorting will be enabled. Each sorting object should contain an `id` key with the corresponding column ID to sort by. An optional `desc` key may be set to true or false to indicated ascending or descending sorting for that column. This information is stored in state since the table is allowed to manipulate the filter through user interaction.
- `manualSorting: Bool`
- Enables sorting detection functionality, but does not automatically perform row sorting. Turn this on if you wish to implement your own sorting outside of the table (eg. server-side or manual row grouping/nesting)
- `disableSorting: Bool`
- Disables sorting for every column in the entire table.
- `disableMultiSort: Bool`
- Disables multi-sorting for the entire table.
- `disableSortRemove: Bool`
- If true, the un-sorted state will not be available to columns once they have been sorted.
- `disableMultiRemove: Bool`
- If true, the un-sorted state will not be available to multi-sorted columns.
- `orderByFn: Function`
- Must be **memoizd**
- Defaults to the built-in [default orderBy function](TODO)
- This function is responsible for composing multiple sorting functions together for multi-sorting, and also handles both the directional sorting and stable-sorting tie breaking. Rarely would you want to override this function unless you have a very advanced use-case that requires it.
- `sortTypes: Object<sortKey: sortType>`
- Must be **memoized**
- Defaults to [`defaultGroupByFn`](TODO)
- This function is responsible for grouping rows based on the `state.groupBy` keys provided. It's very rare you would need to customize this function.
- `manualGroupBy: Bool`
- Enables groupBy detection and functionality, but does not automatically perform row grouping.
- Turn this on if you wish to implement your own row grouping outside of the table (eg. server-side or manual row grouping/nesting)
- `disableGrouping: Bool`
- Disables groupBy for the entire table.
- `aggregations: Object<aggregationKey: aggregationFn>`
- Must be **memoized**
- Allows overriding or adding additional aggregation functions for use when grouping/aggregating row values. If an aggregation key isn't found on this object, it will default to using the [built-in aggregation functions](TODO)
- Allows overriding or adding additional sort types for columns to use. If a column's sort type isn't found on this object, it will default to using the [built-in sort types](TODO).
- For mor information on sort types, see [Sorting](TODO)
### `Column` Options
The following options are supported on any `Column` object passed to the `columns` options in `useTable()`
- `disableSorting: Bool`
- Optional
- Defualts to `false`
- If set to `true`, the sorting for this column will be disabled
- `sortDescFirst: Bool`
- Optional
- Defaults to `false`
- If set to `true`, the first sort direction for this column will be descending instead of ascending
- `sortInverted: Bool`
- Optional
- Defaults to `false`
- If set to `true`, the underlying sorting direction will be inverted, but the UI will not.
- This may be useful in situations where positive and negative connotation is inverted, eg. a Golfing score where a lower score is considered more positive than a higher one.
- `sortType: String | Function`
- If a **function** is passed, it must be **memoized**
- Defaults to [`alphanumeric`](TODO)
- The resolved function from the this string/function will be used to sort the this column's data.
- If a `string` is passed, the function with that name located on either the custom `sortTypes` option or the built-in sorting types object will be used. If
- If a `function` is passed, it will be used.
- For mor information on sort types, see [Sorting](TODO)
### `Instance` Properties
The following values are provided to the table `instance`:
- `rows: Array<Row>`
- An array of **grouped and aggregated** rows.
- An array of **sorted** rows.
- `preSortedRows: Array<Row>`
- The array of rows that were originally sorted.
- `toggleSortBy: Function(ColumnID: String, descending: Bool, isMulti: Bool) => void`
- This function can be used to programmatically toggle the sorting for any specific column
### `Column` Properties
The following properties are available on every `Column` object returned by the table instance.
- `canSort: Bool`
- Denotes whether a column is sortable or not depending on if it has a valid accessor/data model or is manually disabled via an option.
- `toggleSortBy: Function(descending, multi) => void`
- This function can be used to programmatically toggle the sorting for this column.
- This function is similar to the `instance`-level `toggleSortBy`, however, passing a columnID is not required since it is located on a `Column` object already.
- `getSortByToggleProps: Function(props) => props`
- **Required**
- This function is used to resolve any props needed for this column's UI that is responsible for toggling the sort direction when the user clicks it.
- You can use the `getSortByToggleProps` hook to extend its functionality.
- Custom props may be passed. **NOTE: Custom props may override built-in sortBy props, so be careful!**
- `sorted: Boolean`
- Denotes whether this column is currently being sorted
- `sortedIndex: Int`
- If the column is currently sorted, this integer will be the index in the `sortBy` array from state that corresponds to this column.
- If this column is not sorted, the index will always be `-1`
- `sortedDesc: Bool`
- If the column is currently sorted, this denotes whether the column's sort direction is descending or not.
- If `true`, the column is sorted `descending`
- If `false`, the column is sorted `ascending`
- If `undefined`, the column is not currently being sorted.
### Example
```js
const state = useTableState({ groupBy: ['firstName'] })
function Table({ columns, data }) {
// Set some default sorting state
const state = useTableState({ sortBy: [{ id: 'firstName', desc: true }] })
const aggregations = React.useMemo(() => ({
customSum: (values, rows) => values.reduce((sum, next) => sum + next, 0),
}))
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{
columns,
data,
},
useSortBy // Use the sortBy hook
)
const { rows } = useTable(
{
state, // state[0].groupBy === ['firstName']
manualGroupBy: false,
disableGrouping: false,
aggregations,
},
useGroupBy
)
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
// Add the sorting props to control sorting. For this example
// we can add them into the header props
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
<span>
{/* Add a sort direction indicator */}
<span>
{column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''}
</span>
{/* Add a sort index indicator */}
<span>({column.sorted ? column.sortedIndex + 1 : ''})</span>
</span>
</th>
))}
</tr>
))}
</thead>
<tbody>
{rows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
)}
</tbody>
</table>
)
}
```
## `useFilters`
@@ -590,6 +686,27 @@ The following options are supported via the main options object passed to `useTa
- Allows overriding or adding additional filter types for columns to use. If a column's filter type isn't found on this object, it will default to using the [built-in filter types](TODO).
- For mor information on filter types, see [Filtering](TODO)
### `Column` Options
The following options are supported on any `Column` object passed to the `columns` options in `useTable()`
- `Filter: Function | React.Component => JSX`
- **Required**
- Receives the table instance and column model as props
- Must return valid JSX
- This function (or component) is used to render this column's filter UI, eg.
- `disableFilters: Bool`
- Optional
- If set to `true`, will disable filtering for this column
- `filter: String | Function`
- Optional
- Defaults to [`text`](TODO)
- The resolved function from the this string/function will be used to filter the this column's data.
- If a `string` is passed, the function with that name located on either the custom `filterTypes` option or the built-in filtering types object will be used. If
- If a `function` is passed, it will be used directly.
- For mor information on filter types, see [Filtering](TODO)
- If a **function** is passed, it must be **memoized**
### `Instance` Properties
The following values are provided to the table `instance`:
@@ -604,6 +721,20 @@ The following values are provided to the table `instance`:
- `setAllFilters: Function(filtersObject) => void`
- An instance-level function used to update the values for **all** filters on the table, all at once.
### `Column` Properties
The following properties are available on every `Column` object returned by the table instance.
- `canFilter: Bool`
- Denotes whether a column is filterable or not depending on if it has a valid accessor/data model or is manually disabled via an option.
- `setFilter: Function(filterValue) => void`
- An column-level function used to update the filter value for this column
- `filterValue: any`
- The current filter value for this column, resolved from the table state's `filters` object
- `preFilteredRows: Array<row>`
- The array of rows that were originally passed to this columns filter **before** they were filtered.
- This array of rows can be useful if building faceted filter options.
### Example
```js
@@ -650,66 +781,97 @@ const { rows } = useTable(
)
```
## `useSortBy`
## `useGroupBy`
- Plugin Hook
- Optional
`useSortBy` is the hook that implements **row sorting**. It also support multi-sort (keyboard required).
`useGroupBy` is the hook that implements **row grouping and aggregation**.
- Each column's `getGroupByToggleProps()` function can be used to generate the props needed to make a clickable UI element that will toggle the grouping on or off for a specific column.
- Instance and column-level `toggleGroupBy` functions are also made available for programmatic grouping.
### Table Options
The following options are supported via the main options object passed to `useTable(options)`
- `state[0].sortBy: Array<Object<id: columnID, desc: Bool>>`
- `state[0].groupBy: Array<String>`
- Must be **memoized**
- An array of sorting objects. If there is more than one object in the array, multi-sorting will be enabled. Each sorting object should contain an `id` key with the corresponding column ID to sort by. An optional `desc` key may be set to true or false to indicated ascending or descending sorting for that column. This information is stored in state since the table is allowed to manipulate the filter through user interaction.
- `defaultSortType: String | Function`
- If a **function** is passed, it must be **memoized**
- Defaults to [`alphanumeric`](TODO)
- The function (or resolved function from the string) will be used as the default/fallback sort method for every column that has sorting enabled.
- If a `string` is passed, the function with that name located on the `sortTypes` option object will be used.
- If a `function` is passed, it will be used.
- For mor information on sort types, see [Sorting](TODO)
- `manualSorting: Bool`
- Enables sorting detection functionality, but does not automatically perform row sorting. Turn this on if you wish to implement your own sorting outside of the table (eg. server-side or manual row grouping/nesting)
- `disableSorting: Bool`
- Disables sorting for every column in the entire table.
- `disableMultiSort: Bool`
- Disables multi-sorting for the entire table.
- `defaultSortDesc: Bool`
- If true, the first default direction for sorting will be descending. This may also be overridden at the column level.
- `disableSortRemove: Bool`
- If true, the un-sorted state will not be available to columns once they have been sorted.
- `disableMultiRemove: Bool`
- If true, the un-sorted state will not be available to multi-sorted columns.
- `orderByFn: Function`
- Must be **memoizd**
- Defaults to the built-in [default orderBy function](TODO)
- This function is responsible for composing multiple sorting functions together for multi-sorting, and also handles both the directional sorting and stable-sorting tie breaking. Rarely would you want to override this function unless you have a very advanced use-case that requires it.
- `sortTypes: Object<sortKey: sortType>`
- An array of groupBy ID strings, controlling which columns are used to calculate row grouping and aggregation. This information is stored in state since the table is allowed to manipulate the groupBy through user interaction.
- `groupByFn: Function`
- Must be **memoized**
- Allows overriding or adding additional sort types for columns to use. If a column's sort type isn't found on this object, it will default to using the [built-in sort types](TODO).
- For mor information on sort types, see [Sorting](TODO)
- Defaults to [`defaultGroupByFn`](TODO)
- This function is responsible for grouping rows based on the `state.groupBy` keys provided. It's very rare you would need to customize this function.
- `manualGroupBy: Bool`
- Enables groupBy detection and functionality, but does not automatically perform row grouping.
- Turn this on if you wish to implement your own row grouping outside of the table (eg. server-side or manual row grouping/nesting)
- `disableGrouping: Bool`
- Disables groupBy for the entire table.
- `aggregations: Object<aggregationKey: aggregationFn>`
- Must be **memoized**
- Allows overriding or adding additional aggregation functions for use when grouping/aggregating row values. If an aggregation key isn't found on this object, it will default to using the [built-in aggregation functions](TODO)
### Instance Variables
### `Column` Options
The following options are supported on any `Column` object passed to the `columns` options in `useTable()`
- `Aggregated: Function | React.Component => JSX`
- Optional
- Defaults to this column's `Cell` formatter
- 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`
- `disableGrouping: Boolean`
- Defaults to `true`
- If `true`, this column is able to be grouped.
### `Instance` Properties
The following values are provided to the table `instance`:
- `rows: Array<Row>`
- An array of **sorted** rows.
- An array of **grouped and aggregated** rows.
- `preGroupedRows: Array<Row>`
- The array of rows originally used to create the grouped rows.
- `toggleGroupBy: Function(columnID: String, ?set: Bool) => void`
- This function can be used to programmatically set or toggle the groupBy state for a specific column.
### `Column` Properties
The following properties are available on every `Column` object returned by the table instance.
- `canGroupBy: Boolean`
- If `true`, this column is able to be grouped.
- This is resolved from the column having a valid accessor / data model, and not being manually disabled via other `useGroupBy` related options
- `grouped: Boolean`
- If `true`, this column is currently being grouped
- `groupedIndex: Int`
- If this column is currently being grouped, this integer is the index of this column's ID in the table state's `groupBy` array.
- `toggleGroupBy: Function(?set: Bool) => void`
- This function can be used to programmatically set or toggle the groupBy state fo this column.
- `getGroupByToggleProps: Function(props) => props`
- **Required**
- This function is used to resolve any props needed for this column's UI that is responsible for toggling grouping when the user clicks it.
- You can use the `getGroupByToggleProps` hook to extend its functionality.
- Custom props may be passed. **NOTE: Custom props may override built-in sortBy props, so be careful!**
### Example
```js
const state = useTableState({ sortBy: [{ id: 'firstName', desc: true }] })
const state = useTableState({ groupBy: ['firstName'] })
const aggregations = React.useMemo(() => ({
customSum: (values, rows) => values.reduce((sum, next) => sum + next, 0),
}))
const { rows } = useTable(
{
// state[0].sortBy === [{ id: 'firstName', desc: true }]
state,
state, // state[0].groupBy === ['firstName']
manualGroupBy: false,
disableGrouping: false,
aggregations,
},
useSortBy
useGroupBy
)
```
@@ -923,9 +1085,12 @@ export default function MyTable({ manualPageIndex }) {
const state = useTableState(initialState, overrides)
// You can use effects to observe changes to the state
React.useEffect(() => {
console.log('Page Size Changed!', initialState.pageSize)
}, [initialState.pageSize])
React.useEffect(
() => {
console.log('Page Size Changed!', initialState.pageSize)
},
[initialState.pageSize]
)
const { rows } = useTable({
state,
@@ -933,7 +1098,137 @@ export default function MyTable({ manualPageIndex }) {
}
```
<!-- # Guides
# Guides
## Sorting
### Client-Side Sorting
- [Example]()
Client-side sorting can be accomplished by using the `useSortBy` plugin hook. Start by importing the hook from `react-table`:
```diff
-import { useTable } from 'react-table'
+import { useTable, useSortBy } from 'react-table'
```
Next, add the `useSortBy` hook to your `useTable` hook and add the necessary UI pieces we need to make sorting work:
```diff
function MyTable() {
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{
data,
columns,
},
- useSortBy
+ useSortBy
)
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
- <th {...column.getHeaderProps()}>
+ <th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
+ <span>
+ {column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''}
+ </span>
</th>
))}
</tr>
))}
</thead>
<tbody>
{rows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
)}
</tbody>
</table>
)
}
```
### Server-Side Sorting
Server-side sorting can be accomplished by using the `useSortBy` plugin hook in **controlled** mode along with the `useTableState` hook. Start by importing these hooks from `react-table`:
```diff
-import { useTable } from 'react-table'
+import { useTable, useSortBy, useTableState } from 'react-table'
```
Next, add the `useSortBy` and `useTableState` hooks to your `useTable` hook, configure the table state, then add the necessary UI pieces we need to make sorting work:
```diff
function MyTable(data, columns, fetchData) {
+ const state = useTableState()
+ const [{ sortBy }] = state
+ React.useEffect(() => {
+ // When sorting changes, trigger your parent component
+ // or hook to fetch new data with the table state
+ fetchData(state[0])
+ }, [sortBy])
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{
data,
columns,
+ state,
+ manualSorting: true
},
- useSortBy
+ useSortBy
)
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
- <th {...column.getHeaderProps()}>
+ <th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
+ <span>
+ {column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''}
+ </span>
</th>
))}
</tr>
))}
</thead>
<tbody>
{rows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
)}
</tbody>
</table>
)
}
```
<!--
## Client Side Pagination
@@ -1198,14 +1493,11 @@ If you would like to help develop a suggested feature follow these steps:
- Fork this repo
- Install dependencies with `$ yarn`
- Auto-build files as you edit with `$ yarn run watch`
- Implement your changes to files in the `src/` directory
- Run the <a href="https://github.com/tannerlinsley/react-story">React Story</a> locally with `$ yarn run docs`
- View changes as you edit `docs/src`
- Link `react-table` locally with `$ yarn link`
- Auto-build files as you edit with `$ yarn start`
- Implement your changes and tests to files in the `src/` directory
- In any example directory, link to the local `react-table` with `$ yarn link react-table`
- Follow example directions for running. Usually just `$ yarn && yarn start`
- Document your changes in the root `README.md`
- To stage a commit, run `yarn commit`
- Submit PR for review
#### Package Utilities
- `$ yarn run watch` Watches files and builds via babel
- `$ yarn run docs` Runs the storybook server
- `$ yarn run test` Runs the test suite
+1
View File
@@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] }
+5 -6
View File
@@ -1,17 +1,16 @@
const path = require('path');
const path = require('path')
module.exports = {
moduleDirectories: [
'node_modules',
/*
* make 'test/utils' available in tests, e.g.
* __dirname makes 'test/utils' available in tests, e.g.
*
* const {myModule} = require('utils/my-test-helper')
*/
__dirname,
],
rootDir: path.resolve(__dirname, '../..'),
rootDir: path.resolve(__dirname, '../../'),
roots: ['<rootDir>/src', __dirname],
};
transformIgnorePatterns: ['node_modules'],
}
+5 -8
View File
@@ -1,15 +1,12 @@
const commonConfig = require('./jest.common');
const commonConfig = require('./jest.common')
module.exports = {
...commonConfig,
displayName: 'unit',
coverageDirectory: '../../coverage',
testMatch: ['<rootDir>/tests/**/*.js'],
testMatch: ['<rootDir>/src/**/*.test.js'],
transform: {
// '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
},
};
setupFilesAfterEnv: ['./configs/tests/setup.common.js'],
}
+1
View File
@@ -0,0 +1 @@
import 'snapshot-diff/extend-expect'

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -53,13 +53,16 @@ function DefaultColumnFilter({ filterValue, setFilter }) {
function SelectColumnFilter({ filterValue, setFilter, preFilteredRows, id }) {
// Calculate the options for filtering
// using the preFilteredRows
const options = React.useMemo(() => {
const options = new Set()
preFilteredRows.forEach(row => {
options.add(row.values[id])
})
return [...options.values()]
}, [id, preFilteredRows])
const options = React.useMemo(
() => {
const options = new Set()
preFilteredRows.forEach(row => {
options.add(row.values[id])
})
return [...options.values()]
},
[id, preFilteredRows]
)
// Render a multi-select box
return (
@@ -85,15 +88,18 @@ function SelectColumnFilter({ filterValue, setFilter, preFilteredRows, id }) {
function SliderColumnFilter({ filterValue, setFilter, preFilteredRows, id }) {
// Calculate the min and max
// using the preFilteredRows
const [min, max] = React.useMemo(() => {
let min = 0
let max = 0
preFilteredRows.forEach(row => {
min = Math.min(row.values[id], min)
max = Math.max(row.values[id], max)
})
return [min, max]
}, [id, preFilteredRows])
const [min, max] = React.useMemo(
() => {
let min = 0
let max = 0
preFilteredRows.forEach(row => {
min = Math.min(row.values[id], min)
max = Math.max(row.values[id], max)
})
return [min, max]
},
[id, preFilteredRows]
)
return (
<>
@@ -152,19 +158,19 @@ function NumberRangeColumnFilter({ filterValue = [], setFilter }) {
)
}
function fuzzyText(rows, id, filterValue) {
function fuzzyTextFilterFn(rows, id, filterValue) {
return matchSorter(rows, filterValue, { keys: [row => row.values[id]] })
}
// Let the table remove the filter if the string is empty
fuzzyText.autoRemove = val => !val
fuzzyTextFilterFn.autoRemove = val => !val
// Our table component
function Table({ columns, data }) {
const filterTypes = React.useMemo(
() => ({
// Add a new fuzzyText filter type.
fuzzyText,
// Add a new fuzzyTextFilterFn filter type.
fuzzyText: fuzzyTextFilterFn,
// Or, override the default text filter to use
// "startWith"
text: (rows, id, filterValue) => {

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

+260
View File
@@ -0,0 +1,260 @@
import React from 'react'
import styled from 'styled-components'
import { useTable, useGroupBy, useExpanded } from 'react-table'
import makeData from './makeData'
const Styles = styled.div`
padding: 1rem;
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;
}
}
}
`
function Table({ columns, data }) {
const {
getTableProps,
headerGroups,
rows,
prepareRow,
state: [{ groupBy, expanded }],
} = useTable(
{
columns,
data,
},
useGroupBy,
useExpanded
)
// We don't want to render all 2000 rows for this example, so cap
// it at 20 for this use case
const firstPageRows = rows.slice()
return (
<>
<pre>
<code>{JSON.stringify({ groupBy, expanded }, null, 2)}</code>
</pre>
<Legend />
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
// Add the sorting props to control sorting. For this example
// we can add them into the header props
<th {...column.getHeaderProps()}>
{column.canGroupBy ? (
<span {...column.getGroupByToggleProps()}>
{column.grouped ? '🛑' : '👊'}
</span>
) : null}
{column.render('Header')}
{/* Add a sort direction indicator */}
<span>
{column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''}
</span>
</th>
))}
</tr>
))}
</thead>
<tbody>
{firstPageRows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td
{...cell.getCellProps()}
style={{
background: cell.grouped
? '#0aff0082'
: cell.aggregated
? '#ffa50078'
: cell.repeatedValue
? '#ff000042'
: 'white',
}}
>
{cell.grouped ? (
// Add an expander and row count to the grouped cell
<>
<span
style={{
cursor: 'pointer',
}}
onClick={() => row.toggleExpanded()}
>
{row.isExpanded ? '👇' : '👉'}
</span>
{cell.render('Cell')} ({row.subRows.length})
</>
) : cell.aggregated ? (
// Use the Aggregated renderer for cells that have
// aggregated values
cell.render('Aggregated')
) : cell.repeatedValue ? null : ( // For cells with repeated values, render null
// Otherwise, just render the regular cell
cell.render('Cell')
)}
</td>
)
})}
</tr>
)
)}
</tbody>
</table>
<br />
<div>Showing the first 20 results of {rows.length} rows</div>
</>
)
}
function Legend() {
return (
<div
css={`
padding: 0.5rem 0;
`}
>
<span
css={`
display: inline-block;
background: #0aff0082;
padding: 0.5rem;
`}
>
Grouped
</span>{' '}
<span
css={`
display: inline-block;
background: #ffa50078;
padding: 0.5rem;
`}
>
Aggregated
</span>{' '}
<span
css={`
display: inline-block;
background: #ff000042;
padding: 0.5rem;
`}
>
Repeated Value
</span>
</div>
)
}
function roundedMedian(values) {
let min = values[0] || ''
let max = values[0] || ''
values.forEach(value => {
min = Math.min(min, value)
max = Math.max(max, value)
})
return Math.round((min + max) / 2)
}
function App() {
const columns = React.useMemo(
() => [
{
Header: 'Name',
columns: [
{
Header: 'First Name',
accessor: 'firstName',
// Use a two-stage aggregator here to first
// count the total rows being aggregated,
// then sum any of those counts if they are
// aggregated further
aggregate: ['sum', 'count'],
Aggregated: ({ value }) => `${value} Names`,
},
{
Header: 'Last Name',
accessor: 'lastName',
// Use another two-stage aggregator here to
// first count the UNIQUE values from the rows
// being aggregated, then sum those counts if
// they are aggregated further
aggregate: ['sum', 'uniqueCount'],
Aggregated: ({ value }) => `${value} Unique Names`,
},
],
},
{
Header: 'Info',
columns: [
{
Header: 'Age',
accessor: 'age',
// Aggregate the average age of visitors
aggregate: 'average',
Aggregated: ({ value }) => `${value} (avg)`,
},
{
Header: 'Visits',
accessor: 'visits',
// Aggregate the sum of all visits
aggregate: 'sum',
Aggregated: ({ value }) => `${value} (total)`,
},
{
Header: 'Status',
accessor: 'status',
},
{
Header: 'Profile Progress',
accessor: 'progress',
// Use our custom roundedMedian aggregator
aggregate: roundedMedian,
Aggregated: ({ value }) => `${value} (med)`,
},
],
},
],
[]
)
const data = React.useMemo(() => makeData(100), [])
return (
<Styles>
<Table columns={columns} data={data} />
</Styles>
)
}
export default App
@@ -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)
})
@@ -0,0 +1,12 @@
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import * as serviceWorker from './serviceWorker'
ReactDOM.render(<App />, document.getElementById('root'))
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()
@@ -0,0 +1,135 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
)
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
)
})
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config)
}
})
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing
if (installingWorker == null) {
return
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
)
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration)
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.')
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration)
}
}
}
}
}
})
.catch(error => {
console.error('Error during service worker registration:', error)
})
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type')
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload()
})
})
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config)
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
)
})
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister()
})
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"presets": ["react-app"],
"plugins": ["styled-components"]
}
+1
View File
@@ -0,0 +1 @@
SKIP_PREFLIGHT_CHECK=true
+7
View File
@@ -0,0 +1,7 @@
{
"extends": ["react-app", "prettier"],
"rules": {
// "eqeqeq": 0,
// "jsx-a11y/anchor-is-valid": 0
}
}
+23
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*
@@ -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,
]
+6
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/basic)
- `yarn` and `yarn start` to run and edit the example
+35
View File
@@ -0,0 +1,35 @@
{
"private": true,
"scripts": {
"start": "rescripts start",
"build": "rescripts build",
"test": "rescripts test",
"eject": "rescripts eject"
},
"dependencies": {
"namor": "^1.1.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.1",
"react-table": "next",
"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

@@ -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>
@@ -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"
}
+155
View File
@@ -0,0 +1,155 @@
import React from 'react'
import styled from 'styled-components'
import { useTable, useSortBy } from 'react-table'
import makeData from './makeData'
const Styles = styled.div`
padding: 1rem;
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 defaultColumn = {
sort: 'numeric',
}
function Table({ columns, data }) {
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{
columns,
data,
defaultColumn,
debug: true,
},
useSortBy
)
// We don't want to render all 2000 rows for this example, so cap
// it at 20 for this use case
const firstPageRows = rows.slice(0, 20)
return (
<>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(
column =>
console.log(column) || (
// Add the sorting props to control sorting. For this example
// we can add them into the header props
<th
{...column.getHeaderProps(column.getSortByToggleProps())}
>
{column.render('Header')}
{/* Add a sort direction indicator */}
<span>
{column.sorted
? column.sortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</span>
</th>
)
)}
</tr>
))}
</thead>
<tbody>
{firstPageRows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
)
})}
</tr>
)
)}
</tbody>
</table>
<br />
<div>Showing the first 20 results of {rows.length} rows</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',
},
{
Header: 'Visits',
accessor: 'visits',
},
{
Header: 'Status',
accessor: 'status',
},
{
Header: 'Profile Progress',
accessor: 'progress',
},
],
},
],
[]
)
const data = React.useMemo(() => makeData(2000), [])
return (
<Styles>
<Table columns={columns} data={data} />
</Styles>
)
}
export default App
@@ -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;
}
@@ -0,0 +1,40 @@
import namor from 'namor'
const range = len => {
const arr = []
for (let i = 0; i < len; i++) {
arr.push(i)
}
return arr
}
const newPerson = () => {
const statusChance = Math.random()
return {
firstName: namor.generate({ words: 1, numbers: 0 }),
lastName: namor.generate({ words: 1, numbers: 0 }),
age: Math.floor(Math.random() * 30),
visits: Math.floor(Math.random() * 100),
progress: Math.floor(Math.random() * 100),
status:
statusChance > 0.66
? 'relationship'
: statusChance > 0.33
? 'complicated'
: 'single',
}
}
export default function makeData(...lens) {
const makeDataLevel = (depth = 0) => {
const len = lens[depth]
return range(len).map(d => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,7 +1,7 @@
const path = require('path');
const path = require('path')
const lintProject = require('./configs/tests/jest.lint');
const unitProject = require('./configs/tests/jest.unit');
const lintProject = require('./configs/tests/jest.lint')
const unitProject = require('./configs/tests/jest.unit')
module.exports = {
...require('./configs/tests/jest.common'),
@@ -13,4 +13,4 @@ module.exports = {
'jest-watch-typeahead/testname',
'jest-watch-select-projects',
],
};
}
+1 -1
View File
@@ -1,3 +1,3 @@
module.exports = {
'*.js': ['prettier --write', 'git add'],
'*.{md,js,ts,jsx,tsx}': ['prettier --write --no-editorconfig', 'git add'],
}
+2109 -1977
View File
File diff suppressed because it is too large Load Diff
+30 -19
View File
@@ -1,6 +1,6 @@
{
"name": "react-table",
"version": "7.0.0-alpha.8",
"version": "7.0.0-alpha.13",
"description": "A fast, lightweight, opinionated table and datagrid built on React",
"license": "MIT",
"homepage": "https://github.com/tannerlinsley/react-table#readme",
@@ -18,7 +18,7 @@
"module": "dist/index.es.js",
"jsnext:main": "dist/index.es.js",
"scripts": {
"commit": "git-cz",
"commit": "git add . && git-cz",
"test": "is-ci 'test:ci' 'test:dev'",
"test:dev": "jest --watch",
"test:ci": "jest",
@@ -26,7 +26,8 @@
"start": "rollup -c -w",
"prepare": "yarn build",
"release": "yarn publish",
"releaseNext": "yarn publish --tag next"
"releaseNext": "yarn publish --tag next",
"format": "prettier ./**{md,js,jsx,tsx} --write"
},
"files": [
"src/",
@@ -43,31 +44,36 @@
"react-dom": "^16.8.3"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.2.1",
"@babel/preset-env": "^7.2.0",
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.2.0",
"@svgr/rollup": "^4.1.0",
"@testing-library/react": "^8.0.4",
"babel-core": "7.0.0-bridge.0",
"@babel/runtime": "^7.5.5",
"@commitlint/cli": "^8.1.0",
"@commitlint/config-conventional": "^8.1.0",
"@svgr/rollup": "^4.3.2",
"@testing-library/dom": "^5.6.0",
"@testing-library/jest-dom": "^4.0.0",
"@testing-library/react": "^8.0.7",
"babel-eslint": "9.x",
"commitizen": "^4.0.3",
"cross-env": "^5.1.4",
"core-js": "3",
"cross-env": "^5.2.0",
"eslint": "5.x",
"eslint-config-prettier": "^4.3.0",
"eslint-config-react-app": "^4.0.1",
"eslint-config-standard": "^12.0.0",
"eslint-config-standard-react": "^7.0.2",
"eslint-plugin-flowtype": "2.x",
"eslint-plugin-import": "2.x",
"eslint-plugin-jsx-a11y": "6.x",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-promise": "^4.0.0",
"eslint-plugin-react": "7.x",
"eslint-plugin-react-hooks": "1.5.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "7.14.3",
"eslint-plugin-react-hooks": "1.6.1",
"eslint-plugin-standard": "^4.0.0",
"husky": "^3.0.1",
"husky": "^3.0.2",
"is-ci-cli": "^1.1.1",
"jest": "^24.8.0",
"jest-cli": "^24.8.0",
@@ -75,16 +81,21 @@
"jest-watch-select-projects": "^0.1.2",
"jest-watch-typeahead": "^0.3.1",
"lint-staged": "^9.2.1",
"prop-types": "^15.5.0",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"rollup": "^0.68.0",
"rollup-plugin-babel": "^4.1.0",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-peer-deps-external": "^2.2.0",
"rollup-plugin-uglify": "^6.0.2"
"rollup-plugin-uglify": "^6.0.2",
"snapshot-diff": "^0.5.2"
},
"config": {
"commitizen": {
"path": "node_modules/cz-conventional-changelog"
}
}
},
"browserslist": "> 0.25%, not dead"
}
+7 -1
View File
@@ -1,6 +1,12 @@
module.exports = {
trailingComma: 'es5',
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
trailingComma: 'es5',
bracketSpacing: true,
jsxBracketSameLine: false,
arrowParens: 'avoid',
endOfLine: 'auto',
}
+9 -9
View File
@@ -12,17 +12,17 @@ export default [
output: {
file: pkg.main,
format: 'cjs',
sourcemap: true
sourcemap: true,
},
plugins: [
external(),
babel({
exclude: 'node_modules/**'
exclude: 'node_modules/**',
}),
resolve(),
commonjs(),
uglify()
]
uglify(),
],
},
{
input: 'src/index.js',
@@ -30,12 +30,12 @@ export default [
output: {
file: pkg.module,
format: 'es',
sourcemap: true
sourcemap: true,
},
plugins: [
babel({
exclude: ['/**/node_modules/**']
})
]
}
exclude: ['/**/node_modules/**'],
}),
],
},
]
+20
View File
@@ -5,3 +5,23 @@ export function sum(values, rows) {
export function average(values, rows) {
return Math.round((sum(values, rows) / values.length) * 100) / 100
}
export function median(values) {
let min = values[0] || ''
let max = values[0] || ''
values.forEach(value => {
min = Math.min(min, value)
max = Math.max(max, value)
})
return (min + max) / 2
}
export function uniqueCount(values) {
return new Set(values).size
}
export function count(values) {
return values.length
}
+2 -3
View File
@@ -63,6 +63,7 @@ exact.autoRemove = val => typeof val === 'undefined'
export const equals = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
// eslint-disable-next-line eqeqeq
return rowValue == filterValue
})
}
@@ -88,6 +89,4 @@ export const between = (rows, id, filterValue) => {
}
between.autoRemove = val =>
console.log(val) ||
!val ||
(typeof val[0] !== 'number' && typeof val[1] !== 'number')
!val || (typeof val[0] !== 'number' && typeof val[1] !== 'number')
@@ -0,0 +1,172 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders a basic table 1`] = `
<DocumentFragment>
<table
class=""
>
<thead>
<tr
class=""
>
<th
class=""
colspan="2"
>
Name
</th>
<th
class=""
colspan="4"
>
Info
</th>
</tr>
<tr
class=""
>
<th
class=""
colspan="1"
>
First Name
</th>
<th
class=""
colspan="1"
>
Last Name
</th>
<th
class=""
colspan="1"
>
Age
</th>
<th
class=""
colspan="1"
>
Visits
</th>
<th
class=""
colspan="1"
>
Status
</th>
<th
class=""
colspan="1"
>
Profile Progress
</th>
</tr>
</thead>
<tbody>
<tr
class=""
>
<td
class=""
>
tanner
</td>
<td
class=""
>
linsley
</td>
<td
class=""
>
29
</td>
<td
class=""
>
100
</td>
<td
class=""
>
In Relationship
</td>
<td
class=""
>
50
</td>
</tr>
<tr
class=""
>
<td
class=""
>
derek
</td>
<td
class=""
>
perkins
</td>
<td
class=""
>
40
</td>
<td
class=""
>
40
</td>
<td
class=""
>
Single
</td>
<td
class=""
>
80
</td>
</tr>
<tr
class=""
>
<td
class=""
>
joe
</td>
<td
class=""
>
bergevin
</td>
<td
class=""
>
45
</td>
<td
class=""
>
20
</td>
<td
class=""
>
Complicated
</td>
<td
class=""
>
10
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;
+126
View File
@@ -0,0 +1,126 @@
import '@testing-library/react/cleanup-after-each'
import '@testing-library/jest-dom/extend-expect'
// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required
import React from 'react'
import { render } from '@testing-library/react'
import { useTable } from '../useTable'
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 }) {
// Use the state and functions returned from useTable to build your UI
const { getTableProps, headerGroups, rows, prepareRow } = useTable({
columns,
data,
})
// Render the UI for your table
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody>
{rows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <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 basic table', () => {
const { getByText, asFragment } = 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()
})
-229
View File
@@ -1,229 +0,0 @@
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { getBy } from '../utils'
const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
Cell: PropTypes.any,
Header: PropTypes.any,
})
),
defaultColumn: PropTypes.any,
}
// Find the depth of the columns
function findMaxDepth(columns, depth = 0) {
return columns.reduce((prev, curr) => {
if (curr.columns) {
return Math.max(prev, findMaxDepth(curr.columns, depth + 1))
}
return depth
}, 0)
}
function decorateColumn(column, defaultColumn, parent, depth, index) {
// Apply the defaultColumn
column = { ...defaultColumn, ...column }
// First check for string accessor
let { id, accessor, Header } = column
if (typeof accessor === 'string') {
id = id || accessor
const accessorString = accessor
accessor = row => getBy(row, accessorString)
}
if (!id && typeof Header === 'string') {
id = Header
}
if (!id && column.columns) {
console.error(column)
throw new Error('A column ID (or unique "Header" value) is required!')
}
if (!id) {
console.error(column)
throw new Error('A column ID (or string accessor) is required!')
}
column = {
Header: ({ id }) => id,
Cell: ({ value }) => value,
show: true,
...column,
id,
accessor,
parent,
depth,
index,
}
return column
}
// Build the visible columns, headers and flat column list
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
})
}
// Build the header groups from the bottom up
function makeHeaderGroups(columns, maxDepth, defaultColumn) {
const headerGroups = []
const removeChildColumns = column => {
delete column.columns
if (column.parent) {
removeChildColumns(column.parent)
}
}
columns.forEach(removeChildColumns)
const buildGroup = (columns, depth = 0) => {
const headerGroup = {
headers: [],
}
const parentColumns = []
const hasParents = columns.some(col => col.parent)
columns.forEach(column => {
const isFirst = !parentColumns.length
let latestParentColumn = [...parentColumns].reverse()[0]
// If the column has a parent, add it if necessary
if (column.parent) {
if (isFirst || latestParentColumn.originalID !== column.parent.id) {
parentColumns.push({
...column.parent,
originalID: column.parent.id,
id: [column.parent.id, parentColumns.length].join('_'),
})
}
} else if (hasParents) {
// If other columns have parents, add a place holder if necessary
const placeholderColumn = decorateColumn(
{
originalID: [column.id, 'placeholder', maxDepth - depth].join('_'),
id: [
column.id,
'placeholder',
maxDepth - depth,
parentColumns.length,
].join('_'),
},
defaultColumn
)
if (
isFirst ||
latestParentColumn.originalID !== placeholderColumn.originalID
) {
parentColumns.push(placeholderColumn)
}
}
// Establish the new columns[] relationship on the parent
if (column.parent || hasParents) {
latestParentColumn = [...parentColumns].reverse()[0]
latestParentColumn.columns = latestParentColumn.columns || []
if (!latestParentColumn.columns.includes(column)) {
latestParentColumn.columns.push(column)
}
}
headerGroup.headers.push(column)
})
headerGroups.push(headerGroup)
if (parentColumns.length) {
buildGroup(parentColumns)
}
}
buildGroup(columns)
return headerGroups.reverse()
}
export const useColumns = props => {
const {
debug,
columns: userColumns,
defaultColumn = {},
state: [{ groupBy }],
} = props
PropTypes.checkPropTypes(propTypes, props, 'property', 'useColumns')
const { columns, headerGroups, headers } = useMemo(() => {
if (debug) console.info('getColumns')
// Decorate All the columns
let columnTree = decorateColumnTree(userColumns, defaultColumn)
// Get the flat list of all columns
let columns = flattenBy(columnTree, 'columns')
columns = [
...groupBy.map(g => columns.find(col => col.id === g)),
...columns.filter(col => !groupBy.includes(col.id)),
]
// Get headerGroups
const headerGroups = makeHeaderGroups(
columns,
findMaxDepth(columnTree),
defaultColumn
)
const headers = flattenBy(headerGroups, 'headers')
return {
columns,
headerGroups,
headers,
}
}, [debug, defaultColumn, groupBy, userColumns])
return {
...props,
columns,
headerGroups,
headers,
}
function flattenBy(columns, childKey) {
const flatColumns = []
const recurse = columns => {
columns.forEach(d => {
if (!d[childKey]) {
flatColumns.push(d)
} else {
recurse(d[childKey])
}
})
}
recurse(columns)
return flatColumns
}
}
-66
View File
@@ -1,66 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
const propTypes = {
subRowsKey: PropTypes.string,
}
export const useRows = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useRows')
const { debug, columns, subRowsKey = 'subRows', data } = props
const accessedRows = React.useMemo(() => {
if (debug) console.info('getAccessedRows')
// Access the row's data
const accessRow = (originalRow, i, depth = 0) => {
// Keep the original reference around
const original = originalRow
// Process any subRows
const subRows = originalRow[subRowsKey]
? originalRow[subRowsKey].map((d, i) => accessRow(d, i, depth + 1))
: undefined
const row = {
original,
index: i,
path: [i], // used to create a key for each row even if not nested
subRows,
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
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 = {}
columns.forEach(column => {
row.values[column.id] = column.accessor
? column.accessor(originalRow, i, { subRows, depth, data })
: undefined
})
return row
}
// Use the resolved data
return data.map((d, i) => accessRow(d, i))
}, [debug, data, subRowsKey, columns])
return {
...props,
rows: accessedRows,
}
}
+250 -89
View File
@@ -1,14 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
//
import { applyHooks, applyPropHooks, mergeProps, flexRender } from '../utils'
import {
applyHooks,
applyPropHooks,
mergeProps,
flexRender,
decorateColumnTree,
makeHeaderGroups,
findMaxDepth,
flattenBy,
} from '../utils'
import { useTableState } from './useTableState'
import { useColumns } from './useColumns'
import { useRows } from './useRows'
const propTypes = {
// General
data: PropTypes.array.isRequired,
columns: PropTypes.arrayOf(
PropTypes.shape({
Cell: PropTypes.any,
Header: PropTypes.any,
})
).isRequired,
defaultColumn: PropTypes.object,
subRowsKey: PropTypes.string,
debug: PropTypes.bool,
}
@@ -23,8 +39,9 @@ export const useTable = (props, ...plugins) => {
let {
data,
state: userState,
useColumns: userUseColumns = useColumns,
useRows: userUseRows = useRows,
columns: userColumns,
defaultColumn = {},
subRowsKey = 'subRows',
debug,
} = props
@@ -36,87 +53,207 @@ export const useTable = (props, ...plugins) => {
// But use the users state if provided
const state = userState || defaultState
// These are hooks that plugins can use right before render
const hooks = {
beforeRender: [],
columns: [],
headers: [],
headerGroups: [],
rows: [],
row: [],
getTableProps: [],
getRowProps: [],
getHeaderGroupProps: [],
getHeaderProps: [],
getCellProps: [],
}
// The initial api
let api = {
let instanceRef = React.useRef({})
Object.assign(instanceRef.current, {
...props,
data,
state,
hooks,
plugins,
}
if (debug) console.time('hooks')
// Loop through plugins to build the api out
api = [userUseColumns, userUseRows, ...plugins]
.filter(Boolean)
.reduce((prev, next) => next(prev), api)
if (debug) console.timeEnd('hooks')
// Determine column visibility
api.columns.forEach(column => {
column.visible =
typeof column.show === 'function' ? column.show(api) : !!column.show
hooks: {
columnsBeforeHeaderGroups: [],
useMain: [],
useColumns: [],
useHeaders: [],
useHeaderGroups: [],
useRows: [],
prepareRow: [],
getTableProps: [],
getRowProps: [],
getHeaderGroupProps: [],
getHeaderProps: [],
getCellProps: [],
},
})
// Allow hooks to decorate columns
if (debug) console.time('hooks.columns')
api.columns = applyHooks(api.hooks.columns, api.columns, api)
if (debug) console.timeEnd('hooks.columns')
// Allow plugins to register hooks
if (debug) console.time('plugins')
plugins.filter(Boolean).forEach(plugin => {
plugin(instanceRef.current.hooks)
})
if (debug) console.timeEnd('plugins')
// Allow hooks to decorate headers
if (debug) console.time('hooks.headers')
api.headers = applyHooks(api.hooks.headers, api.headers, api)
if (debug) console.timeEnd('hooks.headers')
;[...api.columns, ...api.headers].forEach(column => {
// Give columns/headers rendering power
column.render = (type, userProps = {}) => {
const Comp = typeof type === 'string' ? column[type] : type
// Compute columns, headerGroups and headers
const columnInfo = React.useMemo(
() => {
if (debug) console.info('buildColumns/headerGroup/headers')
// Decorate All the columns
let columnTree = decorateColumnTree(userColumns, defaultColumn)
if (typeof Comp === 'undefined') {
throw new Error(renderErr)
// Get the flat list of all columns
let columns = flattenBy(columnTree, 'columns')
// Allow hooks to decorate columns
if (debug) console.time('hooks.columnsBeforeHeaderGroups')
columns = applyHooks(
instanceRef.current.hooks.columnsBeforeHeaderGroups,
columns,
instanceRef.current
)
if (debug) console.timeEnd('hooks.columnsBeforeHeaderGroups')
// Make the headerGroups
const headerGroups = makeHeaderGroups(
columns,
findMaxDepth(columnTree),
defaultColumn
)
const headers = flattenBy(headerGroups, 'headers')
return {
columns,
headerGroups,
headers,
}
},
[debug, defaultColumn, userColumns]
)
// Place the columns, headerGroups and headers on the api
Object.assign(instanceRef.current, columnInfo)
// Access the row model
instanceRef.current.rows = React.useMemo(
() => {
if (debug) console.time('getAccessedRows')
// Access the row's data
const accessRow = (originalRow, i, depth = 0) => {
// Keep the original reference around
const original = originalRow
// Process any subRows
const subRows = originalRow[subRowsKey]
? originalRow[subRowsKey].map((d, i) => accessRow(d, i, depth + 1))
: []
const row = {
original,
index: i,
path: [i], // used to create a key for each row even if not nested
subRows,
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
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 = {}
instanceRef.current.columns.forEach(column => {
row.values[column.id] = column.accessor
? column.accessor(originalRow, i, { subRows, depth, data })
: undefined
})
return row
}
return flexRender(Comp, {
...api,
...column,
...userProps,
})
}
// Use the resolved data
const accessedData = data.map((d, i) => accessRow(d, i))
if (debug) console.timeEnd('getAccessedRows')
return accessedData
},
[debug, data, subRowsKey]
)
// Give columns/headers a default getHeaderProps
column.getHeaderProps = props =>
mergeProps(
{
key: ['header', column.id].join('_'),
colSpan: column.columns ? column.columns.length : 1,
},
applyPropHooks(api.hooks.getHeaderProps, column, api),
props
)
// Determine column visibility
instanceRef.current.columns.forEach(column => {
column.visible =
typeof column.show === 'function'
? column.show(instanceRef.current)
: !!column.show
})
// Allow hooks to decorate headerGroups
if (debug) console.time('hooks.headerGroups')
api.headerGroups = applyHooks(
api.hooks.headerGroups,
api.headerGroups,
api
).filter((headerGroup, i) => {
if (debug) console.time('hooks.useMain')
instanceRef.current = applyHooks(
instanceRef.current.hooks.useMain,
instanceRef.current
)
if (debug)
console.timeEnd('hooks.useMain')
// // Allow hooks to decorate columns
// if (debug) console.time('hooks.useColumns')
// instanceRef.current.columns = applyHooks(
// instanceRef.current.hooks.useColumns,
// instanceRef.current.columns,
// instanceRef.current
// )
// if (debug) console.timeEnd('hooks.useColumns')
// // Allow hooks to decorate headers
// if (debug) console.time('hooks.useHeaders')
// instanceRef.current.headers = applyHooks(
// instanceRef.current.hooks.useHeaders,
// instanceRef.current.headers,
// instanceRef.current
// )
// if (debug) console.timeEnd('hooks.useHeaders')
;[...instanceRef.current.columns, ...instanceRef.current.headers].forEach(
column => {
// Give columns/headers rendering power
column.render = (type, userProps = {}) => {
const Comp = typeof type === 'string' ? column[type] : type
if (typeof Comp === 'undefined') {
throw new Error(renderErr)
}
return flexRender(Comp, {
...instanceRef.current,
...column,
...userProps,
})
}
// Give columns/headers a default getHeaderProps
column.getHeaderProps = props =>
mergeProps(
{
key: ['header', column.id].join('_'),
colSpan: column.columns ? column.columns.length : 1,
},
applyPropHooks(
instanceRef.current.hooks.getHeaderProps,
column,
instanceRef.current
),
props
)
}
)
// // Allow hooks to decorate headerGroups
// if (debug) console.time('hooks.useHeaderGroups')
// instanceRef.current.headerGroups = applyHooks(
// instanceRef.current.hooks.useHeaderGroups,
// instanceRef.current.headerGroups,
// instanceRef.current
// )
instanceRef.current.headerGroups.filter((headerGroup, i) => {
// Filter out any headers and headerGroups that don't have visible columns
headerGroup.headers = headerGroup.headers.filter(header => {
const recurse = columns =>
@@ -139,7 +276,11 @@ export const useTable = (props, ...plugins) => {
{
key: [`header${i}`].join('_'),
},
applyPropHooks(api.hooks.getHeaderGroupProps, headerGroup, api),
applyPropHooks(
instanceRef.current.hooks.getHeaderGroupProps,
headerGroup,
instanceRef.current
),
props
)
return true
@@ -147,29 +288,36 @@ export const useTable = (props, ...plugins) => {
return false
})
if (debug) console.timeEnd('hooks.headerGroups')
// if (debug) console.timeEnd('hooks.useHeaderGroups')
// Run the rows (this could be a dangerous hook with a ton of data)
if (debug) console.time('hooks.rows')
api.rows = applyHooks(api.hooks.rows, api.rows, api)
if (debug) console.timeEnd('hooks.rows')
if (debug) console.time('hooks.useRows')
instanceRef.current.rows = applyHooks(
instanceRef.current.hooks.useRows,
instanceRef.current.rows,
instanceRef.current
)
if (debug) console.timeEnd('hooks.useRows')
// The prepareRow function is absolutely necessary and MUST be called on
// any rows the user wishes to be displayed.
api.prepareRow = row => {
instanceRef.current.prepareRow = row => {
const { path } = row
row.getRowProps = props =>
mergeProps(
{ key: ['row', ...path].join('_') },
applyPropHooks(api.hooks.getRowProps, row, api),
applyPropHooks(
instanceRef.current.hooks.getRowProps,
row,
instanceRef.current
),
props
)
// need to apply any row specific hooks (useExpanded requires this)
applyHooks(api.hooks.row, row, api)
const visibleColumns = api.columns.filter(column => column.visible)
const visibleColumns = instanceRef.current.columns.filter(
column => column.visible
)
// Build the cells for each row
row.cells = visibleColumns.map(column => {
@@ -181,12 +329,16 @@ export const useTable = (props, ...plugins) => {
// Give each cell a getCellProps base
cell.getCellProps = props => {
const columnPathStr = [path, column.id].join('_')
const columnPathStr = [...path, column.id].join('_')
return mergeProps(
{
key: ['cell', columnPathStr].join('_'),
},
applyPropHooks(api.hooks.getCellProps, cell, api),
applyPropHooks(
instanceRef.current.hooks.getCellProps,
cell,
instanceRef.current
),
props
)
}
@@ -200,7 +352,7 @@ export const useTable = (props, ...plugins) => {
}
return flexRender(Comp, {
...api,
...instanceRef.current,
...cell,
...userProps,
})
@@ -208,10 +360,19 @@ export const useTable = (props, ...plugins) => {
return cell
})
// need to apply any row specific hooks (useExpanded requires this)
applyHooks(instanceRef.current.hooks.prepareRow, row, instanceRef.current)
}
api.getTableProps = userProps =>
mergeProps(applyPropHooks(api.hooks.getTableProps, api), userProps)
instanceRef.current.getTableProps = userProps =>
mergeProps(
applyPropHooks(
instanceRef.current.hooks.getTableProps,
instanceRef.current
),
userProps
)
return api
return instanceRef.current
}
-2
View File
@@ -1,8 +1,6 @@
import * as utils from './utils'
export { utils }
export { useTable } from './hooks/useTable'
export { useColumns } from './hooks/useColumns'
export { useRows } from './hooks/useRows'
export { useTableState, defaultState } from './hooks/useTableState'
export { useExpanded } from './plugin-hooks/useExpanded'
export { useFilters } from './plugin-hooks/useFilters'
@@ -0,0 +1,252 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders a sortable table 1`] = `
"Snapshot Diff:
- First value
+ Second value
@@ -31,11 +31,13 @@
colspan=\\"1\\"
style=\\"cursor: pointer;\\"
title=\\"Toggle SortBy\\"
>
First Name
- <span />
+ <span>
+ 🔼
+ </span>
</th>
<th
class=\\"\\"
colspan=\\"1\\"
style=\\"cursor: pointer;\\"
@@ -87,104 +89,104 @@
class=\\"\\"
>
<td
class=\\"\\"
>
- firstName: tanner
+ firstName: derek
</td>
<td
class=\\"\\"
>
- lastName: linsley
+ lastName: perkins
</td>
<td
class=\\"\\"
>
- age: 29
+ age: 40
</td>
<td
class=\\"\\"
>
- visits: 100
+ visits: 40
</td>
<td
class=\\"\\"
>
- status: In Relationship
+ status: Single
</td>
<td
class=\\"\\"
>
- progress: 50
+ progress: 80
</td>
</tr>
<tr
class=\\"\\"
>
<td
class=\\"\\"
>
- firstName: derek
+ firstName: joe
</td>
<td
class=\\"\\"
>
- lastName: perkins
+ lastName: bergevin
</td>
<td
class=\\"\\"
>
- age: 40
+ age: 45
</td>
<td
class=\\"\\"
>
- visits: 40
+ visits: 20
</td>
<td
class=\\"\\"
>
- status: Single
+ status: Complicated
</td>
<td
class=\\"\\"
>
- progress: 80
+ progress: 10
</td>
</tr>
<tr
class=\\"\\"
>
<td
class=\\"\\"
>
- firstName: joe
+ firstName: tanner
</td>
<td
class=\\"\\"
>
- lastName: bergevin
+ lastName: linsley
</td>
<td
class=\\"\\"
>
- age: 45
+ age: 29
</td>
<td
class=\\"\\"
>
- visits: 20
+ visits: 100
</td>
<td
class=\\"\\"
>
- status: Complicated
+ status: In Relationship
</td>
<td
class=\\"\\"
>
- progress: 10
+ progress: 50
</td>
</tr>
</tbody>
</table>
</DocumentFragment>"
`;
exports[`renders a sortable table 2`] = `
"Snapshot Diff:
- First value
+ Second value
@@ -32,11 +32,11 @@
style=\\"cursor: pointer;\\"
title=\\"Toggle SortBy\\"
>
First Name
<span>
- 🔼
+ 🔽
</span>
</th>
<th
class=\\"\\"
colspan=\\"1\\"
@@ -89,36 +89,36 @@
class=\\"\\"
>
<td
class=\\"\\"
>
- firstName: derek
+ firstName: tanner
</td>
<td
class=\\"\\"
>
- lastName: perkins
+ lastName: linsley
</td>
<td
class=\\"\\"
>
- age: 40
+ age: 29
</td>
<td
class=\\"\\"
>
- visits: 40
+ visits: 100
</td>
<td
class=\\"\\"
>
- status: Single
+ status: In Relationship
</td>
<td
class=\\"\\"
>
- progress: 80
+ progress: 50
</td>
</tr>
<tr
class=\\"\\"
>
@@ -157,36 +157,36 @@
class=\\"\\"
>
<td
class=\\"\\"
>
- firstName: tanner
+ firstName: derek
</td>
<td
class=\\"\\"
>
- lastName: linsley
+ lastName: perkins
</td>
<td
class=\\"\\"
>
- age: 29
+ age: 40
</td>
<td
class=\\"\\"
>
- visits: 100
+ visits: 40
</td>
<td
class=\\"\\"
>
- status: In Relationship
+ status: Single
</td>
<td
class=\\"\\"
>
- progress: 50
+ progress: 80
</td>
</tr>
</tbody>
</table>
</DocumentFragment>"
`;
@@ -1,43 +1,49 @@
import '@testing-library/react/cleanup-after-each'
import '@testing-library/jest-dom/extend-expect'
// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required
import React from 'react'
import styled from 'styled-components'
import { useTable, useSortBy } from 'react-table'
import { render, fireEvent } from '@testing-library/react'
import { useTable } from '../../hooks/useTable'
import { useSortBy } from '../useSortBy'
import makeData from './makeData'
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,
},
]
const Styles = styled.div`
padding: 1rem;
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 defaultColumn = {
Cell: ({ value, column: { id } }) => `${id}: ${value}`,
}
function Table({ columns, data }) {
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{
columns,
data,
defaultColumn,
},
useSortBy
)
@@ -118,13 +124,22 @@ function App() {
[]
)
const data = React.useMemo(() => makeData(20), [])
return (
<Styles>
<Table columns={columns} data={data} />
</Styles>
)
return <Table columns={columns} data={data} />
}
export default App
test('renders a sortable table', () => {
const { getByText, asFragment } = render(<App />)
const beforeSort = asFragment()
fireEvent.click(getByText('First Name'))
const afterSort1 = asFragment()
fireEvent.click(getByText('First Name'))
const afterSort2 = asFragment()
expect(beforeSort).toMatchDiffSnapshot(afterSort1)
expect(afterSort1).toMatchDiffSnapshot(afterSort2)
})
+33 -30
View File
@@ -14,8 +14,12 @@ const propTypes = {
paginateSubRows: PropTypes.bool,
}
export const useExpanded = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useExpanded')
export const useExpanded = hooks => {
hooks.useMain.push(useMain)
}
function useMain(instance) {
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useExpanded')
const {
debug,
@@ -24,7 +28,7 @@ export const useExpanded = props => {
hooks,
state: [{ expanded }, setState],
paginateSubRows = true,
} = props
} = instance
const toggleExpandedByPath = (path, set) => {
return setState(old => {
@@ -38,50 +42,49 @@ export const useExpanded = props => {
}, actions.toggleExpanded)
}
hooks.row.push(row => {
hooks.prepareRow.push(row => {
const { path } = row
row.toggleExpanded = set => toggleExpandedByPath(path, set)
return row
})
const expandedRows = useMemo(() => {
if (debug) console.info('getExpandedRows')
const expandedRows = useMemo(
() => {
if (debug) console.info('getExpandedRows')
const expandedRows = []
const expandedRows = []
// Here we do some mutation, but it's the last stage in the
// immutable process so this is safe
const handleRow = (row, depth = 0, parentPath = []) => {
// Compute some final state for the row
const path = [...parentPath, row.index]
// Here we do some mutation, but it's the last stage in the
// immutable process so this is safe
const handleRow = row => {
row.isExpanded =
(row.original && row.original[manualExpandedKey]) ||
getBy(expanded, row.path)
row.path = path
row.depth = depth
if (paginateSubRows || (!paginateSubRows && row.depth === 0)) {
expandedRows.push(row)
}
row.isExpanded =
(row.original && row.original[manualExpandedKey]) ||
getBy(expanded, path)
row.canExpand = row.subRows && !!row.subRows.length
if (paginateSubRows || (!paginateSubRows && row.depth === 0)) {
expandedRows.push(row)
if (row.isExpanded && row.subRows && row.subRows.length) {
row.subRows.forEach((row, i) => handleRow(row))
}
return row
}
if (row.isExpanded && row.subRows && row.subRows.length) {
row.subRows.forEach((row, i) => handleRow(row, depth + 1, path))
}
rows.forEach(row => handleRow(row))
return row
}
rows.forEach(row => handleRow(row))
return expandedRows
}, [debug, rows, manualExpandedKey, expanded, paginateSubRows])
return expandedRows
},
[debug, rows, manualExpandedKey, expanded, paginateSubRows]
)
const expandedDepth = findExpandedDepth(expanded)
return {
...props,
...instance,
toggleExpandedByPath,
expandedDepth,
rows: expandedRows,
+96 -104
View File
@@ -14,19 +14,20 @@ const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
filterFn: PropTypes.func,
filterAll: PropTypes.bool,
canFilter: PropTypes.bool,
disableFilters: PropTypes.bool,
Filter: PropTypes.any,
})
),
filterFn: PropTypes.func,
manualFilters: PropTypes.bool,
}
export const useFilters = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useFilters')
export const useFilters = hooks => {
hooks.useMain.push(useMain)
}
function useMain(instance) {
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useFilters')
const {
debug,
@@ -35,9 +36,8 @@ export const useFilters = props => {
filterTypes: userFilterTypes,
manualFilters,
disableFilters,
hooks,
state: [{ filters }, setState],
} = props
} = instance
const setFilter = (id, updater) => {
const column = columns.find(d => d.id === id)
@@ -48,14 +48,12 @@ export const useFilters = props => {
filterTypes
)
const { autoRemove } = filterMethod
return setState(old => {
const newFilter =
typeof updater === 'function' ? updater(old.filters[id]) : updater
//
if (shouldAutoRemove(autoRemove, newFilter)) {
if (shouldAutoRemove(filterMethod.autoRemove, newFilter)) {
const { [id]: remove, ...newFilters } = old.filters
return {
...old,
@@ -81,13 +79,13 @@ export const useFilters = props => {
Object.keys(newFilters).forEach(id => {
const newFilter = newFilters[id]
const column = columns.find(d => d.id === id)
const autoRemove = getFilterMethod(
const filterMethod = getFilterMethod(
column.filter,
userFilterTypes || {},
filterTypes
)
if (shouldAutoRemove(autoRemove, newFilter)) {
if (shouldAutoRemove(filterMethod.autoRemove, newFilter)) {
delete newFilters[id]
}
})
@@ -99,28 +97,24 @@ export const useFilters = props => {
}, actions.setAllFilters)
}
hooks.columns.push(columns => {
columns.forEach(column => {
const { id, accessor, canFilter } = column
columns.forEach(column => {
const { id, accessor, disableFilters: columnDisableFilters } = column
// Determine if a column is filterable
column.canFilter = accessor
? getFirstDefined(
canFilter,
disableFilters === true ? false : undefined,
true
)
: false
// Determine if a column is filterable
column.canFilter = accessor
? getFirstDefined(
columnDisableFilters,
disableFilters === true ? false : undefined,
true
)
: false
// Provide the column a way of updating the filter value
column.setFilter = val => setFilter(column.id, val)
// Provide the column a way of updating the filter value
column.setFilter = val => setFilter(column.id, val)
// Provide the current filter value to the column for
// convenience
column.filterValue = filters[id]
})
return columns
// Provide the current filter value to the column for
// convenience
column.filterValue = filters[id]
})
// TODO: Create a filter cache for incremental high speed multi-filtering
@@ -128,90 +122,83 @@ export const useFilters = props => {
// cache for each row group (top-level rows, and each row's recursive subrows)
// This would make multi-filtering a lot faster though. Too far?
const filteredRows = React.useMemo(() => {
if (manualFilters || !Object.keys(filters).length) {
return rows
}
const filteredRows = React.useMemo(
() => {
if (manualFilters || !Object.keys(filters).length) {
return rows
}
if (debug) console.info('getFilteredRows')
if (debug) console.info('getFilteredRows')
// Filters top level and nested rows
const filterRows = rows => {
let filteredRows = rows
// Filters top level and nested rows
const filterRows = rows => {
let filteredRows = rows
filteredRows = Object.entries(filters).reduce(
(filteredSoFar, [columnID, filterValue]) => {
// Find the filters column
const column = columns.find(d => d.id === columnID)
filteredRows = Object.entries(filters).reduce(
(filteredSoFar, [columnID, filterValue]) => {
// Find the filters column
const column = columns.find(d => d.id === columnID)
column.preFilteredRows = filteredSoFar
if (!column) {
return filteredSoFar
}
// Don't filter hidden columns or columns that have had their filters disabled
if (!column || column.filterable === false) {
return filteredSoFar
}
column.preFilteredRows = filteredSoFar
const columnFilter = column.filter || filterTypes.text
// Look up filter functions in this order:
// column function
// column string lookup on user filters
// column string lookup on built-in filters
// default function
// default string lookup on user filters
// default string lookup on built-in filters
const filterMethod = getFilterMethod(
columnFilter,
userFilterTypes || {},
filterTypes
)
if (!filterMethod) {
console.warn(
`Could not find a valid 'column.filter' for column with the ID: ${
column.id
}.`
const filterMethod = getFilterMethod(
column.filter,
userFilterTypes || {},
filterTypes
)
return filteredSoFar
if (!filterMethod) {
console.warn(
`Could not find a valid 'column.filter' for column with the ID: ${
column.id
}.`
)
return filteredSoFar
}
// Pass the rows, id, filterValue and column to the filterMethod
// to get the filtered rows back
return filterMethod(filteredSoFar, columnID, filterValue, column)
},
rows
)
// Apply the filter to any subRows
// We technically could do this recursively in the above loop,
// but that would severely hinder the API for the user, since they
// would be required to do that recursion in some scenarios
filteredRows = filteredRows.map(row => {
if (!row.subRows) {
return row
}
return {
...row,
subRows: filterRows(row.subRows),
}
})
// Pass the rows, id, filterValue and column to the filterMethod
// to get the filtered rows back
return filterMethod(filteredSoFar, columnID, filterValue, column)
},
rows
)
// then filter any rows without subcolumns because it would be strange to show
filteredRows = filteredRows.filter(row => {
if (!row.subRows) {
return true
}
return row.subRows.length > 0
})
// Apply the filter to any subRows
// We technically could do this recursively in the above loop,
// but that would severely hinder the API for the user, since they
// would be required to do that recursion in some scenarios
filteredRows = filteredRows.map(row => {
if (!row.subRows) {
return row
}
return {
...row,
subRows: filterRows(row.subRows),
}
})
return filteredRows
}
// then filter any rows without subcolumns because it would be strange to show
filteredRows = filteredRows.filter(row => {
if (!row.subRows) {
return true
}
return row.subRows.length > 0
})
return filteredRows
}
return filterRows(rows)
}, [manualFilters, filters, debug, rows, columns, userFilterTypes])
return filterRows(rows)
},
[manualFilters, filters, debug, rows, columns, userFilterTypes]
)
return {
...props,
...instance,
setFilter,
setAllFilters,
preFilteredRows: rows,
@@ -224,5 +211,10 @@ function shouldAutoRemove(autoRemove, value) {
}
function getFilterMethod(filter, userFilterTypes, filterTypes) {
return isFunction(filter) || userFilterTypes[filter] || filterTypes[filter]
return (
isFunction(filter) ||
userFilterTypes[filter] ||
filterTypes[filter] ||
filterTypes.text
)
}
+163 -105
View File
@@ -19,43 +19,70 @@ const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
aggregate: PropTypes.func,
canGroupBy: PropTypes.bool,
aggregate: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.func, PropTypes.string])
),
]),
disableGrouping: PropTypes.bool,
Aggregated: PropTypes.any,
})
),
groupByFn: PropTypes.func,
manualGrouping: PropTypes.bool,
disableGrouping: PropTypes.bool,
aggregations: PropTypes.object,
}
export const useGroupBy = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useGroupBy')
export const useGroupBy = hooks => {
hooks.columnsBeforeHeaderGroups.push(columnsBeforeHeaderGroups)
hooks.useMain.push(useMain)
}
function columnsBeforeHeaderGroups(columns, { state: [{ groupBy }] }) {
// Sort grouped columns to the start of the column list
// before the headers are built
return [
...groupBy.map(g => columns.find(col => col.id === g)),
...columns.filter(col => !groupBy.includes(col.id)),
]
}
function useMain(instance) {
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useGroupBy')
const {
debug,
rows,
columns,
headers,
groupByFn = defaultGroupByFn,
manualGroupBy,
disableGrouping,
aggregations: userAggregations = {},
hooks,
state: [{ groupBy }, setState],
} = props
} = instance
columns.forEach(column => {
const { id, accessor, canGroupBy } = column
const { id, accessor, disableGrouping: columnDisableGrouping } = column
column.grouped = groupBy.includes(id)
column.groupedIndex = groupBy.indexOf(id)
column.canGroupBy = accessor
? getFirstDefined(
canGroupBy,
columnDisableGrouping,
disableGrouping === true ? false : undefined,
true
)
: false
if (column.canGroupBy) {
column.toggleGroupBy = () => toggleGroupBy(column.id)
}
column.Aggregated = column.Aggregated || column.Cell
})
@@ -76,120 +103,151 @@ export const useGroupBy = props => {
}, actions.toggleGroupBy)
}
hooks.columns.push(columns => {
columns.forEach(column => {
if (column.canGroupBy) {
column.toggleGroupBy = () => toggleGroupBy(column.id)
}
})
return columns
})
hooks.getGroupByToggleProps = []
const addGroupByToggleProps = (columns, api) => {
columns.forEach(column => {
const { canGroupBy } = column
column.getGroupByToggleProps = props => {
return mergeProps(
{
onClick: canGroupBy
? e => {
e.persist()
column.toggleGroupBy()
}
: undefined,
style: {
cursor: canGroupBy ? 'pointer' : undefined,
},
title: 'Toggle GroupBy',
//
;[...columns, ...headers].forEach(column => {
const { canGroupBy } = column
column.getGroupByToggleProps = props => {
return mergeProps(
{
onClick: canGroupBy
? e => {
e.persist()
column.toggleGroupBy()
}
: undefined,
style: {
cursor: canGroupBy ? 'pointer' : undefined,
},
applyPropHooks(api.hooks.getGroupByToggleProps, column, api),
props
)
}
title: 'Toggle GroupBy',
},
applyPropHooks(instance.hooks.getGroupByToggleProps, column, instance),
props
)
}
})
hooks.prepareRow.push(row => {
row.cells.forEach(cell => {
// Grouped cells are in the groupBy and the pivot cell for the row
cell.grouped = cell.column.grouped && cell.column.id === row.groupByID
// Repeated cells are any columns in the groupBy that are not grouped
cell.repeatedValue = !cell.grouped && cell.column.grouped
// Aggregated cells are not grouped, not repeated, but still have subRows
cell.aggregated = !cell.grouped && !cell.repeatedValue && row.canExpand
})
return columns
}
return row
})
hooks.columns.push(addGroupByToggleProps)
hooks.headers.push(addGroupByToggleProps)
const groupedRows = useMemo(() => {
if (manualGroupBy || !groupBy.length) {
return rows
}
if (debug) console.info('getGroupedRows')
// Find the columns that can or are aggregating
// Uses each column to aggregate rows into a single value
const aggregateRowsToValues = rows => {
const values = {}
columns.forEach(column => {
const columnValues = rows.map(d => d.values[column.id])
let aggregate =
userAggregations[column.aggregate] ||
aggregations[column.aggregate] ||
column.aggregate
if (typeof aggregate === 'function') {
values[column.id] = aggregate(columnValues, rows)
} else if (aggregate) {
throw new Error(
`Invalid aggregate "${aggregate}" passed to column with ID: "${
column.id
}"`
)
} else {
values[column.id] = columnValues[0]
}
})
return values
}
// Recursively group the data
const groupRecursively = (rows, groupBy, depth = 0) => {
// This is the last level, just return the rows
if (depth >= groupBy.length) {
const groupedRows = useMemo(
() => {
if (manualGroupBy || !groupBy.length) {
return rows
}
// Group the rows together for this level
let groupedRows = Object.entries(groupByFn(rows, groupBy[depth])).map(
([groupByVal, subRows], index) => {
// Recurse to sub rows before aggregation
subRows = groupRecursively(subRows, groupBy, depth + 1)
if (debug) console.info('getGroupedRows')
// Find the columns that can or are aggregating
const values = aggregateRowsToValues(subRows)
// Uses each column to aggregate rows into a single value
const aggregateRowsToValues = (rows, isSourceRows) => {
const values = {}
const row = {
groupByID: groupBy[depth],
groupByVal,
values,
subRows,
depth,
index,
columns.forEach(column => {
// Don't aggregate columns that are in the groupBy
if (groupBy.includes(column.id)) {
values[column.id] = rows[0] ? rows[0].values[column.id] : null
return
}
return row
const columnValues = rows.map(d => d.values[column.id])
let aggregator = column.aggregate
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 (isSourceRows) {
aggregator = aggregator[1]
} else {
aggregator = aggregator[0]
}
}
let aggregateFn =
typeof aggregator === 'function'
? aggregator
: userAggregations[aggregator] || aggregations[aggregator]
if (aggregateFn) {
values[column.id] = aggregateFn(columnValues, rows)
} else if (aggregator) {
console.info({ column })
throw new Error(
`React Table: Invalid aggregate option for column listed above`
)
} else {
values[column.id] = null
}
})
return values
}
// Recursively group the data
const groupRecursively = (rows, depth = 0, parentPath = []) => {
// This is the last level, just return the rows
if (depth >= groupBy.length) {
return rows
}
)
return groupedRows
}
const columnID = groupBy[depth]
// Assign the new data
return groupRecursively(rows, groupBy)
}, [
manualGroupBy,
groupBy,
debug,
rows,
columns,
userAggregations,
groupByFn,
])
// Group the rows together for this level
let groupedRows = groupByFn(rows, columnID)
// Recurse to sub rows before aggregation
groupedRows = Object.entries(groupedRows).map(
([groupByVal, subRows], index) => {
const path = [...parentPath, groupByVal]
subRows = groupRecursively(subRows, depth + 1, path)
const values = aggregateRowsToValues(
subRows,
depth + 1 >= groupBy.length
)
const row = {
groupByID: columnID,
groupByVal,
values,
subRows,
depth,
index,
path,
}
return row
}
)
return groupedRows
}
// Assign the new data
return groupRecursively(rows)
},
[manualGroupBy, groupBy, debug, rows, columns, userAggregations, groupByFn]
)
return {
...props,
...instance,
toggleGroupBy,
rows: groupedRows,
preGroupedRows: rows,
}
}
+116 -132
View File
@@ -1,4 +1,4 @@
import { useMemo } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { addActions, actions } from '../actions'
@@ -20,41 +20,42 @@ const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
sortBy: PropTypes.func,
defaultSortDesc: PropTypes.bool,
sortType: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
sortDescFirst: PropTypes.bool,
disableSorting: PropTypes.bool,
})
),
orderByFn: PropTypes.func,
sortTypes: PropTypes.object,
defaultSortType: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
manualSorting: PropTypes.bool,
disableSorting: PropTypes.bool,
defaultSortDesc: PropTypes.bool,
disableMultiSort: PropTypes.bool,
disableSortRemove: PropTypes.bool,
disableMultiRemove: PropTypes.bool,
}
export const useSortBy = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useSortBy')
export const useSortBy = hooks => {
hooks.useMain.push(useMain)
}
function useMain(instance) {
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useSortBy')
const {
debug,
rows,
columns,
orderByFn = defaultOrderByFn,
defaultSort = 'alphanumeric',
sortTypes: userSortTypes,
manualSorting,
disableSorting,
defaultSortDesc,
disableSortRemove,
disableMultiRemove,
disableMultiSort,
hooks,
state: [{ sortBy }, setState],
plugins,
} = props
} = instance
// If useSortBy should probably come after useFilters for
// the best performance, so let's hint to the user about that...
@@ -70,32 +71,21 @@ export const useSortBy = props => {
)
}
columns.forEach(column => {
const { accessor, canSortBy } = column
column.canSortBy = accessor
? getFirstDefined(
canSortBy,
disableSorting === true ? false : undefined,
true
)
: false
})
// Add custom hooks
hooks.getSortByToggleProps = []
// Updates sorting based on a columnID, desc flag and multi flag
const toggleSortByID = (columnID, desc, multi) => {
const toggleSortBy = (columnID, desc, multi) => {
return setState(old => {
const { sortBy } = old
// Find the column for this columnID
const column = columns.find(d => d.id === columnID)
const resolvedDefaultSortDesc = getFirstDefined(
column.defaultSortDesc,
defaultSortDesc
)
const { sortDescFirst } = column
// Find any existing sortBy for this column
const existingSortBy = sortBy.find(d => d.id === columnID)
const existingIndex = sortBy.findIndex(d => d.id == columnID)
const existingIndex = sortBy.findIndex(d => d.id === columnID)
const hasDescDefined = typeof desc !== 'undefined' && desc !== null
let newSortBy = []
@@ -127,8 +117,8 @@ export const useSortBy = props => {
!hasDescDefined && // Must not be setting desc
(multi ? !disableMultiRemove : true) && // If multi, don't allow if disableMultiRemove
((existingSortBy && // Finally, detect if it should indeed be removed
(existingSortBy.desc && !resolvedDefaultSortDesc)) ||
(!existingSortBy.desc && resolvedDefaultSortDesc))
(existingSortBy.desc && !sortDescFirst)) ||
(!existingSortBy.desc && sortDescFirst))
) {
action = 'remove'
}
@@ -137,7 +127,7 @@ export const useSortBy = props => {
newSortBy = [
{
id: columnID,
desc: hasDescDefined ? desc : resolvedDefaultSortDesc,
desc: hasDescDefined ? desc : sortDescFirst,
},
]
} else if (action === 'add') {
@@ -145,7 +135,7 @@ export const useSortBy = props => {
...sortBy,
{
id: columnID,
desc: hasDescDefined ? desc : resolvedDefaultSortDesc,
desc: hasDescDefined ? desc : sortDescFirst,
},
]
} else if (action === 'toggle') {
@@ -170,127 +160,121 @@ export const useSortBy = props => {
}, actions.sortByChange)
}
hooks.columns.push(columns => {
columns.forEach(column => {
if (column.canSortBy) {
column.toggleSortBy = (desc, multi) =>
toggleSortByID(column.id, desc, multi)
}
})
return columns
})
// Add the getSortByToggleProps method to columns and headers
;[...instance.columns, ...instance.headers].forEach(column => {
const { accessor, disableSorting: columnDisableSorting, id } = column
hooks.getSortByToggleProps = []
const addSortByToggleProps = (columns, api) => {
columns.forEach(column => {
const { canSortBy } = column
column.getSortByToggleProps = props => {
return mergeProps(
{
onClick: canSortBy
? e => {
e.persist()
column.toggleSortBy(
undefined,
!api.disableMultiSort && e.shiftKey
)
}
: undefined,
style: {
cursor: canSortBy ? 'pointer' : undefined,
},
title: 'Toggle SortBy',
},
applyPropHooks(api.hooks.getSortByToggleProps, column, api),
props
const canSort = accessor
? getFirstDefined(
columnDisableSorting,
disableSorting === true ? false : undefined,
true
)
}
})
return columns
}
: false
hooks.columns.push(addSortByToggleProps)
hooks.headers.push(addSortByToggleProps)
column.canSort = canSort
if (column.canSort) {
column.toggleSortBy = (desc, multi) =>
toggleSortBy(column.id, desc, multi)
}
column.getSortByToggleProps = props => {
return mergeProps(
{
onClick: canSort
? e => {
e.persist()
column.toggleSortBy(
undefined,
!instance.disableMultiSort && e.shiftKey
)
}
: undefined,
style: {
cursor: canSort ? 'pointer' : undefined,
},
title: 'Toggle SortBy',
},
applyPropHooks(instance.hooks.getSortByToggleProps, column, instance),
props
)
}
// Mutate columns to reflect sorting state
columns.forEach(column => {
const { id } = column
column.sorted = sortBy.find(d => d.id === id)
column.sortedIndex = sortBy.findIndex(d => d.id === id)
column.sortedDesc = column.sorted ? column.sorted.desc : undefined
})
const sortedRows = useMemo(() => {
if (manualSorting || !sortBy.length) {
return rows
}
if (debug) console.info('getSortedRows')
const sortedRows = React.useMemo(
() => {
if (manualSorting || !sortBy.length) {
return rows
}
if (debug) console.time('getSortedRows')
const sortTypesByColumnID = {}
const sortData = rows => {
// Use the orderByFn to compose multiple sortBy's together.
// This will also perform a stable sorting using the row index
// if needed.
const sortedData = orderByFn(
rows,
sortBy.map(sort => {
// Support custom sorting methods for each column
const { sortType } = columns.find(d => d.id === sort.id)
columns.forEach(col => {
sortTypesByColumnID[col.id] = col.sortBy
})
// Look up sortBy functions in this order:
// column function
// column string lookup on user sortType
// column string lookup on built-in sortType
// default function
// default string lookup on user sortType
// default string lookup on built-in sortType
const sortMethod =
isFunction(sortType) ||
(userSortTypes || {})[sortType] ||
sortTypes[sortType] ||
sortTypes.alphanumeric
const sortData = rows => {
// Use the orderByFn to compose multiple sortBy's together.
// This will also perform a stable sorting using the row index
// if needed.
const sortedData = orderByFn(
rows,
sortBy.map(sort => {
// Support custom sorting methods for each column
const columnSort = sortTypesByColumnID[sort.id]
// Return the correct sortFn
return (a, b) =>
sortMethod(a.values[sort.id], b.values[sort.id], sort.desc)
}),
// Map the directions
sortBy.map(sort => {
// Detect and use the sortInverted option
const { sortInverted } = columns.find(d => d.id === sort.id)
// Look up sortBy functions in this order:
// column function
// column string lookup on user sortType
// column string lookup on built-in sortType
// default function
// default string lookup on user sortType
// default string lookup on built-in sortType
const sortMethod =
isFunction(columnSort) ||
(userSortTypes || {})[columnSort] ||
sortTypes[columnSort] ||
isFunction(defaultSort) ||
(userSortTypes || {})[defaultSort] ||
sortTypes[defaultSort]
if (sortInverted) {
return sort.desc
}
// Return the correct sortFn
return (a, b) =>
sortMethod(a.values[sort.id], b.values[sort.id], sort.desc)
}),
// Map the directions
sortBy.map(d => !d.desc)
)
return !sort.desc
})
)
// If there are sub-rows, sort them
sortedData.forEach(row => {
if (!row.subRows) {
return
}
row.subRows = sortData(row.subRows)
})
// If there are sub-rows, sort them
sortedData.forEach(row => {
if (!row.subRows) {
return
}
row.subRows = sortData(row.subRows)
})
return sortedData
}
if (debug) console.timeEnd('getSortedRows')
return sortData(rows)
}, [
manualSorting,
sortBy,
debug,
columns,
rows,
orderByFn,
userSortTypes,
defaultSort,
])
return sortedData
}
return sortData(rows)
},
[manualSorting, sortBy, debug, columns, rows, orderByFn, userSortTypes]
)
return {
...props,
...instance,
toggleSortBy,
rows: sortedRows,
preSortedRows: rows,
}
}
+179 -6
View File
@@ -1,5 +1,153 @@
import React from 'react'
// Find the depth of the columns
export function findMaxDepth(columns, depth = 0) {
return columns.reduce((prev, curr) => {
if (curr.columns) {
return Math.max(prev, findMaxDepth(curr.columns, depth + 1))
}
return depth
}, 0)
}
export function decorateColumn(column, defaultColumn, parent, depth, index) {
// Apply the defaultColumn
column = { ...defaultColumn, ...column }
// First check for string accessor
let { id, accessor, Header } = column
if (typeof accessor === 'string') {
id = id || accessor
const accessorString = accessor
accessor = row => getBy(row, accessorString)
}
if (!id && typeof Header === 'string') {
id = Header
}
if (!id && column.columns) {
console.error(column)
throw new Error('A column ID (or unique "Header" value) is required!')
}
if (!id) {
console.error(column)
throw new Error('A column ID (or string accessor) is required!')
}
column = {
Header: ({ id }) => id,
Cell: ({ value }) => typeof value !== 'undefined' ? value : '',
show: true,
...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
})
}
// Build the header groups from the bottom up
export function makeHeaderGroups(columns, maxDepth, defaultColumn) {
const headerGroups = []
const removeChildColumns = column => {
delete column.columns
if (column.parent) {
removeChildColumns(column.parent)
}
}
columns.forEach(removeChildColumns)
const buildGroup = (columns, depth = 0) => {
const headerGroup = {
headers: [],
}
const parentColumns = []
const hasParents = columns.some(col => col.parent)
columns.forEach(column => {
const isFirst = !parentColumns.length
let latestParentColumn = [...parentColumns].reverse()[0]
// If the column has a parent, add it if necessary
if (column.parent) {
if (isFirst || latestParentColumn.originalID !== column.parent.id) {
parentColumns.push({
...column.parent,
originalID: column.parent.id,
id: [column.parent.id, parentColumns.length].join('_'),
})
}
} else if (hasParents) {
// If other columns have parents, add a place holder if necessary
const placeholderColumn = decorateColumn(
{
originalID: [column.id, 'placeholder', maxDepth - depth].join('_'),
id: [
column.id,
'placeholder',
maxDepth - depth,
parentColumns.length,
].join('_'),
},
defaultColumn
)
if (
isFirst ||
latestParentColumn.originalID !== placeholderColumn.originalID
) {
parentColumns.push(placeholderColumn)
}
}
// Establish the new columns[] relationship on the parent
if (column.parent || hasParents) {
latestParentColumn = [...parentColumns].reverse()[0]
latestParentColumn.columns = latestParentColumn.columns || []
if (!latestParentColumn.columns.includes(column)) {
latestParentColumn.columns.push(column)
}
}
headerGroup.headers.push(column)
})
headerGroups.push(headerGroup)
if (parentColumns.length) {
buildGroup(parentColumns)
}
}
buildGroup(columns)
return headerGroups.reverse()
}
export function getBy(obj, path, def) {
if (!path) {
return obj
@@ -36,12 +184,11 @@ export function getFirstDefined(...args) {
}
}
export function defaultGroupByFn(rows, grouper) {
export function defaultGroupByFn(rows, columnID) {
return rows.reduce((prev, row, i) => {
const resKey =
typeof grouper === 'function'
? grouper(row.values, i)
: row.values[grouper]
// TODO: Might want to implement a key serializer here so
// irregular column values can still be grouped if needed?
const resKey = `${row.values[columnID]}`
prev[resKey] = Array.isArray(prev[resKey]) ? prev[resKey] : []
prev[resKey].push(row)
return prev
@@ -112,7 +259,15 @@ export const mergeProps = (...groups) => {
}
export const applyHooks = (hooks, initial, ...args) =>
hooks.reduce((prev, next) => next(prev, ...args), initial)
hooks.reduce((prev, next) => {
const nextValue = next(prev, ...args)
if (typeof nextValue === 'undefined') {
throw new Error(
'React Table: A hook just returned undefined! This is not allowed.'
)
}
return nextValue
}, initial)
export const applyPropHooks = (hooks, ...args) =>
hooks.reduce((prev, next) => mergeProps(prev, next(...args)), {})
@@ -137,6 +292,24 @@ export function isFunction(a) {
}
}
export function flattenBy(columns, childKey) {
const flatColumns = []
const recurse = columns => {
columns.forEach(d => {
if (!d[childKey]) {
flatColumns.push(d)
} else {
recurse(d[childKey])
}
})
}
recurse(columns)
return flatColumns
}
//
function makePathArray(obj) {
+2299 -1187
View File
File diff suppressed because it is too large Load Diff