mirror of
https://github.com/gosticks/react-table.git
synced 2026-08-19 16:30:21 +00:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f01c32216 | ||
|
|
5145a632c9 | ||
|
|
8508a6567d | ||
|
|
1e80dd85d6 | ||
|
|
3cbb49fc37 | ||
|
|
110e04bf61 | ||
|
|
2b30c84abc | ||
|
|
fd0fe43ad9 | ||
|
|
5975e0c4cd | ||
|
|
e7bbddbb0f | ||
|
|
49a1023467 | ||
|
|
7167350921 | ||
|
|
77cb0d97a8 | ||
|
|
0672de486a | ||
|
|
e0f83d32fe | ||
|
|
3aaa5b9b1e | ||
|
|
352de348fe | ||
|
|
21d3853dba | ||
|
|
095979b9db | ||
|
|
f52d5af115 | ||
|
|
037c32345f | ||
|
|
6ad0d4e0c0 | ||
|
|
512790bc1c | ||
|
|
118b873531 | ||
|
|
39c2b152f6 |
@@ -17,32 +17,31 @@ Hooks for building **lightweight, fast and extendable datagrids** for React
|
||||
<a href="https://twitter.com/tannerlinsley" target="\_parent">
|
||||
<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://patreon.com/tannerlinsley">
|
||||
<img width="180" alt="" src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/become-a-patron.png" />
|
||||
</a>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## Features
|
||||
|
||||
- Lightweight
|
||||
- Lightweight (4kb - 10kb depending on features and tree-shaking)
|
||||
- Headless (100% customizable, Bring-your-own-UI)
|
||||
- Client-side & Server-side pagination support
|
||||
- Sorting (Multi and Stable)
|
||||
- Filters
|
||||
- Pivoting & Aggregation
|
||||
- Row Selection
|
||||
- Row Expansion
|
||||
- Fully controllable
|
||||
- Extensible via hooks
|
||||
- Extensible via hook-based plugin system
|
||||
- <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
|
||||
|
||||
## [See Examples](#examples)
|
||||
|
||||
## Versions
|
||||
|
||||
- This documentation is for version 7.
|
||||
- [View the Changelog](https://github.com/tannerlinsley/react-table/blob/master/CHANGELOG.md)
|
||||
- Previous versions:
|
||||
- [6.x.x Readme](https://github.com/tannerlinsley/react-table/tree/v6/)
|
||||
- [5.x.x Readme](https://github.com/tannerlinsley/react-table/blob/ad7d31cd3978eb45da7c6194dbab93c1e9a8594d/README.md)
|
||||
|
||||
## Sponsors
|
||||
|
||||
**React Table v7** is being built and maintained by me, @tannerlinsley and I am always in need of more Patreon support to keep this project afloat. If you would like to contribute to my Patreon goal for v7 and beyond, [visit my Patreon and help me out!](https://patreon.com/tannerlinsley).
|
||||
@@ -52,17 +51,12 @@ Hooks for building **lightweight, fast and extendable datagrids** for React
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://patreon.com/tannerlinsley" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/platinum.png">
|
||||
<img width='200' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/platinum.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=react_table" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/sponsor-retool.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://patreon.com/tannerlinsley" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/platinum-placeholder.png">
|
||||
<img width='480' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/sponsor-retool.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -74,22 +68,17 @@ Hooks for building **lightweight, fast and extendable datagrids** for React
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://patreon.com/tannerlinsley" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/gold.png">
|
||||
<img width='200' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/gold.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://nozzle.io" target="_blank">
|
||||
<img width="300" src="https://nozzle.io/img/logo-blue.png">
|
||||
<img width='230' src="https://nozzle.io/img/logo-blue.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://zappi.com/web" target="_blank">
|
||||
<img width="300" src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/sponsor-zappi.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://patreon.com/tannerlinsley" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/gold-placeholder.png">
|
||||
<img width='230' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/sponsor-zappi.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -101,22 +90,7 @@ Hooks for building **lightweight, fast and extendable datagrids** for React
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://patreon.com/tannerlinsley" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/silver.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://patreon.com/tannerlinsley" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/silver-placeholder.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://patreon.com/tannerlinsley" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/silver-placeholder.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://patreon.com/tannerlinsley" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/silver-placeholder.png">
|
||||
<img width='200' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/silver.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
@@ -128,6 +102,51 @@ Hooks for building **lightweight, fast and extendable datagrids** for React
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<a href="https://patreon.com/tannerlinsley">
|
||||
<img width='200' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/supporters.png" />
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Nicholas Kaufmann</li>
|
||||
<li>Pekka</li>
|
||||
<li>Jon Eickmeier</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<a href="https://patreon.com/tannerlinsley">
|
||||
<img width='200' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/fans.png" />
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Sortmy.games</li>
|
||||
<li>Hugo Meissner</li>
|
||||
<li>Benoit Leger-Derville</li>
|
||||
<li>Thomas Funk</li>
|
||||
<li>Dan Houle</li>
|
||||
<li>David Pickut</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<a href="https://patreon.com/tannerlinsley">
|
||||
<img width="150" alt="" src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/become-a-patron.png" />
|
||||
</a>
|
||||
|
||||
# Documentation
|
||||
|
||||
- [Installation](#installation)
|
||||
@@ -163,11 +182,15 @@ import {
|
||||
|
||||
# Examples
|
||||
|
||||
- [Basic](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/basic)
|
||||
- [Basic - Client Side](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/basic-client-side)
|
||||
- [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)
|
||||
- [Pagination - Client Side](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/pagination-client-side)
|
||||
- [Row Selection - Client Side](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/row-selection-client-side)
|
||||
- [Expanding - Client Side](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/expanding-client-side)
|
||||
- [Sub Components](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/sub-components)
|
||||
- [Editable Data](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/editable-cells)
|
||||
|
||||
# Concepts
|
||||
|
||||
@@ -272,7 +295,7 @@ The following options are supported via the main options object passed to `useTa
|
||||
- Required
|
||||
- Must be **memoized**
|
||||
- The core columns configuration object for the entire table.
|
||||
- Supports nested `columns` arrays via the `column.children` key
|
||||
- Supports nested `columns` arrays via the column's `columns` key, eg. `[{ Header: 'My Group', columns: [...] }]`
|
||||
- `data: Array<any>`
|
||||
- Required
|
||||
- Must be **memoized**
|
||||
@@ -289,6 +312,11 @@ 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 }`
|
||||
- `initialRowStateKey: String`
|
||||
- Optional
|
||||
- Defaults to `initialState`
|
||||
- This key is used to look for the initial state of a row when initializing the `rowState` for a`data` array.
|
||||
- If the value located at `row[initialRowStateKey]` is falsey, `{}` will be used instead.
|
||||
- `debug: Bool`
|
||||
- Optional
|
||||
- A flag to turn on debug mode.
|
||||
@@ -355,6 +383,21 @@ The following properties are available on the table instance returned from `useT
|
||||
- **Required**
|
||||
- This function is responsible for lazily preparing a row for rendering. Any row that you intend to render in your table needs to be passed to this function **before every render**.
|
||||
- **Why?** Since table data could potentially be very large, it can become very expensive to compute all of the necessary state for every row to be rendered regardless if it actually is rendered or not (for example if you are paginating or virtualizing the rows, you may only have a few rows visible at any given moment). This function allows only the rows you intend to display to be computed and prepped with the correct state.
|
||||
- `rowPaths: Array<string>`
|
||||
- An array containing the stringified `path` of every original row in the table. eg. If a row has a path of `[0, 3, 2]`, its stringified path would be `0.3.2`.
|
||||
- This array is used by many plugin hooks including `useRowSelect` to manage row selection state
|
||||
- Only rows that exist on the original `data` array will have a path in this array. Rows created by `useGroupBy`'s aggregations and grouping are not included in this array, since they do not reference an original data row.
|
||||
- `flatRows: Array<Row>`
|
||||
- An array of all rows, including subRows which have been flattened into the order in which they were detected (depth first)
|
||||
- This can be helpful in calculating total row counts that must include subRows
|
||||
- `setRowState: Function(rowPath, updater: Function | any) => void`
|
||||
- This function can be used to update the internal state for any row.
|
||||
- Pass it a valid `rowPath` array and `updater`. The `updater` may be a value or function, similar to `React.useState`'s usage.
|
||||
- If `updater` is a function, it will be passed the previous value
|
||||
- `setCellState: Function(rowPath, columnID, updater: Function | any) => void`
|
||||
- This function can be used to update the internal state for any cell.
|
||||
- Pass it a valid `rowPath` array, `columnID` and `updater`. The `updater` may be a value or function, similar to `React.useState`'s usage.
|
||||
- If `updater` is a function, it will be passed the previous value
|
||||
|
||||
### `HeaderGroup` Properties
|
||||
|
||||
@@ -401,6 +444,19 @@ The following additional properties are available on every `row` object returned
|
||||
- This function is used to resolve any props needed for this row.
|
||||
- You can use the `getRowProps` hook to extend its functionality.
|
||||
- Custom props may be passed. **NOTE: Custom props will override built-in table props, so be careful!**
|
||||
- `index: Int`
|
||||
- The index of the original row in the `data` array that was passed to `useTable`. If this row is a subRow, it is the original index within the parent row's subRows array
|
||||
- `original: Object`
|
||||
- The original row object from the `data` array that was used to materialize this row.
|
||||
- `path: Array<string>`
|
||||
- This array is the sequential path of indices one could use to navigate to it, eg. a row path of `[3, 1, 0]` would mean that it is the **first** subRow of a parent that is the **second** subRow of a parent that is the **fourth** row in the original `data` array.
|
||||
- This array is used with plugin hooks like `useExpanded` and `useGroupBy` to compute expanded states for individual rows.
|
||||
- `subRows: Array<Row>`
|
||||
- If subRows were detect on the original data object, this will be an array of those materialized row objects.
|
||||
- `state: Object`
|
||||
|
||||
- The current state of the row. It's lifespan is attached to that of the original `data` array. When the raw `data` is changed, this state value is reset to the row's initial value (using the `initialRowStateKey` option).
|
||||
- Can be updated via `instance.setRowState` or the row's `setState` function.
|
||||
|
||||
### Cell Properties
|
||||
|
||||
@@ -419,6 +475,7 @@ The following additional properties are available on every `Cell` object returne
|
||||
- You can use the `getCellProps` hook to extend its functionality.
|
||||
- Custom props may be passed. **NOTE: Custom props will override built-in table props, so be careful!**
|
||||
- `render: Function(type: String | Function | Component, ?props)`
|
||||
- **Required**
|
||||
- This function is used to render content in context of a cell.
|
||||
- If `type` is a string, will render using the `column[type]` renderer. React Table ships with a default `Cell` renderer. Other renderers like `Aggregated` are available via hooks like `useFilters`.
|
||||
- If a function or component is passed instead of a string, it will be be passed the table instance and cell model as props and is expected to return any valid JSX.
|
||||
@@ -538,7 +595,7 @@ The following options are supported via the main options object passed to `useTa
|
||||
- `sortTypes: Object<sortKey: sortType>`
|
||||
- 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)
|
||||
- For more information on sort types, see [Sorting](TODO)
|
||||
|
||||
### Column Options
|
||||
|
||||
@@ -1058,49 +1115,169 @@ function App() {
|
||||
- Plugin Hook
|
||||
- Optional
|
||||
|
||||
`useExpanded` is the hook that implements **row expanding**. It is most often used with `useGroupBy` to expand grouped rows, but is not limited to that use-case. It supports expanding rows both via internal table state and also via a hard-coded key on the raw row model.
|
||||
`useExpanded` is the hook that implements **row expanding**. It is most often used with `useGroupBy` to expand grouped rows or on its own with nested `subRows` in tree-li,ke `data` sets, but is not limited to these use-cases. It supports expanding rows both via internal table state and also via a hard-coded key on the raw row model.
|
||||
|
||||
### Table Options
|
||||
|
||||
The following options are supported via the main options object passed to `useTable(options)`
|
||||
|
||||
- `state[0].expanded: Object<[pathIndex]: Boolean | ExpandedStateObject>`
|
||||
- Optional
|
||||
- Must be **memoized**
|
||||
- An nested object of expanded paths.
|
||||
- A `pathIndex` can be set as the key and its value set to `true` to expand that row's subRows into view. For example, if `{ '3': true }` was passed as the `expanded` state, the **4th row in the original data array** would be expanded.
|
||||
- For nested expansion, you may **use another object** instead of a Boolean to expand sub rows. For example, if `{ '3': { '5' : true }}` was passed as the `expanded` state, then the **6th subRow of the 4th row and the 4th row of the original data array** would be expanded.
|
||||
- This information is stored in state since the table is allowed to manipulate the filter through user interaction.
|
||||
- `subRowsKey: String`
|
||||
- Required
|
||||
- Defaults to `subRows`
|
||||
- React Table will use this key when materializing the final row object. It also uses this key to infer sub-rows from the raw data.
|
||||
- See [Grouping and Aggregation](#grouping-and-aggregation) for more information
|
||||
- `paginateSubRows: Bool`
|
||||
- Defaults to `true`
|
||||
- Optional
|
||||
- See the [useTable hook](#table-options) for more details
|
||||
- `nestExpandedRows: Bool`
|
||||
- Optional
|
||||
- Defaults to `false`
|
||||
- If set to `false`, expanded rows will not be paginated. Thus, any expanded subrows would potentially increase the size of any given page by the amount of total expanded subrows on the page.
|
||||
- `manualExpandedKey: String`
|
||||
- Optional
|
||||
- Defaults to `expanded`
|
||||
- This string is used as the key to detect manual expanded state on any given row. For example, if a raw data row like `{ name: 'Tanner Linsley', friends: [...], expanded: true}` was detected, it would be forcibly expanded, regardless of state.
|
||||
- This string is used as the key to detect manual expanded state on any given row. For example, if a raw data row like `{ name: 'Tanner Linsley', friends: [...], expanded: true}` was detected, it would always be expanded, regardless of state.
|
||||
|
||||
### Instance Variables
|
||||
### Instance Properties
|
||||
|
||||
The following values are provided to the table `instance`:
|
||||
The following properties are available on the table instance returned from `useTable`
|
||||
|
||||
- `rows: Array<Row>`
|
||||
- An array of **sorted** rows.
|
||||
|
||||
### Row Properties
|
||||
|
||||
The following additional properties are available on every `row` object returned by the table instance.
|
||||
|
||||
- `isExpanded: Bool`
|
||||
- If `true`, this row is in an expanded state.
|
||||
- `toggleExpanded: Function(?isExpanded: Bool) => void`
|
||||
- This function will toggle the expanded state of a row between `true` and `false` or, if an `isExpanded` boolean is passed to the function, it will be set as the new `isExpanded` value.
|
||||
- Rows with a hard-coded `manualExpandedKey` (defaults to `expanded`) set to `true` are not affected by this function or the internal expanded state.
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
const state = useTableState({ expanded: { '3': true, '5': { '2': true } } })
|
||||
function Table({ columns: userColumns, data }) {
|
||||
const {
|
||||
getTableProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
state: [{ expanded }],
|
||||
} = useTable(
|
||||
{
|
||||
columns: userColumns,
|
||||
data,
|
||||
},
|
||||
useExpanded // Use the useExpanded plugin hook
|
||||
)
|
||||
|
||||
const { rows } = useTable(
|
||||
{
|
||||
// state[0].sortBy === { '3': true, '5': { '2': true } }
|
||||
state,
|
||||
},
|
||||
useExpanded
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<pre>
|
||||
<code>{JSON.stringify({ expanded }, null, 2)}</code>
|
||||
</pre>
|
||||
<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>
|
||||
<br />
|
||||
<div>Showing the first 20 results of {rows.length} rows</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const columns = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
// Build our expander column
|
||||
Header: () => null, // No header, please
|
||||
id: 'expander', // Make sure it has an ID
|
||||
Cell: ({ row }) =>
|
||||
// Use the row.canExpand and row.getExpandedToggleProps prop getter
|
||||
// to build the toggle for expanding a row
|
||||
row.canExpand ? (
|
||||
<span
|
||||
{...row.getExpandedToggleProps({
|
||||
style: {
|
||||
// We can even use the row.depth property
|
||||
// and paddingLeft to indicate the depth
|
||||
// of the row
|
||||
paddingLeft: `${row.depth * 2}rem`,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{row.isExpanded ? '👇' : '👉'}
|
||||
</span>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
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(5, 5, 5), [])
|
||||
|
||||
return <Table columns={columns} data={data} />
|
||||
}
|
||||
```
|
||||
|
||||
## `usePagination`
|
||||
@@ -1135,7 +1312,7 @@ The following options are supported via the main options object passed to `useTa
|
||||
- Normally, any changes detected to `rows`, `state.filters`, `state.groupBy`, or `state.sortBy` will trigger the `pageIndex` to be reset to `0`
|
||||
- If set to `true`, the `pageIndex` will not be automatically set to `0` when these dependencies change.
|
||||
|
||||
### Instance Variables
|
||||
### Instance Properties
|
||||
|
||||
The following values are provided to the table `instance`:
|
||||
|
||||
@@ -1173,18 +1350,179 @@ The following values are provided to the table `instance`:
|
||||
### Example
|
||||
|
||||
```js
|
||||
const state = useTableState({ pageSize: 20, pageIndex: 1 })
|
||||
function Table({ columns, data }) {
|
||||
// Use the state and functions returned from useTable to build your UI
|
||||
const {
|
||||
getTableProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
page, // Instead of using 'rows', we'll use page,
|
||||
// which has only the rows for the active page
|
||||
|
||||
const { rows } = useTable(
|
||||
{
|
||||
// state[0] === { pageSize: 20, pageIndex: 1 }
|
||||
state,
|
||||
},
|
||||
usePagination
|
||||
)
|
||||
// The rest of these things are super handy, too ;)
|
||||
canPreviousPage,
|
||||
canNextPage,
|
||||
pageOptions,
|
||||
pageCount,
|
||||
gotoPage,
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageSize,
|
||||
state: [{ pageIndex, pageSize }],
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
},
|
||||
usePagination
|
||||
)
|
||||
|
||||
// Render the UI for your table
|
||||
return (
|
||||
<>
|
||||
<pre>
|
||||
<code>
|
||||
{JSON.stringify(
|
||||
{
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageCount,
|
||||
canNextPage,
|
||||
canPreviousPage,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</code>
|
||||
</pre>
|
||||
<table {...getTableProps()}>
|
||||
<thead>
|
||||
{headerGroups.map(headerGroup => (
|
||||
<tr {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map(column => (
|
||||
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{page.map(
|
||||
(row, i) =>
|
||||
prepareRow(row) || (
|
||||
<tr {...row.getRowProps()}>
|
||||
{row.cells.map(cell => {
|
||||
return (
|
||||
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{/*
|
||||
Pagination can be built however you'd like.
|
||||
This is just a very basic UI implementation:
|
||||
*/}
|
||||
<div className="pagination">
|
||||
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
|
||||
{'<<'}
|
||||
</button>{' '}
|
||||
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
|
||||
{'<'}
|
||||
</button>{' '}
|
||||
<button onClick={() => nextPage()} disabled={!canNextPage}>
|
||||
{'>'}
|
||||
</button>{' '}
|
||||
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
|
||||
{'>>'}
|
||||
</button>{' '}
|
||||
<span>
|
||||
Page{' '}
|
||||
<strong>
|
||||
{pageIndex + 1} of {pageOptions.length}
|
||||
</strong>{' '}
|
||||
</span>
|
||||
<span>
|
||||
| Go to page:{' '}
|
||||
<input
|
||||
type="number"
|
||||
defaultValue={pageIndex + 1}
|
||||
onChange={e => {
|
||||
const page = e.target.value ? Number(e.target.value) - 1 : 0
|
||||
gotoPage(page)
|
||||
}}
|
||||
style={{ width: '100px' }}
|
||||
/>
|
||||
</span> <select
|
||||
value={pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value))
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 40, 50].map(pageSize => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
Show {pageSize}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</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(100000), [])
|
||||
|
||||
return (
|
||||
<Styles>
|
||||
<Table columns={columns} data={data} />
|
||||
</Styles>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## `useTokenPagination`
|
||||
## `useTokenPagination (Coming Soon)`
|
||||
|
||||
- Plugin Hook
|
||||
- Optional
|
||||
@@ -1193,6 +1531,225 @@ const { rows } = useTable(
|
||||
|
||||
> Documentation Coming Soon...
|
||||
|
||||
## `useRowSelect`
|
||||
|
||||
- Plugin Hook
|
||||
- Optional
|
||||
|
||||
`useRowSelect` is the hook that implements **basic row selection**. For more information on row selection, see [Row Selection](TODO)
|
||||
|
||||
### Table Options
|
||||
|
||||
The following options are supported via the main options object passed to `useTable(options)`
|
||||
|
||||
- `state[0].selectedRows: Array<RowPathKey>`
|
||||
- Optional
|
||||
- Defaults to `[]`
|
||||
- If a row's path key (eg. a row path of `[1, 3, 2]` would have a path key of `1.3.2`) is found in this array, it will have a selected state.
|
||||
- `manualRowSelectedKey: String`
|
||||
- Optional
|
||||
- Defaults to `selected`
|
||||
- If this key is found on the **original** data row, and it is true, this row will be manually selected
|
||||
|
||||
### Instance Properties
|
||||
|
||||
The following values are provided to the table `instance`:
|
||||
|
||||
- `toggleRowSelected: Function(rowPath: String, ?set: Bool) => void`
|
||||
- Use this function to toggle a row's selected state.
|
||||
- Optionally pass `true` or `false` to set it to that state
|
||||
- `toggleRowSelectedAll: Function(?set: Bool) => void`
|
||||
- Use this function to toggle all rows as select or not
|
||||
- Optionally pass `true` or `false` to set all rows to that state
|
||||
- `getToggleAllRowsSelectedProps: Function(props) => props`
|
||||
- Use this function to get the props needed for a **select all checkbox**.
|
||||
- Props:
|
||||
- `onChange: Function()`
|
||||
- `style.cursor: 'pointer'`
|
||||
- `checked: Bool`
|
||||
- `title: 'Toggle All Rows Selected'`
|
||||
- `allRowsSelected: Bool`
|
||||
- Will be `true` if all rows are selected.
|
||||
- If at least one row is not selected, will be `false`
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
function Table({ columns, data }) {
|
||||
// Use the state and functions returned from useTable to build your UI
|
||||
const {
|
||||
getTableProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
state: [{ selectedRows }],
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
},
|
||||
useRowSelect
|
||||
)
|
||||
|
||||
// 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>
|
||||
<p>Selected Rows: {selectedRows.length}</p>
|
||||
<pre>
|
||||
<code>{JSON.stringify({ selectedRows }, null, 2)}</code>
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const columns = React.useMemo(
|
||||
() => [
|
||||
// Let's make a column for selection
|
||||
{
|
||||
id: 'selection',
|
||||
// The header can use the table's getToggleAllRowsSelectedProps method
|
||||
// to render a checkbox
|
||||
Header: ({ getToggleAllRowsSelectedProps }) => (
|
||||
<div>
|
||||
<input type="checkbox" {...getToggleAllRowsSelectedProps()} />
|
||||
</div>
|
||||
),
|
||||
// The cell can use the individual row's getToggleRowSelectedProps method
|
||||
// to the render a checkbox
|
||||
Cell: ({ row }) => (
|
||||
<div>
|
||||
<input type="checkbox" {...row.getToggleRowSelectedProps()} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
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(10), [])
|
||||
|
||||
return <Table columns={columns} data={data} />
|
||||
}
|
||||
```
|
||||
|
||||
## `useRowState`
|
||||
|
||||
- Plugin Hook
|
||||
- Optional
|
||||
|
||||
`useRowState` is a plugin hook that implements **basic state management for _prepared_ rows and their cells**.
|
||||
|
||||
### Table Options
|
||||
|
||||
The following options are supported via the main options object passed to `useTable(options)`
|
||||
|
||||
- `state[0].rowState: Object<RowPathKey:Object<any, cellState: {columnID: Object}>>`
|
||||
- Optional
|
||||
- Defaults to `{}`
|
||||
- If a row's path key (eg. a row path of `[1, 3, 2]` would have a path key of `1.3.2`) is found in this array, it will have the state of the value corresponding to that key.
|
||||
- Individual row states can contain anything, but they also contain a `cellState` key, which provides cell-level state based on column ID's to every
|
||||
**prepared** cell in the table.
|
||||
- `initialRowStateAccessor: Function`
|
||||
- Optional
|
||||
- This function may optionally return the initial state for a row.
|
||||
- If this function is defined, it will be passed a `Row` object, from which you can return a value to use as the initial state, eg. `row => row.original.initialState`
|
||||
|
||||
### Instance Properties
|
||||
|
||||
The following values are provided to the table `instance`:
|
||||
|
||||
- `setRowState: Function(rowPath: Array<string>, updater: Function | Any) => void`
|
||||
- Use this function to programmatically update the state of a row.
|
||||
- `updater` can be a function or value. If a `function` is passed, it will receive the current value and expect a new one to be returned.
|
||||
- `setCellState: Function(rowPath: Array<string>, columnID: String, updater: Function | Any) => void`
|
||||
- Use this function to programmatically update the cell of a row.
|
||||
- `updater` can be a function or value. If a `function` is passed, it will receive the current value and expect a new one to be returned.
|
||||
|
||||
### Row Properties
|
||||
|
||||
The following additional properties are available on every **prepared** `row` object returned by the table instance.
|
||||
|
||||
- `state: Object`
|
||||
- This is the state object for each row, pre-mapped to the row from the table state's `rowState` object via `rowState[row.path.join('.')]`
|
||||
- May also contain a `cellState` key/value pair, which is used to provide individual cell states to this row's cells
|
||||
- `setState: Function(updater: Function | any)`
|
||||
- Use this function to programmatically update the state of a row.
|
||||
- `updater` can be a function or value. If a `function` is passed, it will receive the current value and expect a new one to be returned.
|
||||
|
||||
### Cell Properties
|
||||
|
||||
The following additional properties are available on every `Cell` object returned in an array of `cells` on every row object.
|
||||
|
||||
- `state: Object`
|
||||
- This is the state object for each cell, pre-mapped to the cell from the table state's `rowState` object via `rowState[row.path.join('.')].cellState[columnID]`
|
||||
- `setState: Function(updater: Function | any)`
|
||||
- Use this function to programmatically update the state of a cell.
|
||||
- `updater` can be a function or value. If a `function` is passed, it will receive the current value and expect a new one to be returned.
|
||||
|
||||
### Example
|
||||
|
||||
> Have an example of using useRowState? Submit a PR to add it here!
|
||||
|
||||
## `useTableState`
|
||||
|
||||
- Optional
|
||||
@@ -1209,7 +1766,7 @@ Some common use cases for this hook are:
|
||||
- Disallowing specific states via a custom state reducer
|
||||
- Enabling parent/unrelated components to manipulate the table state
|
||||
|
||||
### Table Options
|
||||
### Hook Options
|
||||
|
||||
The following options are supported via the main options object passed to `useTable(options)`
|
||||
|
||||
@@ -1232,14 +1789,16 @@ The following options are supported via the main options object passed to `useTa
|
||||
- Defaults to `React.useState`
|
||||
- This function, if defined will be used as the state hook internally instead of the default `React.useState`. This can be useful for implementing custom state storage hooks like useLocalStorage, etc.
|
||||
|
||||
### Instance Variables
|
||||
### Output
|
||||
|
||||
- `tableStateTuple: [tableState, setTableState]`
|
||||
- Similar in structure to the result of `React.useState`
|
||||
- **Memoized**. This tuple array will not change between renders unless state or `useTableState` options change.
|
||||
- **Memoized** - This tuple array will not change between renders unless the state or `useTableState` options change.
|
||||
- `tableState: Object`
|
||||
- **Memoized** - This object reference will not change unless the state changes.
|
||||
- This is the final state object of the table, which is the product of the `initialState`, `overrides` and the `reducer` options (if applicable)
|
||||
- `setTableState: Function(updater, type) => void`
|
||||
- **Memoized** - This function reference will not change unless the internal state `reducer` is changed
|
||||
- This function is used both internally by React Table, and optionally by you (the developer) to update the table state programmatically.
|
||||
- `updater: Function`
|
||||
- This function signature is **almost** (see next point) identical to the functional API exposed by `React.setState`. It is passed the previous state and is expected to return a new version of the state.
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["react-app"],
|
||||
"plugins": ["styled-components"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
SKIP_PREFLIGHT_CHECK=true
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": ["react-app", "prettier"],
|
||||
"rules": {
|
||||
// "eqeqeq": 0,
|
||||
// "jsx-a11y/anchor-is-valid": 0
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useTable, usePagination } 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;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 1rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
`
|
||||
|
||||
// Create an editable cell renderer
|
||||
const EditableCell = ({
|
||||
value: initialValue,
|
||||
row: { index },
|
||||
column: { id },
|
||||
updateMyData, // This is a custom function that we supplied to our table instance
|
||||
}) => {
|
||||
// We need to keep and update the state of the cell normally
|
||||
const [value, setValue] = React.useState(initialValue)
|
||||
const onChange = e => {
|
||||
setValue(e.target.value)
|
||||
}
|
||||
// We'll only update the external data when the input is blurred
|
||||
const onBlur = () => {
|
||||
updateMyData(index, id, value)
|
||||
}
|
||||
return <input value={value} onChange={onChange} onBlur={onBlur} />
|
||||
}
|
||||
|
||||
// Set our editable cell renderer as the default Cell renderer
|
||||
const defaultColumn = {
|
||||
Cell: EditableCell,
|
||||
}
|
||||
|
||||
// Be sure to pass our updateMyData and the disablePageResetOnDataChange option
|
||||
function Table({ columns, data, updateMyData, disablePageResetOnDataChange }) {
|
||||
// For this example, we're using pagination to illustrate how to stop
|
||||
// the current page from resetting when our data changes
|
||||
// Otherwise, nothing is different here.
|
||||
const {
|
||||
getTableProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
page,
|
||||
canPreviousPage,
|
||||
canNextPage,
|
||||
pageOptions,
|
||||
pageCount,
|
||||
gotoPage,
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageSize,
|
||||
state: [{ pageIndex, pageSize }],
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
defaultColumn,
|
||||
disablePageResetOnDataChange,
|
||||
// updateMyData isn't part of the API, but
|
||||
// anything we put into these options will
|
||||
// automatically be available on the instance.
|
||||
// That way we can call this function from our
|
||||
// cell renderer!
|
||||
updateMyData,
|
||||
},
|
||||
usePagination
|
||||
)
|
||||
|
||||
// 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>
|
||||
{page.map(
|
||||
(row, i) =>
|
||||
prepareRow(row) || (
|
||||
<tr {...row.getRowProps()}>
|
||||
{row.cells.map(cell => {
|
||||
return (
|
||||
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="pagination">
|
||||
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
|
||||
{'<<'}
|
||||
</button>{' '}
|
||||
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
|
||||
{'<'}
|
||||
</button>{' '}
|
||||
<button onClick={() => nextPage()} disabled={!canNextPage}>
|
||||
{'>'}
|
||||
</button>{' '}
|
||||
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
|
||||
{'>>'}
|
||||
</button>{' '}
|
||||
<span>
|
||||
Page{' '}
|
||||
<strong>
|
||||
{pageIndex + 1} of {pageOptions.length}
|
||||
</strong>{' '}
|
||||
</span>
|
||||
<span>
|
||||
| Go to page:{' '}
|
||||
<input
|
||||
type="number"
|
||||
defaultValue={pageIndex + 1}
|
||||
onChange={e => {
|
||||
const page = e.target.value ? Number(e.target.value) - 1 : 0
|
||||
gotoPage(page)
|
||||
}}
|
||||
style={{ width: '100px' }}
|
||||
/>
|
||||
</span>{' '}
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value))
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 40, 50].map(pageSize => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
Show {pageSize}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</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, setData] = React.useState(() => makeData(20))
|
||||
|
||||
// We need to keep the table from resetting the pageIndex when we
|
||||
// Update data. So we can keep track of that flag with a ref.
|
||||
const skipPageResetRef = React.useRef(false)
|
||||
|
||||
// When our cell renderer calls updateMyData, we'll use
|
||||
// the rowIndex, columnID and new value to update the
|
||||
// original data
|
||||
const updateMyData = (rowIndex, columnID, value) => {
|
||||
// We also turn on the flag to not reset the page
|
||||
skipPageResetRef.current = true
|
||||
setData(old => {
|
||||
return old.filter((row, index) => {
|
||||
if (index === rowIndex) {
|
||||
return {
|
||||
...old[rowIndex],
|
||||
[columnID]: value,
|
||||
}
|
||||
}
|
||||
return row
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// After data chagnes, we turn the flag back off
|
||||
// so that if data actually changes when we're not
|
||||
// editing it, the page is reset
|
||||
React.useEffect(
|
||||
() => {
|
||||
skipPageResetRef.current = false
|
||||
},
|
||||
[data]
|
||||
)
|
||||
|
||||
// Let's add a data resetter/randomizer to help
|
||||
// illustrate that flow...
|
||||
const resetData = () => setData(makeData(20))
|
||||
|
||||
return (
|
||||
<Styles>
|
||||
<button onClick={resetData}>Reset Data</button>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data}
|
||||
updateMyData={updateMyData}
|
||||
disablePageResetOnDataChange={skipPageResetRef.current}
|
||||
/>
|
||||
</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,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,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,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()
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,20 +33,19 @@ const Styles = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
function Table({ columns: userColumns, data, SubComponent }) {
|
||||
function Table({ columns: userColumns, data }) {
|
||||
const {
|
||||
getTableProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
columns,
|
||||
state: [{ expanded }],
|
||||
} = useTable(
|
||||
{
|
||||
columns: userColumns,
|
||||
data,
|
||||
},
|
||||
useExpanded
|
||||
useExpanded // Use the useExpanded plugin hook
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -68,20 +67,13 @@ function Table({ columns: userColumns, data, SubComponent }) {
|
||||
{rows.map(
|
||||
(row, i) =>
|
||||
prepareRow(row) || (
|
||||
<>
|
||||
<tr {...row.getRowProps()}>
|
||||
{row.cells.map(cell => {
|
||||
return (
|
||||
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
{!row.subRows.length && row.isExpanded ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length}>{SubComponent({ row })}</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</>
|
||||
<tr {...row.getRowProps()}>
|
||||
{row.cells.map(cell => {
|
||||
return (
|
||||
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
@@ -96,19 +88,26 @@ function App() {
|
||||
const columns = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: () => null,
|
||||
id: 'expander',
|
||||
Cell: ({ row }) => (
|
||||
<span
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
paddingLeft: `${row.depth * 2}rem`,
|
||||
}}
|
||||
onClick={() => row.toggleExpanded()}
|
||||
>
|
||||
{row.isExpanded ? '👇' : '👉'}
|
||||
</span>
|
||||
),
|
||||
// Build our expander column
|
||||
Header: () => null, // No header, please
|
||||
id: 'expander', // Make sure it has an ID
|
||||
Cell: ({ row }) =>
|
||||
// Use the row.canExpand and row.getExpandedToggleProps prop getter
|
||||
// to build the toggle for expanding a row
|
||||
row.canExpand ? (
|
||||
<span
|
||||
{...row.getExpandedToggleProps({
|
||||
style: {
|
||||
// We can even use the row.depth property
|
||||
// and paddingLeft to indicate the depth
|
||||
// of the row
|
||||
paddingLeft: `${row.depth * 2}rem`,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{row.isExpanded ? '👇' : '👉'}
|
||||
</span>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
@@ -152,15 +151,7 @@ function App() {
|
||||
|
||||
return (
|
||||
<Styles>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data}
|
||||
SubComponent={({ row }) => (
|
||||
<pre>
|
||||
<code>{JSON.stringify({ values: row.values }, null, 2)}</code>
|
||||
</pre>
|
||||
)}
|
||||
/>
|
||||
<Table columns={columns} data={data} />
|
||||
</Styles>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -102,12 +102,7 @@ function Table({ columns, data }) {
|
||||
{cell.grouped ? (
|
||||
// If it's a grouped cell, add an expander and row count
|
||||
<>
|
||||
<span
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => row.toggleExpanded()}
|
||||
>
|
||||
<span {...row.getExpandedToggleProps()}>
|
||||
{row.isExpanded ? '👇' : '👉'}
|
||||
</span>{' '}
|
||||
{cell.render('Cell')} ({row.subRows.length})
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["react-app"],
|
||||
"plugins": ["styled-components"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
SKIP_PREFLIGHT_CHECK=true
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": ["react-app", "prettier"],
|
||||
"rules": {
|
||||
// "eqeqeq": 0,
|
||||
// "jsx-a11y/anchor-is-valid": 0
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useTable, useRowSelect } 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 }) {
|
||||
// Use the state and functions returned from useTable to build your UI
|
||||
const {
|
||||
getTableProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
state: [{ selectedRows }],
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
},
|
||||
useRowSelect
|
||||
)
|
||||
|
||||
// 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>
|
||||
<p>Selected Rows: {selectedRows.length}</p>
|
||||
<pre>
|
||||
<code>{JSON.stringify({ selectedRows }, null, 2)}</code>
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const columns = React.useMemo(
|
||||
() => [
|
||||
// Let's make a column for selection
|
||||
{
|
||||
id: 'selection',
|
||||
// The header can use the table's getToggleAllRowsSelectedProps method
|
||||
// to render a checkbox
|
||||
Header: ({ getToggleAllRowsSelectedProps }) => (
|
||||
<div>
|
||||
<input type="checkbox" {...getToggleAllRowsSelectedProps()} />
|
||||
</div>
|
||||
),
|
||||
// The cell can use the individual row's getToggleRowSelectedProps method
|
||||
// to the render a checkbox
|
||||
Cell: ({ row }) => (
|
||||
<div>
|
||||
<input type="checkbox" {...row.getToggleRowSelectedProps()} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
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(10), [])
|
||||
|
||||
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,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,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,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()
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["react-app"],
|
||||
"plugins": ["styled-components"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
SKIP_PREFLIGHT_CHECK=true
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": ["react-app", "prettier"],
|
||||
"rules": {
|
||||
// "eqeqeq": 0,
|
||||
// "jsx-a11y/anchor-is-valid": 0
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useTable, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// A simple way to support a renderRowSubComponent is to make a render prop
|
||||
// This is NOT part of the React Table API, it's merely a rendering
|
||||
// option we are creating for ourselves in our table renderer
|
||||
function Table({ columns: userColumns, data, renderRowSubComponent }) {
|
||||
const {
|
||||
getTableProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
columns,
|
||||
state: [{ expanded }],
|
||||
} = useTable(
|
||||
{
|
||||
columns: userColumns,
|
||||
data,
|
||||
},
|
||||
useExpanded // We can useExpanded to track the expanded state
|
||||
// for sub components too!
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<pre>
|
||||
<code>{JSON.stringify({ expanded }, null, 2)}</code>
|
||||
</pre>
|
||||
<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) || (
|
||||
// Use a React.Fragment here so the table markup is still valid
|
||||
<>
|
||||
<tr {...row.getRowProps()}>
|
||||
{row.cells.map(cell => {
|
||||
return (
|
||||
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
{/*
|
||||
If the row is in an expanded state, render a row with a
|
||||
column that spands the entire length of the table.
|
||||
*/}
|
||||
{row.isExpanded ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length}>
|
||||
{/*
|
||||
Inside it, call our renderRowSubComponent function. In reality,
|
||||
you coul pass whatever you want as props to
|
||||
a component like this, including the entire
|
||||
table instance. But for this example, we'll just
|
||||
pass the row
|
||||
*/}
|
||||
{renderRowSubComponent({ row })}
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
<div>Showing the first 20 results of {rows.length} rows</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const columns = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
// Make an expander cell
|
||||
Header: () => null, // No header
|
||||
id: 'expander', // It needs an ID
|
||||
Cell: ({ row }) => (
|
||||
// Use Cell to render an expander for each row.
|
||||
// We can use the getExpandedToggleProps prop-getter
|
||||
// to build the expander.
|
||||
<span {...row.getExpandedToggleProps()}>
|
||||
{row.isExpanded ? '👇' : '👉'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
columns: [
|
||||
{
|
||||
Header: 'First Name',
|
||||
accessor: 'firstName',
|
||||
},
|
||||
{
|
||||
Header: 'Last Name',
|
||||
accessor: 'lastName',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
Header: 'Info',
|
||||
columns: [
|
||||
{
|
||||
Header: 'Age',
|
||||
accessor: 'age',
|
||||
},
|
||||
{
|
||||
Header: 'Visits',
|
||||
accessor: 'visits',
|
||||
},
|
||||
{
|
||||
Header: 'Status',
|
||||
accessor: 'status',
|
||||
},
|
||||
{
|
||||
Header: 'Profile Progress',
|
||||
accessor: 'progress',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const data = React.useMemo(() => makeData(10), [])
|
||||
|
||||
// Create a function that will render our row sub components
|
||||
const renderRowSubComponent = React.useCallback(
|
||||
({ row }) => (
|
||||
<pre
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
}}
|
||||
>
|
||||
<code>{JSON.stringify({ values: row.values }, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Styles>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data}
|
||||
// We added this as a prop for our table component
|
||||
// Remember, this is not part of the React Table API,
|
||||
// it's merely a rendering option we created for
|
||||
// ourselves
|
||||
renderRowSubComponent={renderRowSubComponent}
|
||||
/>
|
||||
</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,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,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,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()
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-table",
|
||||
"version": "7.0.0-alpha.17",
|
||||
"version": "7.0.0-alpha.20",
|
||||
"description": "A fast, lightweight, opinionated table and datagrid built on React",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/tannerlinsley/react-table#readme",
|
||||
@@ -27,7 +27,7 @@
|
||||
"prepare": "yarn build",
|
||||
"release": "yarn publish",
|
||||
"releaseNext": "yarn publish --tag next",
|
||||
"format": "prettier ./**{md,js,jsx,tsx} --write"
|
||||
"format": "prettier ./src/**/*.{md,js,jsx,tsx} --write"
|
||||
},
|
||||
"files": [
|
||||
"CHANGELOG.md",
|
||||
|
||||
+33
-40
@@ -79,13 +79,15 @@ export const useTable = (props, ...plugins) => {
|
||||
})
|
||||
|
||||
// Allow plugins to register hooks
|
||||
if (debug) console.time('plugins')
|
||||
if (process.env.NODE_ENV === 'development' && debug) console.time('plugins')
|
||||
plugins.filter(Boolean).forEach(plugin => {
|
||||
plugin(instanceRef.current.hooks)
|
||||
})
|
||||
if (debug) console.timeEnd('plugins')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.timeEnd('plugins')
|
||||
|
||||
if (debug) console.info('buildColumns/headerGroup/headers')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.info('buildColumns/headerGroup/headers')
|
||||
// Decorate All the columns
|
||||
let columnTree = React.useMemo(
|
||||
() => decorateColumnTree(userColumns, defaultColumn),
|
||||
@@ -100,13 +102,15 @@ export const useTable = (props, ...plugins) => {
|
||||
// Allow hooks to decorate columns (and trigger this memoization via deps)
|
||||
columns = React.useMemo(
|
||||
() => {
|
||||
if (debug) console.time('hooks.columnsBeforeHeaderGroups')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.time('hooks.columnsBeforeHeaderGroups')
|
||||
const newColumns = applyHooks(
|
||||
instanceRef.current.hooks.columnsBeforeHeaderGroups,
|
||||
columns,
|
||||
instanceRef.current
|
||||
)
|
||||
if (debug) console.timeEnd('hooks.columnsBeforeHeaderGroups')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.timeEnd('hooks.columnsBeforeHeaderGroups')
|
||||
return newColumns
|
||||
},
|
||||
[
|
||||
@@ -138,9 +142,14 @@ export const useTable = (props, ...plugins) => {
|
||||
})
|
||||
|
||||
// Access the row model
|
||||
instanceRef.current.rows = React.useMemo(
|
||||
const [rows, rowPaths, flatRows] = React.useMemo(
|
||||
() => {
|
||||
if (debug) console.time('getAccessedRows')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.time('getAccessedRows')
|
||||
|
||||
let flatRows = 0
|
||||
const rowPaths = []
|
||||
|
||||
// Access the row's data
|
||||
const accessRow = (originalRow, i, depth = 0, parentPath = []) => {
|
||||
// Keep the original reference around
|
||||
@@ -149,6 +158,9 @@ export const useTable = (props, ...plugins) => {
|
||||
// Make the new path for the row
|
||||
const path = [...parentPath, i]
|
||||
|
||||
flatRows++
|
||||
rowPaths.push(path.join('.'))
|
||||
|
||||
// Process any subRows
|
||||
const subRows = originalRow[subRowsKey]
|
||||
? originalRow[subRowsKey].map((d, i) =>
|
||||
@@ -190,12 +202,17 @@ export const useTable = (props, ...plugins) => {
|
||||
|
||||
// Use the resolved data
|
||||
const accessedData = data.map((d, i) => accessRow(d, i))
|
||||
if (debug) console.timeEnd('getAccessedRows')
|
||||
return accessedData
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.timeEnd('getAccessedRows')
|
||||
return [accessedData, rowPaths, flatRows]
|
||||
},
|
||||
[debug, data, subRowsKey]
|
||||
)
|
||||
|
||||
instanceRef.current.rows = rows
|
||||
instanceRef.current.rowPaths = rowPaths
|
||||
instanceRef.current.flatRows = flatRows
|
||||
|
||||
// Determine column visibility
|
||||
instanceRef.current.columns.forEach(column => {
|
||||
column.visible =
|
||||
@@ -204,31 +221,14 @@ export const useTable = (props, ...plugins) => {
|
||||
: !!column.show
|
||||
})
|
||||
|
||||
if (debug) console.time('hooks.useMain')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.time('hooks.useMain')
|
||||
instanceRef.current = applyHooks(
|
||||
instanceRef.current.hooks.useMain,
|
||||
instanceRef.current
|
||||
)
|
||||
if (debug)
|
||||
if (process.env.NODE_ENV === 'development' && 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
|
||||
@@ -263,14 +263,6 @@ export const useTable = (props, ...plugins) => {
|
||||
}
|
||||
)
|
||||
|
||||
// // 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 => {
|
||||
@@ -306,16 +298,17 @@ export const useTable = (props, ...plugins) => {
|
||||
|
||||
return false
|
||||
})
|
||||
// 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.useRows')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.time('hooks.useRows')
|
||||
instanceRef.current.rows = applyHooks(
|
||||
instanceRef.current.hooks.useRows,
|
||||
instanceRef.current.rows,
|
||||
instanceRef.current
|
||||
)
|
||||
if (debug) console.timeEnd('hooks.useRows')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.timeEnd('hooks.useRows')
|
||||
|
||||
// The prepareRow function is absolutely necessary and MUST be called on
|
||||
// any rows the user wishes to be displayed.
|
||||
|
||||
+14
-11
@@ -16,17 +16,20 @@ export const useTableState = (
|
||||
...initialState,
|
||||
})
|
||||
|
||||
const overriddenState = React.useMemo(() => {
|
||||
const newState = {
|
||||
...state,
|
||||
}
|
||||
if (overrides) {
|
||||
Object.keys(overrides).forEach(key => {
|
||||
newState[key] = overrides[key]
|
||||
})
|
||||
}
|
||||
return newState
|
||||
}, [overrides, state])
|
||||
const overriddenState = React.useMemo(
|
||||
() => {
|
||||
const newState = {
|
||||
...state,
|
||||
}
|
||||
if (overrides) {
|
||||
Object.keys(overrides).forEach(key => {
|
||||
newState[key] = overrides[key]
|
||||
})
|
||||
}
|
||||
return newState
|
||||
},
|
||||
[overrides, state]
|
||||
)
|
||||
|
||||
const overriddenStateRef = React.useRef()
|
||||
overriddenStateRef.current = overriddenState
|
||||
|
||||
+3
-1
@@ -7,6 +7,8 @@ export { useFilters } from './plugin-hooks/useFilters'
|
||||
export { useGroupBy } from './plugin-hooks/useGroupBy'
|
||||
export { useSortBy } from './plugin-hooks/useSortBy'
|
||||
export { usePagination } from './plugin-hooks/usePagination'
|
||||
export { useRowSelect } from './plugin-hooks/useRowSelect'
|
||||
export { useRowState } from './plugin-hooks/useRowState'
|
||||
export { useFlexLayout } from './plugin-hooks/useFlexLayout'
|
||||
export { useTokenPagination } from './plugin-hooks/useTokenPagination'
|
||||
export { useTokenPagination } from './utility-hooks/useTokenPagination'
|
||||
export { actions, addActions } from './actions'
|
||||
|
||||
@@ -0,0 +1,520 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders a paginated table 1`] = `
|
||||
"Snapshot Diff:
|
||||
- First value
|
||||
+ Second value
|
||||
|
||||
@@ -65,11 +65,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 1
|
||||
+ tanner 11
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -99,11 +99,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 2
|
||||
+ tanner 12
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -133,11 +133,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 3
|
||||
+ tanner 13
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -167,11 +167,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 4
|
||||
+ tanner 14
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -201,11 +201,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 5
|
||||
+ tanner 15
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -235,11 +235,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 6
|
||||
+ tanner 16
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -269,11 +269,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 7
|
||||
+ tanner 17
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -303,11 +303,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 8
|
||||
+ tanner 18
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -337,11 +337,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 9
|
||||
+ tanner 19
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -371,11 +371,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 10
|
||||
+ tanner 20
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -404,19 +404,15 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
class=\\"pagination\\"
|
||||
>
|
||||
- <button
|
||||
- disabled=\\"\\"
|
||||
- >
|
||||
+ <button>
|
||||
<<
|
||||
</button>
|
||||
|
||||
- <button
|
||||
- disabled=\\"\\"
|
||||
- >
|
||||
+ <button>
|
||||
<
|
||||
</button>
|
||||
|
||||
<button>
|
||||
>
|
||||
@@ -427,20 +423,20 @@
|
||||
</button>
|
||||
|
||||
<span>
|
||||
Page
|
||||
<strong>
|
||||
- 1 of 10
|
||||
+ 2 of 10
|
||||
</strong>
|
||||
|
||||
</span>
|
||||
<span>
|
||||
| Go to page:
|
||||
<input
|
||||
style=\\"width: 100px;\\"
|
||||
type=\\"number\\"
|
||||
- value=\\"1\\"
|
||||
+ value=\\"2\\"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<select>
|
||||
<option"
|
||||
`;
|
||||
|
||||
exports[`renders a paginated table 2`] = `
|
||||
"Snapshot Diff:
|
||||
- First value
|
||||
+ Second value
|
||||
|
||||
@@ -65,11 +65,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 11
|
||||
+ tanner 21
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -99,11 +99,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 12
|
||||
+ tanner 22
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -133,11 +133,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 13
|
||||
+ tanner 23
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -167,11 +167,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 14
|
||||
+ tanner 24
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -201,11 +201,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 15
|
||||
+ tanner 25
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -235,11 +235,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 16
|
||||
+ tanner 26
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -269,11 +269,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 17
|
||||
+ tanner 27
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -303,11 +303,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 18
|
||||
+ tanner 28
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -337,11 +337,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 19
|
||||
+ tanner 29
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -371,11 +371,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 20
|
||||
+ tanner 30
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -423,20 +423,20 @@
|
||||
</button>
|
||||
|
||||
<span>
|
||||
Page
|
||||
<strong>
|
||||
- 2 of 10
|
||||
+ 3 of 10
|
||||
</strong>
|
||||
|
||||
</span>
|
||||
<span>
|
||||
| Go to page:
|
||||
<input
|
||||
style=\\"width: 100px;\\"
|
||||
type=\\"number\\"
|
||||
- value=\\"2\\"
|
||||
+ value=\\"3\\"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<select>
|
||||
<option"
|
||||
`;
|
||||
|
||||
exports[`renders a paginated table 3`] = `
|
||||
"Snapshot Diff:
|
||||
- First value
|
||||
+ Second value
|
||||
|
||||
@@ -65,11 +65,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 21
|
||||
+ tanner 91
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -99,11 +99,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 22
|
||||
+ tanner 92
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -133,11 +133,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 23
|
||||
+ tanner 93
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -167,11 +167,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 24
|
||||
+ tanner 94
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -201,11 +201,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 25
|
||||
+ tanner 95
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -235,11 +235,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 26
|
||||
+ tanner 96
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -269,11 +269,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 27
|
||||
+ tanner 97
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -303,11 +303,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 28
|
||||
+ tanner 98
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -337,11 +337,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 29
|
||||
+ tanner 99
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -371,11 +371,11 @@
|
||||
class=\\"\\"
|
||||
>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
- tanner 30
|
||||
+ tanner 100
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
linsley
|
||||
@@ -412,31 +412,35 @@
|
||||
|
||||
<button>
|
||||
<
|
||||
</button>
|
||||
|
||||
- <button>
|
||||
+ <button
|
||||
+ disabled=\\"\\"
|
||||
+ >
|
||||
>
|
||||
</button>
|
||||
|
||||
- <button>
|
||||
+ <button
|
||||
+ disabled=\\"\\"
|
||||
+ >
|
||||
>>
|
||||
</button>
|
||||
|
||||
<span>
|
||||
Page
|
||||
<strong>
|
||||
- 3 of 10
|
||||
+ 10 of 10
|
||||
</strong>
|
||||
|
||||
</span>
|
||||
<span>
|
||||
| Go to page:
|
||||
<input
|
||||
style=\\"width: 100px;\\"
|
||||
type=\\"number\\"
|
||||
- value=\\"3\\"
|
||||
+ value=\\"10\\"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<select>
|
||||
<option"
|
||||
`;
|
||||
@@ -0,0 +1,219 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders a table with seletable rows 1`] = `
|
||||
"Snapshot Diff:
|
||||
- First value
|
||||
+ Second value
|
||||
|
||||
@@ -109,11 +109,11 @@
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
<div>
|
||||
- Not Selected
|
||||
+ Selected
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
@@ -165,11 +165,11 @@
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
<div>
|
||||
- Not Selected
|
||||
+ Selected
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
@@ -221,11 +221,11 @@
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
<div>
|
||||
- Not Selected
|
||||
+ Selected
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
@@ -277,11 +277,11 @@
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
<div>
|
||||
- Not Selected
|
||||
+ Selected
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
@@ -314,15 +314,20 @@
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
- Selected Rows: 0
|
||||
+ Selected Rows: 4
|
||||
</p>
|
||||
<pre>
|
||||
<code>
|
||||
{
|
||||
- \\"selectedRows\\": []
|
||||
+ \\"selectedRows\\": [
|
||||
+ \\"0\\",
|
||||
+ \\"1\\",
|
||||
+ \\"2\\",
|
||||
+ \\"3\\"
|
||||
+ ]
|
||||
}
|
||||
</code>
|
||||
</pre>
|
||||
</DocumentFragment>"
|
||||
`;
|
||||
|
||||
exports[`renders a table with seletable rows 2`] = `
|
||||
"Snapshot Diff:
|
||||
- First value
|
||||
+ Second value
|
||||
|
||||
@@ -109,11 +109,11 @@
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
<div>
|
||||
- Selected
|
||||
+ Not Selected
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
@@ -165,11 +165,11 @@
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
<div>
|
||||
- Selected
|
||||
+ Not Selected
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
@@ -221,11 +221,11 @@
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
<div>
|
||||
- Selected
|
||||
+ Not Selected
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
@@ -277,11 +277,11 @@
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
<div>
|
||||
- Selected
|
||||
+ Not Selected
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
@@ -314,20 +314,15 @@
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
- Selected Rows: 4
|
||||
+ Selected Rows: 0
|
||||
</p>
|
||||
<pre>
|
||||
<code>
|
||||
{
|
||||
- \\"selectedRows\\": [
|
||||
- \\"0\\",
|
||||
- \\"1\\",
|
||||
- \\"2\\",
|
||||
- \\"3\\"
|
||||
- ]
|
||||
+ \\"selectedRows\\": []
|
||||
}
|
||||
</code>
|
||||
</pre>
|
||||
</DocumentFragment>"
|
||||
`;
|
||||
|
||||
exports[`renders a table with seletable rows 3`] = `
|
||||
"Snapshot Diff:
|
||||
- First value
|
||||
+ Second value
|
||||
|
||||
@@ -109,11 +109,11 @@
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
<div>
|
||||
- Not Selected
|
||||
+ Selected
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
@@ -221,11 +221,11 @@
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
<div>
|
||||
- Not Selected
|
||||
+ Selected
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class=\\"\\"
|
||||
>
|
||||
@@ -314,15 +314,18 @@
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
- Selected Rows: 0
|
||||
+ Selected Rows: 2
|
||||
</p>
|
||||
<pre>
|
||||
<code>
|
||||
{
|
||||
- \\"selectedRows\\": []
|
||||
+ \\"selectedRows\\": [
|
||||
+ \\"0\\",
|
||||
+ \\"2\\"
|
||||
+ ]
|
||||
}
|
||||
</code>
|
||||
</pre>
|
||||
</DocumentFragment>"
|
||||
`;
|
||||
@@ -0,0 +1,180 @@
|
||||
import '@testing-library/react/cleanup-after-each'
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
|
||||
import React from 'react'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import { useTable } from '../../hooks/useTable'
|
||||
import { usePagination } from '../usePagination'
|
||||
|
||||
const data = [...new Array(100)].map((d, i) => ({
|
||||
firstName: 'tanner ' + (i + 1),
|
||||
lastName: 'linsley',
|
||||
age: 29,
|
||||
visits: 100,
|
||||
status: 'In Relationship',
|
||||
progress: 50,
|
||||
}))
|
||||
|
||||
function Table({ columns, data }) {
|
||||
const {
|
||||
getTableProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
page,
|
||||
canPreviousPage,
|
||||
canNextPage,
|
||||
pageOptions,
|
||||
pageCount,
|
||||
gotoPage,
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageSize,
|
||||
state: [{ pageIndex, pageSize }],
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
},
|
||||
usePagination
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<table {...getTableProps()}>
|
||||
<thead>
|
||||
{headerGroups.map(headerGroup => (
|
||||
<tr {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map(column => (
|
||||
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{page.map(
|
||||
(row, i) =>
|
||||
prepareRow(row) || (
|
||||
<tr {...row.getRowProps()}>
|
||||
{row.cells.map(cell => {
|
||||
return (
|
||||
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="pagination">
|
||||
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
|
||||
{'<<'}
|
||||
</button>{' '}
|
||||
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
|
||||
{'<'}
|
||||
</button>{' '}
|
||||
<button onClick={() => nextPage()} disabled={!canNextPage}>
|
||||
{'>'}
|
||||
</button>{' '}
|
||||
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
|
||||
{'>>'}
|
||||
</button>{' '}
|
||||
<span>
|
||||
Page{' '}
|
||||
<strong>
|
||||
{pageIndex + 1} of {pageOptions.length}
|
||||
</strong>{' '}
|
||||
</span>
|
||||
<span>
|
||||
| Go to page:{' '}
|
||||
<input
|
||||
type="number"
|
||||
defaultValue={pageIndex + 1}
|
||||
onChange={e => {
|
||||
const page = e.target.value ? Number(e.target.value) - 1 : 0
|
||||
gotoPage(page)
|
||||
}}
|
||||
style={{ width: '100px' }}
|
||||
/>
|
||||
</span>{' '}
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value))
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 40, 50].map(pageSize => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
Show {pageSize}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
return <Table columns={columns} data={data} />
|
||||
}
|
||||
|
||||
test('renders a paginated table', () => {
|
||||
const { getByText, asFragment } = render(<App />)
|
||||
|
||||
const beforeSort = asFragment()
|
||||
|
||||
fireEvent.click(getByText('>'))
|
||||
|
||||
const afterSort1 = asFragment()
|
||||
|
||||
fireEvent.click(getByText('>'))
|
||||
|
||||
const afterSort2 = asFragment()
|
||||
|
||||
fireEvent.click(getByText('>>'))
|
||||
|
||||
const afterSort3 = asFragment()
|
||||
|
||||
expect(beforeSort).toMatchDiffSnapshot(afterSort1)
|
||||
expect(afterSort1).toMatchDiffSnapshot(afterSort2)
|
||||
expect(afterSort2).toMatchDiffSnapshot(afterSort3)
|
||||
})
|
||||
@@ -0,0 +1,191 @@
|
||||
import '@testing-library/react/cleanup-after-each'
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
|
||||
import React from 'react'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import { useTable } from '../../hooks/useTable'
|
||||
import { useRowSelect } from '../useRowSelect'
|
||||
|
||||
const data = [
|
||||
{
|
||||
firstName: 'tanner',
|
||||
lastName: 'linsley',
|
||||
age: 29,
|
||||
visits: 100,
|
||||
status: 'In Relationship',
|
||||
progress: 50,
|
||||
},
|
||||
{
|
||||
firstName: 'derek',
|
||||
lastName: 'perkins',
|
||||
age: 40,
|
||||
visits: 40,
|
||||
status: 'Single',
|
||||
progress: 80,
|
||||
},
|
||||
{
|
||||
firstName: 'joe',
|
||||
lastName: 'bergevin',
|
||||
age: 45,
|
||||
visits: 20,
|
||||
status: 'Complicated',
|
||||
progress: 10,
|
||||
},
|
||||
{
|
||||
firstName: 'jaylen',
|
||||
lastName: 'linsley',
|
||||
age: 26,
|
||||
visits: 99,
|
||||
status: 'In Relationship',
|
||||
progress: 70,
|
||||
},
|
||||
]
|
||||
|
||||
function Table({ columns, data }) {
|
||||
// Use the state and functions returned from useTable to build your UI
|
||||
const {
|
||||
getTableProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
state: [{ selectedRows }],
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
},
|
||||
useRowSelect
|
||||
)
|
||||
|
||||
// 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>
|
||||
<p>Selected Rows: {selectedRows.length}</p>
|
||||
<pre>
|
||||
<code>{JSON.stringify({ selectedRows }, null, 2)}</code>
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const columns = React.useMemo(
|
||||
() => [
|
||||
// Let's make a column for selection
|
||||
{
|
||||
id: 'selection',
|
||||
// The header can use the table's getToggleAllRowsSelectedProps method
|
||||
// to render a checkbox
|
||||
Header: ({ getToggleAllRowsSelectedProps }) => (
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" {...getToggleAllRowsSelectedProps()} />{' '}
|
||||
Select All
|
||||
</label>
|
||||
</div>
|
||||
),
|
||||
// The cell can use the individual row's getToggleRowSelectedProps method
|
||||
// to the render a checkbox
|
||||
Cell: ({ row }) => (
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" {...row.getToggleRowSelectedProps()} />{' '}
|
||||
Select Row
|
||||
</label>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'selectedStatus',
|
||||
Cell: ({ row }) => (
|
||||
<div>{row.selected ? 'Selected' : 'Not Selected'}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 table with seletable rows', () => {
|
||||
const { getByLabelText, getAllByLabelText, asFragment } = render(<App />)
|
||||
|
||||
const fragment1 = asFragment()
|
||||
|
||||
fireEvent.click(getByLabelText('Select All'))
|
||||
|
||||
const fragment2 = asFragment()
|
||||
|
||||
fireEvent.click(getByLabelText('Select All'))
|
||||
|
||||
const fragment3 = asFragment()
|
||||
|
||||
fireEvent.click(getAllByLabelText('Select Row')[0])
|
||||
fireEvent.click(getAllByLabelText('Select Row')[2])
|
||||
|
||||
const fragment4 = asFragment()
|
||||
|
||||
expect(fragment1).toMatchDiffSnapshot(fragment2)
|
||||
expect(fragment2).toMatchDiffSnapshot(fragment3)
|
||||
expect(fragment3).toMatchDiffSnapshot(fragment4)
|
||||
})
|
||||
@@ -1,7 +1,13 @@
|
||||
import { useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { getBy, getFirstDefined, setBy } from '../utils'
|
||||
import {
|
||||
getBy,
|
||||
getFirstDefined,
|
||||
setBy,
|
||||
mergeProps,
|
||||
applyPropHooks,
|
||||
} from '../utils'
|
||||
import { addActions, actions } from '../actions'
|
||||
import { defaultState } from '../hooks/useTableState'
|
||||
|
||||
@@ -11,13 +17,16 @@ addActions('toggleExpanded', 'useExpanded')
|
||||
|
||||
const propTypes = {
|
||||
manualExpandedKey: PropTypes.string,
|
||||
paginateSubRows: PropTypes.bool,
|
||||
nestExpandedRows: PropTypes.bool,
|
||||
}
|
||||
|
||||
export const useExpanded = hooks => {
|
||||
hooks.getExpandedToggleProps = []
|
||||
hooks.useMain.push(useMain)
|
||||
}
|
||||
|
||||
useExpanded.pluginName = 'useExpanded'
|
||||
|
||||
function useMain(instance) {
|
||||
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useExpanded')
|
||||
|
||||
@@ -27,7 +36,7 @@ function useMain(instance) {
|
||||
manualExpandedKey = 'expanded',
|
||||
hooks,
|
||||
state: [{ expanded }, setState],
|
||||
paginateSubRows = true,
|
||||
nestExpandedRows,
|
||||
} = instance
|
||||
|
||||
const toggleExpandedByPath = (path, set) => {
|
||||
@@ -44,12 +53,29 @@ function useMain(instance) {
|
||||
|
||||
hooks.prepareRow.push(row => {
|
||||
row.toggleExpanded = set => toggleExpandedByPath(row.path, set)
|
||||
row.getExpandedToggleProps = props => {
|
||||
return mergeProps(
|
||||
{
|
||||
onClick: e => {
|
||||
e.persist()
|
||||
row.toggleExpanded()
|
||||
},
|
||||
style: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
title: 'Toggle Expanded',
|
||||
},
|
||||
applyPropHooks(instance.hooks.getExpandedToggleProps, row, instance),
|
||||
props
|
||||
)
|
||||
}
|
||||
return row
|
||||
})
|
||||
|
||||
const expandedRows = useMemo(
|
||||
() => {
|
||||
if (debug) console.info('getExpandedRows')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.info('getExpandedRows')
|
||||
|
||||
const expandedRows = []
|
||||
|
||||
@@ -60,7 +86,7 @@ function useMain(instance) {
|
||||
(row.original && row.original[manualExpandedKey]) ||
|
||||
getBy(expanded, row.path)
|
||||
|
||||
if (paginateSubRows || (!paginateSubRows && row.depth === 0)) {
|
||||
if (!nestExpandedRows || (nestExpandedRows && row.depth === 0)) {
|
||||
expandedRows.push(row)
|
||||
}
|
||||
|
||||
@@ -77,7 +103,7 @@ function useMain(instance) {
|
||||
|
||||
return expandedRows
|
||||
},
|
||||
[debug, rows, manualExpandedKey, expanded, paginateSubRows]
|
||||
[debug, rows, manualExpandedKey, expanded, nestExpandedRows]
|
||||
)
|
||||
|
||||
const expandedDepth = findExpandedDepth(expanded)
|
||||
|
||||
@@ -26,6 +26,8 @@ export const useFilters = hooks => {
|
||||
hooks.useMain.push(useMain)
|
||||
}
|
||||
|
||||
useFilters.pluginName = 'useFilters'
|
||||
|
||||
function useMain(instance) {
|
||||
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useFilters')
|
||||
|
||||
@@ -103,7 +105,7 @@ function useMain(instance) {
|
||||
// Determine if a column is filterable
|
||||
column.canFilter = accessor
|
||||
? getFirstDefined(
|
||||
columnDisableFilters,
|
||||
columnDisableFilters === true ? false : undefined,
|
||||
disableFilters === true ? false : undefined,
|
||||
true
|
||||
)
|
||||
@@ -128,7 +130,8 @@ function useMain(instance) {
|
||||
return rows
|
||||
}
|
||||
|
||||
if (debug) console.info('getFilteredRows')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.info('getFilteredRows')
|
||||
|
||||
// Filters top level and nested rows
|
||||
const filterRows = rows => {
|
||||
|
||||
@@ -45,6 +45,8 @@ export const useGroupBy = hooks => {
|
||||
hooks.useMain.push(useMain)
|
||||
}
|
||||
|
||||
useGroupBy.pluginName = 'useGroupBy'
|
||||
|
||||
function columnsBeforeHeaderGroups(columns, { state: [{ groupBy }] }) {
|
||||
// Sort grouped columns to the start of the column list
|
||||
// before the headers are built
|
||||
@@ -150,7 +152,8 @@ function useMain(instance) {
|
||||
return rows
|
||||
}
|
||||
|
||||
if (debug) console.info('getGroupedRows')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.info('getGroupedRows')
|
||||
// Find the columns that can or are aggregating
|
||||
|
||||
// Uses each column to aggregate rows into a single value
|
||||
|
||||
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
|
||||
//
|
||||
import { addActions, actions } from '../actions'
|
||||
import { defaultState } from '../hooks/useTableState'
|
||||
import { ensurePluginOrder } from '../utils'
|
||||
|
||||
defaultState.pageSize = 10
|
||||
defaultState.pageIndex = 0
|
||||
@@ -25,6 +26,8 @@ export const usePagination = hooks => {
|
||||
hooks.useMain.push(useMain)
|
||||
}
|
||||
|
||||
usePagination.pluginName = 'usePagination'
|
||||
|
||||
function useMain(instance) {
|
||||
PropTypes.checkPropTypes(propTypes, instance, 'property', 'usePagination')
|
||||
|
||||
@@ -33,6 +36,7 @@ function useMain(instance) {
|
||||
manualPagination,
|
||||
disablePageResetOnDataChange,
|
||||
debug,
|
||||
plugins,
|
||||
state: [
|
||||
{
|
||||
pageSize,
|
||||
@@ -46,6 +50,13 @@ function useMain(instance) {
|
||||
],
|
||||
} = instance
|
||||
|
||||
ensurePluginOrder(
|
||||
plugins,
|
||||
['useFilters', 'useGroupBy', 'useSortBy'],
|
||||
'usePagination',
|
||||
[]
|
||||
)
|
||||
|
||||
const rowDep = disablePageResetOnDataChange ? null : rows
|
||||
|
||||
useLayoutEffect(
|
||||
@@ -69,7 +80,8 @@ function useMain(instance) {
|
||||
pageCount: userPageCount,
|
||||
}
|
||||
}
|
||||
if (debug) console.info('getPages')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.info('getPages')
|
||||
|
||||
// Create a new pages with the first page ready to go.
|
||||
const pages = rows.length ? [] : [[]]
|
||||
@@ -102,7 +114,8 @@ function useMain(instance) {
|
||||
const canNextPage = pageIndex < pageCount - 1
|
||||
|
||||
const gotoPage = pageIndex => {
|
||||
if (debug) console.info('gotoPage')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.info('gotoPage')
|
||||
return setState(old => {
|
||||
if (pageIndex < 0 || pageIndex > pageCount - 1) {
|
||||
return old
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { mergeProps, applyPropHooks, ensurePluginOrder } from '../utils'
|
||||
import { addActions, actions } from '../actions'
|
||||
import { defaultState } from '../hooks/useTableState'
|
||||
|
||||
defaultState.selectedRows = []
|
||||
|
||||
addActions('toggleRowSelected', 'toggleRowSelectedAll')
|
||||
|
||||
const propTypes = {
|
||||
manualRowSelectedKey: PropTypes.string,
|
||||
}
|
||||
|
||||
export const useRowSelect = hooks => {
|
||||
hooks.getToggleRowSelectedProps = []
|
||||
hooks.getToggleAllRowsSelectedProps = []
|
||||
hooks.useMain.push(useMain)
|
||||
}
|
||||
|
||||
useRowSelect.pluginName = 'useRowSelect'
|
||||
|
||||
function useMain(instance) {
|
||||
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useRowSelect')
|
||||
|
||||
const {
|
||||
hooks,
|
||||
manualRowSelectedKey = 'selected',
|
||||
plugins,
|
||||
rowPaths,
|
||||
state: [{ selectedRows }, setState],
|
||||
} = instance
|
||||
|
||||
ensurePluginOrder(
|
||||
plugins,
|
||||
['useFilters', 'useGroupBy', 'useSortBy'],
|
||||
'useRowSelect',
|
||||
[]
|
||||
)
|
||||
|
||||
const allRowsSelected = rowPaths.length === selectedRows.length
|
||||
|
||||
const toggleRowSelectedAll = set => {
|
||||
setState(old => {
|
||||
const selectAll = typeof set !== 'undefined' ? set : !allRowsSelected
|
||||
return {
|
||||
...old,
|
||||
selectedRows: selectAll ? [...rowPaths] : [],
|
||||
}
|
||||
}, actions.toggleRowSelectedAll)
|
||||
}
|
||||
|
||||
const toggleRowSelected = (path, set) => {
|
||||
const key = path.join('.')
|
||||
|
||||
return setState(old => {
|
||||
// Join the paths of deep rows
|
||||
// to make a key, then manage all of the keys
|
||||
// in a flat object
|
||||
const exists = old.selectedRows.includes(key)
|
||||
const shouldExist = typeof set !== 'undefined' ? set : !exists
|
||||
let newSelectedRows = new Set(selectedRows)
|
||||
|
||||
if (!exists && shouldExist) {
|
||||
newSelectedRows.add(key)
|
||||
} else if (exists && !shouldExist) {
|
||||
newSelectedRows.delete(key)
|
||||
} else {
|
||||
return old
|
||||
}
|
||||
|
||||
return {
|
||||
...old,
|
||||
selectedRows: [...newSelectedRows.values()],
|
||||
}
|
||||
}, actions.toggleRowSelected)
|
||||
}
|
||||
|
||||
const getToggleAllRowsSelectedProps = props => {
|
||||
return mergeProps(
|
||||
{
|
||||
onChange: e => {
|
||||
toggleRowSelectedAll(e.target.checked)
|
||||
},
|
||||
style: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
checked: allRowsSelected,
|
||||
title: 'Toggle All Rows Selected',
|
||||
},
|
||||
applyPropHooks(instance.hooks.getToggleAllRowsSelectedProps, instance),
|
||||
props
|
||||
)
|
||||
}
|
||||
|
||||
hooks.prepareRow.push(row => {
|
||||
row.canSelect = !!row.original
|
||||
|
||||
if (row.canSelect) {
|
||||
row.selected = selectedRows.includes(row.path.join('.'))
|
||||
row.toggleRowSelected = set => toggleRowSelected(row.path, set)
|
||||
row.getToggleRowSelectedProps = props => {
|
||||
let checked = false
|
||||
|
||||
if (row.original && row.original[manualRowSelectedKey]) {
|
||||
checked = true
|
||||
} else {
|
||||
checked = selectedRows.includes(row.path.join('.'))
|
||||
}
|
||||
|
||||
return mergeProps(
|
||||
{
|
||||
onChange: e => {
|
||||
row.toggleRowSelected(e.target.checked)
|
||||
},
|
||||
style: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
checked,
|
||||
title: 'Toggle Row Selected',
|
||||
},
|
||||
applyPropHooks(
|
||||
instance.hooks.getToggleRowSelectedProps,
|
||||
row,
|
||||
instance
|
||||
),
|
||||
props
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return row
|
||||
})
|
||||
|
||||
return {
|
||||
...instance,
|
||||
toggleRowSelected,
|
||||
toggleRowSelectedAll,
|
||||
getToggleAllRowsSelectedProps,
|
||||
allRowsSelected,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { addActions, actions } from '../actions'
|
||||
import { defaultState } from '../hooks/useTableState'
|
||||
|
||||
defaultState.rowState = {}
|
||||
|
||||
addActions('setRowState', 'setCellState')
|
||||
|
||||
const propTypes = {
|
||||
initialRowStateAccessor: PropTypes.func,
|
||||
}
|
||||
|
||||
export const useRowState = hooks => {
|
||||
hooks.useMain.push(useMain)
|
||||
}
|
||||
|
||||
useRowState.pluginName = 'useRowState'
|
||||
|
||||
function useMain(instance) {
|
||||
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useRowState')
|
||||
|
||||
const {
|
||||
hooks,
|
||||
rows,
|
||||
initialRowStateAccessor,
|
||||
state: [{ rowState }, setState],
|
||||
} = instance
|
||||
|
||||
const setRowState = React.useCallback(
|
||||
(path, updater, action = actions.setRowState) => {
|
||||
const pathKey = path.join('.')
|
||||
return setState(old => {
|
||||
return {
|
||||
...old,
|
||||
rowState: {
|
||||
...old.rowState,
|
||||
[pathKey]:
|
||||
typeof updater === 'function'
|
||||
? updater(old.rowState[pathKey])
|
||||
: updater,
|
||||
},
|
||||
}
|
||||
}, action)
|
||||
},
|
||||
[setState]
|
||||
)
|
||||
|
||||
const setCellState = React.useCallback(
|
||||
(rowPath, columnID, updater) => {
|
||||
return setRowState(
|
||||
rowPath,
|
||||
old => {
|
||||
return {
|
||||
...old,
|
||||
cellState: {
|
||||
...old.cellState,
|
||||
[columnID]:
|
||||
typeof updater === 'function'
|
||||
? updater(old.cellState[columnID])
|
||||
: updater,
|
||||
},
|
||||
}
|
||||
},
|
||||
actions.setCellState
|
||||
)
|
||||
},
|
||||
[setRowState]
|
||||
)
|
||||
|
||||
// When data changes, reset row and cell state
|
||||
React.useEffect(
|
||||
() => {
|
||||
setState(old => {
|
||||
return {
|
||||
...old,
|
||||
rowState: {},
|
||||
}
|
||||
}, actions.setRowState)
|
||||
},
|
||||
[rows, setState]
|
||||
)
|
||||
|
||||
hooks.prepareRow.push(row => {
|
||||
const pathKey = row.path.join('.')
|
||||
|
||||
if (row.original) {
|
||||
row.state =
|
||||
(typeof rowState[pathKey] !== 'undefined'
|
||||
? rowState[pathKey]
|
||||
: initialRowStateAccessor && initialRowStateAccessor(row)) || {}
|
||||
|
||||
row.setState = updater => {
|
||||
return setRowState(row.path, updater)
|
||||
}
|
||||
|
||||
row.cells.forEach(cell => {
|
||||
cell.state = row.state.cellState || {}
|
||||
|
||||
cell.setState = updater => {
|
||||
return setCellState(row.path, cell.column.id, updater)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return row
|
||||
})
|
||||
|
||||
return {
|
||||
...instance,
|
||||
setRowState,
|
||||
setCellState,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { ensurePluginOrder } from '../utils'
|
||||
import { addActions, actions } from '../actions'
|
||||
import { defaultState } from '../hooks/useTableState'
|
||||
import * as sortTypes from '../sortTypes'
|
||||
@@ -38,6 +39,8 @@ export const useSortBy = hooks => {
|
||||
hooks.useMain.push(useMain)
|
||||
}
|
||||
|
||||
useSortBy.pluginName = 'useSortBy'
|
||||
|
||||
function useMain(instance) {
|
||||
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useSortBy')
|
||||
|
||||
@@ -57,20 +60,7 @@ function useMain(instance) {
|
||||
plugins,
|
||||
} = instance
|
||||
|
||||
// If useSortBy should probably come after useFilters for
|
||||
// the best performance, so let's hint to the user about that...
|
||||
const pluginIndex = plugins.indexOf(useSortBy)
|
||||
|
||||
const useFiltersIndex = plugins.findIndex(
|
||||
plugin => plugin.name === 'useFilters'
|
||||
)
|
||||
|
||||
if (useFiltersIndex > pluginIndex) {
|
||||
console.warn(
|
||||
'React Table: useSortBy should be placed before useFilters in your plugin list for better performance!'
|
||||
)
|
||||
}
|
||||
|
||||
ensurePluginOrder(plugins, [], 'useSortBy', ['useFilters'])
|
||||
// Add custom hooks
|
||||
hooks.getSortByToggleProps = []
|
||||
|
||||
@@ -166,7 +156,7 @@ function useMain(instance) {
|
||||
|
||||
const canSort = accessor
|
||||
? getFirstDefined(
|
||||
columnDisableSorting,
|
||||
columnDisableSorting === true ? false : undefined,
|
||||
disableSorting === true ? false : undefined,
|
||||
true
|
||||
)
|
||||
@@ -211,7 +201,8 @@ function useMain(instance) {
|
||||
if (manualSorting || !sortBy.length) {
|
||||
return rows
|
||||
}
|
||||
if (debug) console.time('getSortedRows')
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.time('getSortedRows')
|
||||
|
||||
const sortData = rows => {
|
||||
// Use the orderByFn to compose multiple sortBy's together.
|
||||
@@ -261,11 +252,12 @@ function useMain(instance) {
|
||||
row.subRows = sortData(row.subRows)
|
||||
})
|
||||
|
||||
if (debug) console.timeEnd('getSortedRows')
|
||||
|
||||
return sortedData
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development' && debug)
|
||||
console.timeEnd('getSortedRows')
|
||||
|
||||
return sortData(rows)
|
||||
},
|
||||
[manualSorting, sortBy, debug, columns, rows, orderByFn, userSortTypes]
|
||||
|
||||
@@ -310,6 +310,40 @@ export function flattenBy(columns, childKey) {
|
||||
return flatColumns
|
||||
}
|
||||
|
||||
export function ensurePluginOrder(plugins, befores, pluginName, afters) {
|
||||
const pluginIndex = plugins.findIndex(
|
||||
plugin => plugin.pluginName === pluginName
|
||||
)
|
||||
|
||||
if (pluginIndex === -1) {
|
||||
throw new Error(`The plugin ${pluginName} was not found in the plugin list!
|
||||
This usually means you need to need to name your plugin hook by setting the 'pluginName' property of the hook function, eg:
|
||||
|
||||
${pluginName}.pluginName = '${pluginName}'
|
||||
`)
|
||||
}
|
||||
|
||||
befores.forEach(before => {
|
||||
const beforeIndex = plugins.findIndex(
|
||||
plugin => plugin.pluginName === before
|
||||
)
|
||||
if (beforeIndex > -1 && beforeIndex > pluginIndex) {
|
||||
throw new Error(
|
||||
`React Table: The ${pluginName} plugin hook must be placed after the ${before} plugin hook!`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
afters.forEach(after => {
|
||||
const afterIndex = plugins.findIndex(plugin => plugin.pluginName === after)
|
||||
if (afterIndex > -1 && afterIndex < pluginIndex) {
|
||||
throw new Error(
|
||||
`React Table: The ${pluginName} plugin hook must be placed before the ${after} plugin hook!`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function makePathArray(obj) {
|
||||
|
||||
Reference in New Issue
Block a user