Compare commits

...
Author SHA1 Message Date
tannerlinsley 5d36f98082 v7.0.0-alpha.9 2019-07-26 10:32:36 -06:00
tannerlinsley 933cc27e33 Update README.md 2019-07-26 10:30:59 -06:00
tannerlinsley 54be978268 Update App.js 2019-07-26 10:30:20 -06:00
tannerlinsley 90922f8637 Upgrade filter types to support configurable autoRemoval 2019-07-26 10:19:38 -06:00
tannerlinsley fd8a686b30 Merge branch 'master' of https://github.com/react-tools/react-table 2019-07-26 07:14:06 -06:00
Larry BothaandTanner Linsley a361a372ee chore(packages): add husky and lint-staged (#1409) 2019-07-26 07:13:58 -06:00
Larry BothaandTanner Linsley 563e809d05 docs(package.json): add commitizen dep, config, and npm script (#1408)
use `npm run commit` to use commitizen for better commit messages

re #1383
2019-07-25 22:39:25 -06:00
tannerlinsley 7d267b3442 v7.0.0-alpha.8 2019-07-25 13:39:37 -06:00
tannerlinsley 388b3ec2b9 Update README.md 2019-07-25 13:39:13 -06:00
tannerlinsley b2abd82350 Use useEffect in dev, not layoutEffect 2019-07-25 13:39:10 -06:00
tannerlinsley 06a6d6fcc3 Rename getHeaderRowProps to getHeaderGroupProps 2019-07-25 13:38:52 -06:00
tannerlinsley d8599817ad Fix immutability issue with pagination and sortBy 2019-07-25 13:38:33 -06:00
tannerlinsley e1060db327 Satisfy exhuastive-deps in useTableState 2019-07-25 13:38:15 -06:00
tannerlinsley ea79cd8388 Fix renderer functions for blank headers 2019-07-25 13:37:58 -06:00
tannerlinsley b2ed18696f Add defaultColumn option 2019-07-25 13:37:31 -06:00
tannerlinsley 9f8eab5777 Update README.md 2019-07-25 10:27:49 -06:00
tannerlinsley 4f13f32023 Update rendering API and column/header model 2019-07-25 10:24:30 -06:00
tannerlinsley 5bb2e49764 Fix useTableState setState memoization 2019-07-25 07:04:52 -06:00
tannerlinsley d1f26fe7bc Fix action type detection 2019-07-24 07:21:26 -06:00
tannerlinsley c7d6562d04 Change actions and addActions to be simpler to use 2019-07-23 12:30:15 -06:00
tannerlinsley e82d2d4b36 Merge branch 'master' of https://github.com/react-tools/react-table 2019-07-23 12:22:01 -06:00
tannerlinsley 0716e234a1 Add sortBy example 2019-07-23 12:21:57 -06:00
tannerlinsley 824ee34ba9 Refactor sortBy logic 2019-07-23 12:20:32 -06:00
tannerlinsley 514fbabb88 Fix type action forcing 2019-07-23 12:12:46 -06:00
Maksim HorbachevskyandTanner Linsley 2ecf40518e Export utils to create custom hooks (#1403) 2019-07-23 12:03:17 -06:00
tannerlinsley fd64486286 Fix usePagination to work during SSR 2019-07-23 10:55:49 -06:00
tannerlinsley 34909305af Force action types in state reducer 2019-07-23 10:41:27 -06:00
tannerlinsley 5043af5071 Move useColumns and useRows into useTable (but allow overrides) 2019-07-23 10:22:46 -06:00
tannerlinsley 2ca7f78d72 Merge branch 'master' of https://github.com/react-tools/react-table 2019-07-21 15:41:28 -06:00
tannerlinsley 811cd015c2 Update README.md 2019-07-21 15:41:21 -06:00
AresMegaGlobalandTanner Linsley 9e37846a21 Update README.md (#1400) 2019-07-20 07:32:30 -06:00
tannerlinsley 723720c8b1 Update README.md 2019-07-18 15:07:41 -06:00
tannerlinsley 2a20ad3ba4 Update App.js 2019-07-18 15:06:10 -06:00
tannerlinsley c6d37e4834 Update basic example 2019-07-18 15:03:21 -06:00
tannerlinsley a1a192052d Merge branch 'master' of https://github.com/react-tools/react-table 2019-07-18 14:56:06 -06:00
tannerlinsley 455e0730bf Bare bones basic example 2019-07-18 14:56:03 -06:00
Tanner LinsleyandGitHub 1d4867fa2f Update FUNDING.yml 2019-07-18 13:53:38 -06:00
Tanner LinsleyandGitHub c3da2a7c41 Update FUNDING.yml 2019-07-18 13:52:48 -06:00
Tanner LinsleyandGitHub c3a52c5845 Update FUNDING.yml 2019-07-18 13:51:26 -06:00
Tanner LinsleyandGitHub 6dea8103d3 Update FUNDING.yml 2019-07-18 13:51:09 -06:00
Tanner LinsleyandGitHub 3bab8372d1 Update FUNDING.yml 2019-07-18 13:50:29 -06:00
Tanner LinsleyandGitHub b85f457e62 Update FUNDING.yml 2019-07-18 13:41:35 -06:00
Tanner LinsleyandGitHub c6871084d6 Create FUNDING.yml 2019-07-18 13:40:43 -06:00
tannerlinsley 8946771563 Update README.md 2019-07-18 12:08:57 -06:00
tannerlinsley fbc8032d29 Fix disableMultiSort, change from sortByFn to sortTypes 2019-07-18 12:08:55 -06:00
tannerlinsley 6a3bc5559e Add pageCount to instance via usePagination hook 2019-07-18 12:08:04 -06:00
tannerlinsley 91e28b93bf Clean up useTokenPagination 2019-07-18 12:07:48 -06:00
tannerlinsley a6345dbe2b Add sortTypes 2019-07-18 12:07:39 -06:00
tannerlinsley 0b0de317ee Clean up useFlexLayout 2019-07-18 12:07:21 -06:00
tannerlinsley 0ed0c78286 Change expandedKey to manualExpandedKey, fix expanded index to use the original row index 2019-07-18 12:06:57 -06:00
tannerlinsley fad6ac4014 Update README.md 2019-07-17 12:40:54 -06:00
tannerlinsley 8f142b3788 Merge branch 'master' of https://github.com/react-tools/react-table 2019-07-17 12:39:40 -06:00
tannerlinsley daff1f9ca3 Documentation Checkpoint 2019-07-17 12:39:35 -06:00
DomenuchandTanner Linsley 96900f645a returned row inside row hook subscription (#1393) 2019-07-16 13:22:13 -06:00
tannerlinsley 272a99fcd8 Update README.md 2019-07-15 11:36:30 -06:00
tannerlinsley 07fb9529e4 v7.0.0-alpha.7 2019-07-15 11:29:50 -06:00
DomenuchandTanner Linsley b5d399efd6 Fixed grouped column Header that were not adjusting based on hidden child columns (#1381)
* added assertion to check for show property inside column

* reverted previous change

* added a filter to weed out non-visible columns when calculating the size for grouped Headers

* added another case to the previous filter to factor in blank grouped Headers
2019-07-15 11:28:22 -06:00
DomenuchandTanner Linsley 14e931548a added undefined to the unspecific getRowProps fn to retain the param order (#1392) 2019-07-15 11:27:13 -06:00
Larry BothaandTanner Linsley bc3ea07c3e Feature/add jest configs ref: #1383 (#1385)
* add deps for testing

* feat(tests): add jest configs

* uncomment module directories path
2019-07-15 11:25:32 -06:00
tannerlinsley 4a3929cd50 Merge branch 'master' of https://github.com/react-tools/react-table 2019-07-15 09:33:04 -06:00
Larry BothaandTanner Linsley e2bb09d8b2 Feature/add prettier config, ref #1383 (#1384)
* add prettier config

* write files with prettier

* install and configure lint-staged and husky - ref 1.2 in #1383

* feat(style): add prettier configs, ref 1.1 & 1.2 in #1383
2019-07-01 21:03:57 -06:00
ggascoigneandTanner Linsley 18f2dea247 Fix lint errors from react-hooks/exhaustive-deps (#1371)
They all seemed like reasonable warnings, this squashes them.
2019-07-01 09:43:46 -06:00
ggascoigneandTanner Linsley ead3599378 Fix pagination resetting to page zero with manualPagination (#1369)
* Fix pagination resetting to page zero with manualPagination

To be honest I'm not sure what this useLayoutEffect is there to do.
It has no visible effect if you don't use manualPagination, and if you
do it, simply jumps you back to the first page, defeating the point of
you having control of the pagination.

* Conditionally reset page on data change

Rather disable the whole page reset when filters, groupBy or sortBy
change, just because I wanted to disable the page reset on data change,
make that bit be conditional.

With that in mind, usePagination now accepts a
disablePageResetOnDataChange parameter.
2019-07-01 09:36:42 -06:00
Adrien DenatandTanner Linsley addf28a011 Fix useExpanded wrongly flattening subRows (#1372)
* Fix useExpanded wrongly flattening subRows

* Add new paginateSubRows option defaulting to true
2019-06-21 10:00:59 -06:00
Adrien DenatandTanner Linsley e4429b7143 Add support for es module build (#1374) 2019-06-21 09:43:05 -06:00
tannerlinsley 4b2d0eb187 v7.0.0-alpha.6 2019-06-20 15:24:02 -06:00
ggascoigneandTanner Linsley 5369051b05 Provide option to toggle sort like V6 (#1370)
V7 adds the option to remove a sort option, so it goes from asc -> desc
-> unset, then repeats.  V6 just went from asc -> desc then repeated.

Personally I much preferred this, I think that there's a case to be made
that this is the more expected behavior.

I'm not sure if this is really the best way to fix this since it adds
yet another api option and I completely understand that that is less
than desirable, but I also would rather add an option than have to
duplicate the whole useSortBy hook.
2019-06-20 12:50:36 -06:00
DomenuchandTanner Linsley 58d38b668b changed applyHook to applyPropHooks inside getRowProps mergeProps fn (#1367) 2019-06-20 12:49:04 -06:00
85 changed files with 44971 additions and 5206 deletions
+20 -12
View File
@@ -1,14 +1,22 @@
{
"parser": "babel-eslint",
"extends": ["react-app", "prettier"],
"env": {
"es6": true
},
"parserOptions": {
"sourceType": "module"
},
"rules": {
"space-before-function-paren": 0,
"react/jsx-boolean-value": 0
}
"parser": "babel-eslint",
"extends": ["react-app", "prettier"],
"plugins": [
"react-hooks"
],
"env": {
"es6": true
},
"parserOptions": {
"sourceType": "module"
},
"rules": {
"space-before-function-paren": 0,
"react/jsx-boolean-value": 0
},
"settings": {
"react": {
"version": "latest"
}
}
}
+5
View File
@@ -0,0 +1,5 @@
# These are supported funding model platforms
github: tannerlinsley
patreon: tannerlinsley
custom: https://youtube.com/tannerlinsley
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
hooks: {
'pre-commit': 'lint-staged',
},
}
+1083 -48
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
const path = require('path');
module.exports = {
moduleDirectories: [
'node_modules',
/*
* make 'test/utils' available in tests, e.g.
*
* const {myModule} = require('utils/my-test-helper')
*/
__dirname,
],
rootDir: path.resolve(__dirname, '../..'),
roots: ['<rootDir>/src', __dirname],
};
+13
View File
@@ -0,0 +1,13 @@
const {rootDir} = require('./jest.common');
module.exports = {
rootDir,
displayName: 'lint',
runner: 'jest-runner-eslint',
testMatch: ['<rootDir>/src/**/*.js'],
testPathIgnorePatterns: ['node_modules', 'coverage', 'dist', '.test.js'],
};
+15
View File
@@ -0,0 +1,15 @@
const commonConfig = require('./jest.common');
module.exports = {
...commonConfig,
displayName: 'unit',
coverageDirectory: '../../coverage',
testMatch: ['<rootDir>/tests/**/*.js'],
transform: {
// '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
},
};
View File
+4
View File
@@ -0,0 +1,4 @@
{
"presets": ["react-app"],
"plugins": ["styled-components"]
}
+1
View File
@@ -0,0 +1 @@
SKIP_PREFLIGHT_CHECK=true
+7
View File
@@ -0,0 +1,7 @@
{
"extends": ["react-app", "prettier"],
"rules": {
// "eqeqeq": 0,
// "jsx-a11y/anchor-is-valid": 0
}
}
+23
View File
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+29
View File
@@ -0,0 +1,29 @@
const path = require('path')
const resolveFrom = require('resolve-from')
const fixLinkedDependencies = config => {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
react$: resolveFrom(path.resolve('node_modules'), 'react'),
'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'),
},
}
return config
}
const includeSrcDirectory = config => {
config.resolve = {
...config.resolve,
modules: [path.resolve('src'), ...config.resolve.modules],
}
return config
}
module.exports = [
['use-babel-config', '.babelrc'],
['use-eslint-config', '.eslintrc'],
fixLinkedDependencies,
// includeSrcDirectory,
]
+6
View File
@@ -0,0 +1,6 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app) and Rescripts.
You can:
- [Open this example in a new CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/basic)
- `yarn` and `yarn start` to run and edit the example
+35
View File
@@ -0,0 +1,35 @@
{
"private": true,
"scripts": {
"start": "rescripts start",
"build": "rescripts build",
"test": "rescripts test",
"eject": "rescripts eject"
},
"dependencies": {
"namor": "^1.1.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.1",
"react-table": "next",
"styled-components": "^4.3.2"
},
"devDependencies": {
"@rescripts/cli": "^0.0.11",
"@rescripts/rescript-use-babel-config": "^0.0.8",
"@rescripts/rescript-use-eslint-config": "^0.0.9",
"babel-eslint": "10.0.1"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+38
View File
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
+121
View File
@@ -0,0 +1,121 @@
import React from 'react'
import styled from 'styled-components'
import { useTable } 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 } = useTable({
columns,
data,
})
// Render the UI for your table
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody>
{rows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
)}
</tbody>
</table>
)
}
function App() {
const columns = React.useMemo(
() => [
{
Header: 'Name',
columns: [
{
Header: 'First Name',
accessor: 'firstName',
},
{
Header: 'Last Name',
accessor: 'lastName',
},
],
},
{
Header: 'Info',
columns: [
{
Header: 'Age',
accessor: 'age',
},
{
Header: 'Visits',
accessor: 'visits',
},
{
Header: 'Status',
accessor: 'status',
},
{
Header: 'Profile Progress',
accessor: 'progress',
},
],
},
],
[]
)
const data = React.useMemo(() => makeData(20), [])
return (
<Styles>
<Table columns={columns} data={data} />
</Styles>
)
}
export default App
+9
View File
@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
+13
View File
@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
+12
View File
@@ -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();
+40
View File
@@ -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()
}
+135
View File
@@ -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"]
}
+1
View File
@@ -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,36 @@
{
"private": true,
"scripts": {
"start": "rescripts start",
"build": "rescripts build",
"test": "rescripts test",
"eject": "rescripts eject"
},
"dependencies": {
"match-sorter": "^4.0.0",
"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,322 @@
import React from 'react'
import styled from 'styled-components'
import { useTable, useFilters } from 'react-table'
// A great library for fuzzy filtering/sorting items
import matchSorter from 'match-sorter'
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;
}
}
}
`
// Define a default UI for filtering
function DefaultColumnFilter({ filterValue, setFilter }) {
return (
<input
value={filterValue || ''}
onChange={e => {
setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely
}}
placeholder="Search..."
/>
)
}
// This is a custom filter UI for selecting
// a unique option from a list
function SelectColumnFilter({ filterValue, setFilter, preFilteredRows, id }) {
// Calculate the options for filtering
// using the preFilteredRows
const options = React.useMemo(() => {
const options = new Set()
preFilteredRows.forEach(row => {
options.add(row.values[id])
})
return [...options.values()]
}, [id, preFilteredRows])
// Render a multi-select box
return (
<select
value={filterValue}
onChange={e => {
setFilter(e.target.value || undefined)
}}
>
<option value="">All</option>
{options.map((option, i) => (
<option key={i} value={option}>
{option}
</option>
))}
</select>
)
}
// This is a custom filter UI that uses a
// slider to set the filter value between a column's
// min and max values
function SliderColumnFilter({ filterValue, setFilter, preFilteredRows, id }) {
// Calculate the min and max
// using the preFilteredRows
const [min, max] = React.useMemo(() => {
let min = 0
let max = 0
preFilteredRows.forEach(row => {
min = Math.min(row.values[id], min)
max = Math.max(row.values[id], max)
})
return [min, max]
}, [id, preFilteredRows])
return (
<>
<input
type="range"
min={min}
max={max}
value={filterValue || min}
onChange={e => {
setFilter(parseInt(e.target.value, 10))
}}
/>
<button onClick={() => setFilter(undefined)}>Off</button>
</>
)
}
// This is a custom UI for our 'between' or number range
// filter. It uses two number boxes and filters rows to
// ones that have values between the two
function NumberRangeColumnFilter({ filterValue = [], setFilter }) {
return (
<div
css={`
display: flex;
`}
>
<input
value={filterValue[0] || ''}
type="number"
onChange={e => {
const val = e.target.value
setFilter((old = []) => [val ? parseInt(val, 10) : undefined, old[1]])
}}
placeholder="Min"
css={`
width: 70px;
margin-right: 0.5rem;
`}
/>
to
<input
value={filterValue[1] || ''}
type="number"
onChange={e => {
const val = e.target.value
setFilter((old = []) => [old[0], val ? parseInt(val, 10) : undefined])
}}
placeholder="Max"
css={`
width: 70px;
margin-left: 0.5rem;
`}
/>
</div>
)
}
function fuzzyText(rows, id, filterValue) {
return matchSorter(rows, filterValue, { keys: [row => row.values[id]] })
}
// Let the table remove the filter if the string is empty
fuzzyText.autoRemove = val => !val
// Our table component
function Table({ columns, data }) {
const filterTypes = React.useMemo(
() => ({
// Add a new fuzzyText filter type.
fuzzyText,
// Or, override the default text filter to use
// "startWith"
text: (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
return rowValue !== undefined
? String(rowValue)
.toLowerCase()
.startsWith(String(filterValue).toLowerCase())
: true
})
},
}),
[]
)
const defaultColumn = React.useMemo(
() => ({
// Let's set up our default Filter UI
Filter: DefaultColumnFilter,
}),
[]
)
const { getTableProps, headerGroups, rows, prepareRow, state } = useTable(
{
columns,
data,
defaultColumn, // Be sure to pass the defaultColumn option
filterTypes,
},
useFilters // useFilters!
)
// We don't want to render all 2000 rows for this example, so cap
// it at 20 for this use case
const firstPageRows = rows.slice(0, 20)
return (
<>
<div>
<pre>
<code>{JSON.stringify(state[0].filters, null, 2)}</code>
</pre>
</div>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>
{column.render('Header')}
{/* Render the columns filter UI */}
<div>{column.canFilter ? column.render('Filter') : null}</div>
</th>
))}
</tr>
))}
</thead>
<tbody>
{firstPageRows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
)
})}
</tr>
)
)}
</tbody>
</table>
<br />
<div>Showing the first 20 results of {rows.length} rows</div>
</>
)
}
// Define a custom filter filter function!
function filterGreaterThan(rows, id, filterValue) {
return rows.filter(row => {
const rowValue = row.values[id]
return rowValue >= filterValue
})
}
// This is an autoRemove method on the filter function that
// when given the new filter value and returns true, the filter
// will be automatically removed. Normally this is just an undefined
// check, but here, we want to remove the filter if it's not a number
filterGreaterThan.autoRemove = val => typeof val !== 'number'
function App() {
const columns = React.useMemo(
() => [
{
Header: 'Name',
columns: [
{
Header: 'First Name',
accessor: 'firstName',
},
{
Header: 'Last Name',
accessor: 'lastName',
// Use our custom `fuzzyText` filter on this column
filter: 'fuzzyText',
},
],
},
{
Header: 'Info',
columns: [
{
Header: 'Age',
accessor: 'age',
Filter: SliderColumnFilter,
filter: 'equals',
},
{
Header: 'Visits',
accessor: 'visits',
Filter: NumberRangeColumnFilter,
filter: 'between',
},
{
Header: 'Status',
accessor: 'status',
Filter: SelectColumnFilter,
filter: 'includes',
},
{
Header: 'Profile Progress',
accessor: 'progress',
Filter: SliderColumnFilter,
filter: filterGreaterThan,
},
],
},
],
[]
)
const data = React.useMemo(() => makeData(10000), [])
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
+4
View File
@@ -0,0 +1,4 @@
{
"presets": ["react-app"],
"plugins": ["styled-components"]
}
+1
View File
@@ -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"
}
+130
View File
@@ -0,0 +1,130 @@
import React from 'react'
import styled from 'styled-components'
import { useTable, useSortBy } from 'react-table'
import makeData from './makeData'
const Styles = styled.div`
padding: 1rem;
table {
border-spacing: 0;
border: 1px solid black;
tr {
:last-child {
td {
border-bottom: 0;
}
}
}
th,
td {
margin: 0;
padding: 0.5rem;
border-bottom: 1px solid black;
border-right: 1px solid black;
:last-child {
border-right: 0;
}
}
}
`
function Table({ columns, data }) {
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{
columns,
data,
},
useSortBy
)
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
// Add the sorting props to control sorting. For this example
// we can add them into the header props
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
{/* Add a sort direction indicator */}
<span>
{column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''}
</span>
</th>
))}
</tr>
))}
</thead>
<tbody>
{rows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
)}
</tbody>
</table>
)
}
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(20), [])
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
+16
View File
@@ -0,0 +1,16 @@
const path = require('path');
const lintProject = require('./configs/tests/jest.lint');
const unitProject = require('./configs/tests/jest.unit');
module.exports = {
...require('./configs/tests/jest.common'),
projects: [lintProject, unitProject],
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
'jest-watch-select-projects',
],
};
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
'*.js': ['prettier --write', 'git add'],
}
+7289 -4400
View File
File diff suppressed because it is too large Load Diff
+22 -5
View File
@@ -1,6 +1,6 @@
{
"name": "react-table",
"version": "7.0.0-alpha.5",
"version": "7.0.0-alpha.9",
"description": "A fast, lightweight, opinionated table and datagrid built on React",
"license": "MIT",
"homepage": "https://github.com/tannerlinsley/react-table#readme",
@@ -15,11 +15,13 @@
"datagrid"
],
"main": "dist/index.js",
"_module": "dist/index.es.js",
"_jsnext:main": "dist/index.es.js",
"module": "dist/index.es.js",
"jsnext:main": "dist/index.es.js",
"scripts": {
"test": "cross-env CI=1 react-scripts test --env=jsdom",
"test:watch": "react-scripts test --env=jsdom",
"commit": "git-cz",
"test": "is-ci 'test:ci' 'test:dev'",
"test:dev": "jest --watch",
"test:ci": "jest",
"build": "rollup -c",
"start": "rollup -c -w",
"prepare": "yarn build",
@@ -46,8 +48,10 @@
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.2.0",
"@svgr/rollup": "^4.1.0",
"@testing-library/react": "^8.0.4",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "9.x",
"commitizen": "^4.0.3",
"cross-env": "^5.1.4",
"eslint": "5.x",
"eslint-config-prettier": "^4.3.0",
@@ -63,11 +67,24 @@
"eslint-plugin-react": "7.x",
"eslint-plugin-react-hooks": "1.5.0",
"eslint-plugin-standard": "^4.0.0",
"husky": "^3.0.1",
"is-ci-cli": "^1.1.1",
"jest": "^24.8.0",
"jest-cli": "^24.8.0",
"jest-runner-eslint": "^0.7.4",
"jest-watch-select-projects": "^0.1.2",
"jest-watch-typeahead": "^0.3.1",
"lint-staged": "^9.2.1",
"rollup": "^0.68.0",
"rollup-plugin-babel": "^4.1.0",
"rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-peer-deps-external": "^2.2.0",
"rollup-plugin-uglify": "^6.0.2"
},
"config": {
"commitizen": {
"path": "node_modules/cz-conventional-changelog"
}
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: false,
singleQuote: true,
}
+30 -21
View File
@@ -6,27 +6,36 @@ import { uglify } from 'rollup-plugin-uglify'
import pkg from './package.json'
export default {
input: 'src/index.js',
output: [
{
export default [
{
input: 'src/index.js',
output: {
file: pkg.main,
format: 'cjs',
sourcemap: true
}
// {
// file: pkg.module,
// format: "es",
// sourcemap: true
// }
],
plugins: [
external(),
babel({
exclude: 'node_modules/**'
}),
resolve(),
commonjs(),
uglify()
]
}
},
plugins: [
external(),
babel({
exclude: 'node_modules/**'
}),
resolve(),
commonjs(),
uglify()
]
},
{
input: 'src/index.js',
external: Object.keys(pkg.peerDependencies),
output: {
file: pkg.module,
format: 'es',
sourcemap: true
},
plugins: [
babel({
exclude: ['/**/node_modules/**']
})
]
}
]
+15 -4
View File
@@ -1,9 +1,20 @@
const actions = {}
const types = {}
export { actions }
export { actions, types }
export const addActions = acts => {
Object.keys(acts).forEach(key => {
actions[key] = acts[key]
export const addActions = (...acts) => {
acts.forEach(action => {
if (actions[action]) {
throw new Error(
`An React Table action type called ${action} has already been registered!`
)
}
// Action values are formatted this way to discourage
// you (the dev) from interacting with them in any way
// other than importing `{ actions } from 'react-table'`
// and referencing an action via `actions[actionName]`
actions[action] = `React Table Action: ${action}`
types[actions[action]] = true
})
}
+93
View File
@@ -0,0 +1,93 @@
export const text = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
return rowValue !== undefined
? String(rowValue)
.toLowerCase()
.includes(String(filterValue).toLowerCase())
: true
})
}
text.autoRemove = val => !val
export const exactText = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
return rowValue !== undefined
? String(rowValue).toLowerCase() === String(filterValue).toLowerCase()
: true
})
}
exactText.autoRemove = val => !val
export const exactTextCase = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
return rowValue !== undefined
? String(rowValue) === String(filterValue)
: true
})
}
exactTextCase.autoRemove = val => !val
export const includes = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
return filterValue.includes(rowValue)
})
}
includes.autoRemove = val => !val || !val.length
export const includesAll = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
return filterValue.every(val => rowValue.includes(val))
})
}
includesAll.autoRemove = val => !val || !val.length
export const exact = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
return rowValue === filterValue
})
}
exact.autoRemove = val => typeof val === 'undefined'
export const equals = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
return rowValue == filterValue
})
}
equals.autoRemove = val => val == null
export const between = (rows, id, filterValue) => {
let [min, max] = filterValue || []
min = typeof min === 'number' ? min : -Infinity
max = typeof max === 'number' ? max : Infinity
if (min > max) {
const temp = min
min = max
max = temp
}
return rows.filter(row => {
const rowValue = row.values[id]
return rowValue >= min && rowValue <= max
})
}
between.autoRemove = val =>
console.log(val) ||
!val ||
(typeof val[0] !== 'number' && typeof val[1] !== 'number')
+164 -140
View File
@@ -8,16 +8,166 @@ const propTypes = {
columns: PropTypes.arrayOf(
PropTypes.shape({
Cell: PropTypes.any,
Header: PropTypes.any
Header: PropTypes.any,
})
)
),
defaultColumn: PropTypes.any,
}
// Find the depth of the columns
function findMaxDepth(columns, depth = 0) {
return columns.reduce((prev, curr) => {
if (curr.columns) {
return Math.max(prev, findMaxDepth(curr.columns, depth + 1))
}
return depth
}, 0)
}
function decorateColumn(column, defaultColumn, parent, depth, index) {
// Apply the defaultColumn
column = { ...defaultColumn, ...column }
// First check for string accessor
let { id, accessor, Header } = column
if (typeof accessor === 'string') {
id = id || accessor
const accessorString = accessor
accessor = row => getBy(row, accessorString)
}
if (!id && typeof Header === 'string') {
id = Header
}
if (!id && column.columns) {
console.error(column)
throw new Error('A column ID (or unique "Header" value) is required!')
}
if (!id) {
console.error(column)
throw new Error('A column ID (or string accessor) is required!')
}
column = {
Header: ({ id }) => id,
Cell: ({ value }) => value,
show: true,
...column,
id,
accessor,
parent,
depth,
index,
}
return column
}
// Build the visible columns, headers and flat column list
function decorateColumnTree(columns, defaultColumn, parent, depth = 0) {
return columns.map((column, columnIndex) => {
column = decorateColumn(column, defaultColumn, parent, depth, columnIndex)
if (column.columns) {
column.columns = decorateColumnTree(
column.columns,
defaultColumn,
column,
depth + 1
)
}
return column
})
}
// Build the header groups from the bottom up
function makeHeaderGroups(columns, maxDepth, defaultColumn) {
const headerGroups = []
const removeChildColumns = column => {
delete column.columns
if (column.parent) {
removeChildColumns(column.parent)
}
}
columns.forEach(removeChildColumns)
const buildGroup = (columns, depth = 0) => {
const headerGroup = {
headers: [],
}
const parentColumns = []
const hasParents = columns.some(col => col.parent)
columns.forEach(column => {
const isFirst = !parentColumns.length
let latestParentColumn = [...parentColumns].reverse()[0]
// If the column has a parent, add it if necessary
if (column.parent) {
if (isFirst || latestParentColumn.originalID !== column.parent.id) {
parentColumns.push({
...column.parent,
originalID: column.parent.id,
id: [column.parent.id, parentColumns.length].join('_'),
})
}
} else if (hasParents) {
// If other columns have parents, add a place holder if necessary
const placeholderColumn = decorateColumn(
{
originalID: [column.id, 'placeholder', maxDepth - depth].join('_'),
id: [
column.id,
'placeholder',
maxDepth - depth,
parentColumns.length,
].join('_'),
},
defaultColumn
)
if (
isFirst ||
latestParentColumn.originalID !== placeholderColumn.originalID
) {
parentColumns.push(placeholderColumn)
}
}
// Establish the new columns[] relationship on the parent
if (column.parent || hasParents) {
latestParentColumn = [...parentColumns].reverse()[0]
latestParentColumn.columns = latestParentColumn.columns || []
if (!latestParentColumn.columns.includes(column)) {
latestParentColumn.columns.push(column)
}
}
headerGroup.headers.push(column)
})
headerGroups.push(headerGroup)
if (parentColumns.length) {
buildGroup(parentColumns)
}
}
buildGroup(columns)
return headerGroups.reverse()
}
export const useColumns = props => {
const {
debug,
columns: userColumns,
state: [{ groupBy }]
defaultColumn = {},
state: [{ groupBy }],
} = props
PropTypes.checkPropTypes(propTypes, props, 'property', 'useColumns')
@@ -26,86 +176,37 @@ export const useColumns = props => {
if (debug) console.info('getColumns')
// Decorate All the columns
let columnTree = decorateColumnTree(userColumns)
let columnTree = decorateColumnTree(userColumns, defaultColumn)
// Get the flat list of all columns
let columns = flattenBy(columnTree, 'columns')
columns = [
...groupBy.map(g => columns.find(col => col.id === g)),
...columns.filter(col => !groupBy.includes(col.id))
...columns.filter(col => !groupBy.includes(col.id)),
]
// Get headerGroups
const headerGroups = makeHeaderGroups(columns, findMaxDepth(columnTree))
const headerGroups = makeHeaderGroups(
columns,
findMaxDepth(columnTree),
defaultColumn
)
const headers = flattenBy(headerGroups, 'headers')
return {
columns,
headerGroups,
headers
headers,
}
}, [groupBy, userColumns])
}, [debug, defaultColumn, groupBy, userColumns])
return {
...props,
columns,
headerGroups,
headers
}
// Find the depth of the columns
function findMaxDepth(columns, depth = 0) {
return columns.reduce((prev, curr) => {
if (curr.columns) {
return Math.max(prev, findMaxDepth(curr.columns, depth + 1))
}
return depth
}, 0)
}
function decorateColumn(column, parent) {
// First check for string accessor
let { id, accessor, Header } = column
if (typeof accessor === 'string') {
id = id || accessor
const accessorString = accessor
accessor = row => getBy(row, accessorString)
}
if (!id && typeof Header === 'string') {
id = Header
}
if (!id) {
// Accessor, but no column id? This is bad.
console.error(column)
throw new Error('A column id is required!')
}
column = {
Header: '',
Cell: cell => cell.value,
show: true,
...column,
id,
accessor,
parent
}
return column
}
// Build the visible columns, headers and flat column list
function decorateColumnTree(columns, parent, depth = 0) {
return columns.map(column => {
column = decorateColumn(column, parent)
if (column.columns) {
column.columns = decorateColumnTree(column.columns, column, depth + 1)
}
return column
})
headers,
}
function flattenBy(columns, childKey) {
@@ -125,81 +226,4 @@ export const useColumns = props => {
return flatColumns
}
// Build the header groups from the bottom up
function makeHeaderGroups(columns, maxDepth) {
const headerGroups = []
const removeChildColumns = column => {
delete column.columns
if (column.parent) {
removeChildColumns(column.parent)
}
}
columns.forEach(removeChildColumns)
const buildGroup = (columns, depth = 0) => {
const headerGroup = {
headers: []
}
const parentColumns = []
const hasParents = columns.some(col => col.parent)
columns.forEach(column => {
const isFirst = !parentColumns.length
let latestParentColumn = [...parentColumns].reverse()[0]
// If the column has a parent, add it if necessary
if (column.parent) {
if (isFirst || latestParentColumn.originalID !== column.parent.id) {
parentColumns.push({
...column.parent,
originalID: column.parent.id,
id: [column.parent.id, parentColumns.length].join('_')
})
}
} else if (hasParents) {
// If other columns have parents, add a place holder if necessary
const placeholderColumn = decorateColumn({
originalID: [column.id, 'placeholder', maxDepth - depth].join('_'),
id: [
column.id,
'placeholder',
maxDepth - depth,
parentColumns.length
].join('_')
})
if (
isFirst ||
latestParentColumn.originalID !== placeholderColumn.originalID
) {
parentColumns.push(placeholderColumn)
}
}
// Establish the new columns[] relationship on the parent
if (column.parent || hasParents) {
latestParentColumn = [...parentColumns].reverse()[0]
latestParentColumn.columns = latestParentColumn.columns || []
if (!latestParentColumn.columns.includes(column)) {
latestParentColumn.columns.push(column)
}
}
headerGroup.headers.push(column)
})
headerGroups.push(headerGroup)
if (parentColumns.length) {
buildGroup(parentColumns)
}
}
buildGroup(columns)
return headerGroups.reverse()
}
}
-161
View File
@@ -1,161 +0,0 @@
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { defaultFilterFn, getFirstDefined } from '../utils'
import { addActions, actions } from '../actions'
import { defaultState } from './useTableState'
defaultState.filters = {}
addActions({
setFilter: '__setFilter__',
setAllFilters: '__setAllFilters__'
})
const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
filterFn: PropTypes.func,
filterAll: PropTypes.bool,
canFilter: PropTypes.bool,
Filter: PropTypes.any
})
),
filterFn: PropTypes.func,
manualFilters: PropTypes.bool
}
export const useFilters = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useFilters')
const {
debug,
rows,
columns,
filterFn = defaultFilterFn,
manualFilters,
disableFilters,
hooks,
state: [{ filters }, setState]
} = props
columns.forEach(column => {
const { id, accessor, canFilter } = column
column.canFilter = accessor
? getFirstDefined(
canFilter,
disableFilters === true ? false : undefined,
true
)
: false
// Was going to add this to the filter hook
column.filterValue = filters[id]
})
const setFilter = (id, val) => {
return setState(old => {
if (typeof val === 'undefined') {
const { [id]: prev, ...rest } = filters
return {
...old,
filters: {
...rest
}
}
}
return {
...old,
filters: {
...filters,
[id]: val
}
}
}, actions.setFilter)
}
const setAllFilters = filters => {
return setState(old => {
return {
...old,
filters
}
}, actions.setAllFilters)
}
hooks.columns.push(columns => {
columns.forEach(column => {
if (column.canFilter) {
column.setFilter = val => setFilter(column.id, val)
}
})
return columns
})
const filteredRows = useMemo(() => {
if (manualFilters || !Object.keys(filters).length) {
return rows
}
if (debug) console.info('getFilteredRows')
// Filters top level and nested rows
const filterRows = rows => {
let filteredRows = rows
filteredRows = Object.entries(filters).reduce(
(filteredSoFar, [columnID, filterValue]) => {
// Find the filters column
const column = columns.find(d => d.id === columnID)
// Don't filter hidden columns or columns that have had their filters disabled
if (!column || column.filterable === false) {
return filteredSoFar
}
const filterMethod = column.filterMethod || filterFn
// If 'filterAll' is set to true, pass the entire dataset to the filter method
if (column.filterAll) {
return filterMethod(filteredSoFar, columnID, filterValue, column)
}
return filteredSoFar.filter(row =>
filterMethod(row, columnID, filterValue, column)
)
},
rows
)
// Apply the filter to any subRows
filteredRows = filteredRows.map(row => {
if (!row.subRows) {
return row
}
return {
...row,
subRows: filterRows(row.subRows)
}
})
// then filter any rows without subcolumns because it would be strange to show
filteredRows = filteredRows.filter(row => {
if (!row.subRows) {
return true
}
return row.subRows.length > 0
})
return filteredRows
}
return filterRows(rows)
}, [rows, filters, manualFilters])
return {
...props,
setFilter,
setAllFilters,
rows: filteredRows
}
}
+6 -6
View File
@@ -1,8 +1,8 @@
import { useMemo } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
const propTypes = {
subRowsKey: PropTypes.string
subRowsKey: PropTypes.string,
}
export const useRows = props => {
@@ -10,7 +10,7 @@ export const useRows = props => {
const { debug, columns, subRowsKey = 'subRows', data } = props
const accessedRows = useMemo(() => {
const accessedRows = React.useMemo(() => {
if (debug) console.info('getAccessedRows')
// Access the row's data
@@ -29,7 +29,7 @@ export const useRows = props => {
path: [i], // used to create a key for each row even if not nested
subRows,
depth,
cells: [{}] // This is a dummy cell
cells: [{}], // This is a dummy cell
}
// Override common array functions (and the dummy cell's getCellProps function)
@@ -57,10 +57,10 @@ export const useRows = props => {
// Use the resolved data
return data.map((d, i) => accessRow(d, i))
}, [data, columns])
}, [debug, data, subRowsKey, columns])
return {
...props,
rows: accessedRows
rows: accessedRows,
}
}
-28
View File
@@ -1,28 +0,0 @@
export const useSimpleLayout = props => {
const {
hooks: { columns: columnsHooks, getHeaderProps, getCellProps }
} = props
columnsHooks.push(columns => {
getHeaderProps.push(column => ({
style: {
boxSizing: 'border-box',
width: column.width !== undefined ? `${column.width}px` : 'auto'
}
}))
getCellProps.push(cell => {
return {
style: {
boxSizing: 'border-box',
width:
cell.column.width !== undefined ? `${cell.column.width}px` : 'auto'
}
}
})
return columns
})
return props
}
+48 -36
View File
@@ -1,24 +1,32 @@
import PropTypes from 'prop-types'
//
import { flexRender, applyHooks, applyPropHooks, mergeProps } from '../utils'
import { applyHooks, applyPropHooks, mergeProps, flexRender } from '../utils'
import { useTableState } from './useTableState'
const renderErr =
'You must specify a render "type". This could be "Header", "Filter", or any other custom renderers you have set on your column.'
import { useColumns } from './useColumns'
import { useRows } from './useRows'
const propTypes = {
// General
data: PropTypes.array.isRequired,
debug: PropTypes.bool
debug: PropTypes.bool,
}
const renderErr =
'You must specify a valid render component. This could be "column.Cell", "column.Header", "column.Filter", "column.Aggregated" or any other custom renderer component.'
export const useTable = (props, ...plugins) => {
// Validate props
PropTypes.checkPropTypes(propTypes, props, 'property', 'useTable')
// Destructure props
let { data = [], state: userState, debug } = props
let {
data,
state: userState,
useColumns: userUseColumns = useColumns,
useRows: userUseRows = useRows,
debug,
} = props
debug = process.env.NODE_ENV === 'production' ? false : debug
@@ -36,12 +44,11 @@ export const useTable = (props, ...plugins) => {
headerGroups: [],
rows: [],
row: [],
renderableRows: [],
getTableProps: [],
getRowProps: [],
getHeaderRowProps: [],
getHeaderGroupProps: [],
getHeaderProps: [],
getCellProps: []
getCellProps: [],
}
// The initial api
@@ -49,56 +56,61 @@ export const useTable = (props, ...plugins) => {
...props,
data,
state,
hooks
hooks,
plugins,
}
if (debug) console.time('hooks')
// Loop through plugins to build the api out
api = plugins.filter(Boolean).reduce((prev, next) => next(prev), api)
api = [userUseColumns, userUseRows, ...plugins]
.filter(Boolean)
.reduce((prev, next) => next(prev), api)
if (debug) console.timeEnd('hooks')
// Run the beforeRender hook
if (debug) console.time('hooks.beforeRender')
applyHooks(api.hooks.beforeRender, undefined, api)
if (debug) console.timeEnd('hooks.beforeRender')
// Determine column visibility
api.columns.forEach(column => {
column.visible =
typeof column.show === 'function' ? column.show(api) : !!column.show
})
// Allow hooks to decorate columns
if (debug) console.time('hooks.columns')
api.columns = applyHooks(api.hooks.columns, api.columns, api)
if (debug) console.timeEnd('hooks.columns')
// Allow hooks to decorate headers
if (debug) console.time('hooks.headers')
api.headers = applyHooks(api.hooks.headers, api.headers, api)
if (debug) console.timeEnd('hooks.headers')
;[...api.columns, ...api.headers].forEach(column => {
// Give columns/headers rendering power
column.render = (type, userProps = {}) => {
if (!type) {
const Comp = typeof type === 'string' ? column[type] : type
if (typeof Comp === 'undefined') {
throw new Error(renderErr)
}
return flexRender(column[type], {
return flexRender(Comp, {
...api,
...column,
...userProps
...userProps,
})
}
// Give columns/headers getHeaderProps
// Give columns/headers a default getHeaderProps
column.getHeaderProps = props =>
mergeProps(
{
key: ['header', column.id].join('_'),
colSpan: column.columns ? column.columns.length : 1
colSpan: column.columns ? column.columns.length : 1,
},
applyPropHooks(api.hooks.getHeaderProps, column, api),
props
)
})
// Allow hooks to decorate headerGroups
if (debug) console.time('hooks.headerGroups')
api.headerGroups = applyHooks(
api.hooks.headerGroups,
@@ -122,12 +134,12 @@ export const useTable = (props, ...plugins) => {
// Give headerGroups getRowProps
if (headerGroup.headers.length) {
headerGroup.getRowProps = (props = {}) =>
headerGroup.getHeaderGroupProps = (props = {}) =>
mergeProps(
{
key: [`header${i}`].join('_')
key: [`header${i}`].join('_'),
},
applyPropHooks(api.hooks.getHeaderRowProps, headerGroup, api),
applyPropHooks(api.hooks.getHeaderGroupProps, headerGroup, api),
props
)
return true
@@ -150,7 +162,7 @@ export const useTable = (props, ...plugins) => {
row.getRowProps = props =>
mergeProps(
{ key: ['row', ...path].join('_') },
applyHooks(api.hooks.getRowProps, row, api),
applyPropHooks(api.hooks.getRowProps, row, api),
props
)
@@ -164,30 +176,33 @@ export const useTable = (props, ...plugins) => {
const cell = {
column,
row,
value: row.values[column.id]
value: row.values[column.id],
}
// Give each cell a getCellProps base
cell.getCellProps = props => {
const columnPathStr = [path, column.id].join('_')
return mergeProps(
{
key: ['cell', columnPathStr].join('_')
key: ['cell', columnPathStr].join('_'),
},
applyPropHooks(api.hooks.getCellProps, cell, api),
props
)
}
// Give each cell a renderer function (supports multiple renderers)
cell.render = (type, userProps = {}) => {
if (!type) {
throw new Error(
'You must specify a render "type". This could be "Cell", "Header", "Filter", "Aggregated" or any other custom renderers you have set on your column.'
)
const Comp = typeof type === 'string' ? column[type] : type
if (typeof Comp === 'undefined') {
throw new Error(renderErr)
}
return flexRender(column[type], {
return flexRender(Comp, {
...api,
...cell,
...userProps
...userProps,
})
}
@@ -198,8 +213,5 @@ export const useTable = (props, ...plugins) => {
api.getTableProps = userProps =>
mergeProps(applyPropHooks(api.hooks.getTableProps, api), userProps)
api.getRowProps = userProps =>
mergeProps(applyPropHooks(api.hooks.getRowProps, api), userProps)
return api
}
+20 -7
View File
@@ -1,4 +1,6 @@
import React from 'react'
//
import { types } from '../actions'
export const defaultState = {}
@@ -11,12 +13,12 @@ export const useTableState = (
) => {
let [state, setState] = userUseState({
...defaultState,
...initialState
...initialState,
})
const overriddenState = React.useMemo(() => {
const newState = {
...state
...state,
}
if (overrides) {
Object.keys(overrides).forEach(key => {
@@ -24,20 +26,31 @@ export const useTableState = (
})
}
return newState
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [overrides, state])
const overriddenStateRef = React.useRef()
overriddenStateRef.current = overriddenState
const reducedSetState = React.useCallback(
(updater, type) =>
setState(old => {
(updater, type) => {
if (!types[type]) {
console.info({
stateUpdaterFn: updater,
actionType: type,
currentState: overriddenStateRef.current,
})
throw new Error('Detected an unknown table action! (Details Above)')
}
return setState(old => {
const newState = updater(old)
return reducer(old, newState, type)
}),
})
},
[reducer, setState]
)
return React.useMemo(() => [overriddenState, reducedSetState], [
overriddenState,
reducedSetState
reducedSetState,
])
}
+11 -10
View File
@@ -1,13 +1,14 @@
import * as utils from './utils'
export { utils }
export { useTable } from './hooks/useTable'
export { useColumns } from './hooks/useColumns'
export { useRows } from './hooks/useRows'
export { useSimpleLayout } from './hooks/useSimpleLayout'
export { useExpanded } from './hooks/useExpanded'
export { useFilters } from './hooks/useFilters'
export { useGroupBy } from './hooks/useGroupBy'
export { useSortBy } from './hooks/useSortBy'
export { usePagination } from './hooks/usePagination'
export { useTableState } from './hooks/useTableState'
export { useFlexLayout } from './hooks/useFlexLayout'
export { useTokenPagination } from './hooks/useTokenPagination'
export { actions } from './actions'
export { useTableState, defaultState } from './hooks/useTableState'
export { useExpanded } from './plugin-hooks/useExpanded'
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 { useFlexLayout } from './plugin-hooks/useFlexLayout'
export { useTokenPagination } from './plugin-hooks/useTokenPagination'
export { actions, addActions } from './actions'
@@ -3,17 +3,15 @@ import PropTypes from 'prop-types'
import { getBy, getFirstDefined, setBy } from '../utils'
import { addActions, actions } from '../actions'
import { defaultState } from './useTableState'
import { defaultState } from '../hooks/useTableState'
defaultState.expanded = {}
addActions({
toggleExpanded: '__toggleExpanded__',
useExpanded: '__useExpanded__'
})
addActions('toggleExpanded', 'useExpanded')
const propTypes = {
expandedKey: PropTypes.string
manualExpandedKey: PropTypes.string,
paginateSubRows: PropTypes.bool,
}
export const useExpanded = props => {
@@ -21,11 +19,11 @@ export const useExpanded = props => {
const {
debug,
columns,
rows,
expandedKey = 'expanded',
manualExpandedKey = 'expanded',
hooks,
state: [{ expanded }, setState]
state: [{ expanded }, setState],
paginateSubRows = true,
} = props
const toggleExpandedByPath = (path, set) => {
@@ -35,7 +33,7 @@ export const useExpanded = props => {
set = getFirstDefined(set, !existing)
return {
...old,
expanded: setBy(expanded, path, set)
expanded: setBy(expanded, path, set),
}
}, actions.toggleExpanded)
}
@@ -43,6 +41,7 @@ export const useExpanded = props => {
hooks.row.push(row => {
const { path } = row
row.toggleExpanded = set => toggleExpandedByPath(path, set)
return row
})
const expandedRows = useMemo(() => {
@@ -52,27 +51,32 @@ export const useExpanded = props => {
// Here we do some mutation, but it's the last stage in the
// immutable process so this is safe
const handleRow = (row, index, depth = 0, parentPath = []) => {
const handleRow = (row, depth = 0, parentPath = []) => {
// Compute some final state for the row
const path = [...parentPath, index]
const path = [...parentPath, row.index]
row.path = path
row.depth = depth
row.isExpanded =
(row.original && row.original[expandedKey]) || getBy(expanded, path)
(row.original && row.original[manualExpandedKey]) ||
getBy(expanded, path)
expandedRows.push(row)
if (paginateSubRows || (!paginateSubRows && row.depth === 0)) {
expandedRows.push(row)
}
if (row.isExpanded && row.subRows && row.subRows.length) {
row.subRows.forEach((row, i) => handleRow(row, i, depth + 1, path))
row.subRows.forEach((row, i) => handleRow(row, depth + 1, path))
}
return row
}
rows.forEach((row, i) => handleRow(row, i))
rows.forEach(row => handleRow(row))
return expandedRows
}, [rows, expanded, columns])
}, [debug, rows, manualExpandedKey, expanded, paginateSubRows])
const expandedDepth = findExpandedDepth(expanded)
@@ -80,7 +84,7 @@ export const useExpanded = props => {
...props,
toggleExpandedByPath,
expandedDepth,
rows: expandedRows
rows: expandedRows,
}
}
+228
View File
@@ -0,0 +1,228 @@
import React from 'react'
import PropTypes from 'prop-types'
import { getFirstDefined, isFunction } from '../utils'
import * as filterTypes from '../filterTypes'
import { addActions, actions } from '../actions'
import { defaultState } from '../hooks/useTableState'
defaultState.filters = {}
addActions('setFilter', 'setAllFilters')
const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
filterFn: PropTypes.func,
filterAll: PropTypes.bool,
canFilter: PropTypes.bool,
Filter: PropTypes.any,
})
),
filterFn: PropTypes.func,
manualFilters: PropTypes.bool,
}
export const useFilters = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useFilters')
const {
debug,
rows,
columns,
filterTypes: userFilterTypes,
manualFilters,
disableFilters,
hooks,
state: [{ filters }, setState],
} = props
const setFilter = (id, updater) => {
const column = columns.find(d => d.id === id)
const filterMethod = getFilterMethod(
column.filter,
userFilterTypes || {},
filterTypes
)
const { autoRemove } = filterMethod
return setState(old => {
const newFilter =
typeof updater === 'function' ? updater(old.filters[id]) : updater
//
if (shouldAutoRemove(autoRemove, newFilter)) {
const { [id]: remove, ...newFilters } = old.filters
return {
...old,
filters: newFilters,
}
}
return {
...old,
filters: {
...old.filters,
[id]: newFilter,
},
}
}, actions.setFilter)
}
const setAllFilters = updater => {
return setState(old => {
const newFilters = typeof updater === 'function' ? updater(old) : updater
// Filter out undefined values
Object.keys(newFilters).forEach(id => {
const newFilter = newFilters[id]
const column = columns.find(d => d.id === id)
const autoRemove = getFilterMethod(
column.filter,
userFilterTypes || {},
filterTypes
)
if (shouldAutoRemove(autoRemove, newFilter)) {
delete newFilters[id]
}
})
return {
...old,
filters: newFilters,
}
}, actions.setAllFilters)
}
hooks.columns.push(columns => {
columns.forEach(column => {
const { id, accessor, canFilter } = column
// Determine if a column is filterable
column.canFilter = accessor
? getFirstDefined(
canFilter,
disableFilters === true ? false : undefined,
true
)
: false
// Provide the column a way of updating the filter value
column.setFilter = val => setFilter(column.id, val)
// Provide the current filter value to the column for
// convenience
column.filterValue = filters[id]
})
return columns
})
// TODO: Create a filter cache for incremental high speed multi-filtering
// This gets pretty complicated pretty fast, since you have to maintain a
// cache for each row group (top-level rows, and each row's recursive subrows)
// This would make multi-filtering a lot faster though. Too far?
const filteredRows = React.useMemo(() => {
if (manualFilters || !Object.keys(filters).length) {
return rows
}
if (debug) console.info('getFilteredRows')
// Filters top level and nested rows
const filterRows = rows => {
let filteredRows = rows
filteredRows = Object.entries(filters).reduce(
(filteredSoFar, [columnID, filterValue]) => {
// Find the filters column
const column = columns.find(d => d.id === columnID)
column.preFilteredRows = filteredSoFar
// Don't filter hidden columns or columns that have had their filters disabled
if (!column || column.filterable === false) {
return filteredSoFar
}
const columnFilter = column.filter || filterTypes.text
// Look up filter functions in this order:
// column function
// column string lookup on user filters
// column string lookup on built-in filters
// default function
// default string lookup on user filters
// default string lookup on built-in filters
const filterMethod = getFilterMethod(
columnFilter,
userFilterTypes || {},
filterTypes
)
if (!filterMethod) {
console.warn(
`Could not find a valid 'column.filter' for column with the ID: ${
column.id
}.`
)
return filteredSoFar
}
// Pass the rows, id, filterValue and column to the filterMethod
// to get the filtered rows back
return filterMethod(filteredSoFar, columnID, filterValue, column)
},
rows
)
// Apply the filter to any subRows
// We technically could do this recursively in the above loop,
// but that would severely hinder the API for the user, since they
// would be required to do that recursion in some scenarios
filteredRows = filteredRows.map(row => {
if (!row.subRows) {
return row
}
return {
...row,
subRows: filterRows(row.subRows),
}
})
// then filter any rows without subcolumns because it would be strange to show
filteredRows = filteredRows.filter(row => {
if (!row.subRows) {
return true
}
return row.subRows.length > 0
})
return filteredRows
}
return filterRows(rows)
}, [manualFilters, filters, debug, rows, columns, userFilterTypes])
return {
...props,
setFilter,
setAllFilters,
preFilteredRows: rows,
rows: filteredRows,
}
}
function shouldAutoRemove(autoRemove, value) {
return autoRemove ? autoRemove(value) : typeof value === 'undefined'
}
function getFilterMethod(filter, userFilterTypes, filterTypes) {
return isFunction(filter) || userFilterTypes[filter] || filterTypes[filter]
}
@@ -5,7 +5,7 @@ import { getFirstDefined, sum } from '../utils'
export const actions = {}
const propTypes = {
defaultFlex: PropTypes.number
defaultFlex: PropTypes.number,
}
export const useFlexLayout = props => {
@@ -16,10 +16,10 @@ export const useFlexLayout = props => {
hooks: {
columns: columnsHooks,
getRowProps,
getHeaderRowProps,
getHeaderGroupProps,
getHeaderProps,
getCellProps
}
getCellProps,
},
} = props
columnsHooks.push((columns, api) => {
@@ -47,47 +47,33 @@ export const useFlexLayout = props => {
const rowStyles = {
style: {
display: 'flex',
minWidth: `${sumWidth}px`
}
minWidth: `${sumWidth}px`,
},
}
api.rowStyles = rowStyles
getRowProps.push(() => rowStyles)
getHeaderRowProps.push(() => rowStyles)
getHeaderGroupProps.push(() => rowStyles)
getHeaderProps.push(column => ({
style: {
boxSizing: 'border-box',
...getStylesForColumn(column, columnMeasurements, defaultFlex, api)
}
// [refKey]: el => {
// renderedCellInfoRef.current[key] = {
// column,
// el
// };
// },
...getStylesForColumn(column, columnMeasurements, defaultFlex, api),
},
}))
getCellProps.push(cell => {
return {
style: {
// display: 'block',
// boxSizing: 'border-box',
...getStylesForColumn(
cell.column,
columnMeasurements,
defaultFlex,
undefined,
api
)
}
// [refKey]: el => {
// renderedCellInfoRef.current[columnPathStr] = {
// column,
// el
// };
// }
),
},
}
})
@@ -110,7 +96,7 @@ function getStylesForColumn(column, columnMeasurements, defaultFlex, api) {
return {
flex: `${flex} 0 auto`,
width: `${width}px`,
maxWidth: `${maxWidth}px`
maxWidth: `${maxWidth}px`,
}
}
@@ -122,6 +108,7 @@ function getSizesForColumn(
) {
if (columns) {
columns = columns
.filter(col => col.show || col.visible)
.map(column =>
getSizesForColumn(column, columnMeasurements, defaultFlex, api)
)
@@ -138,7 +125,7 @@ function getSizesForColumn(
return {
flex,
width,
maxWidth
maxWidth,
}
}
@@ -148,63 +135,6 @@ function getSizesForColumn(
width === 'auto'
? columnMeasurements[id] || defaultFlex
: getFirstDefined(width, minWidth, defaultFlex),
maxWidth
maxWidth,
}
}
// const resetRefs = () => {
// if (debug) console.info("resetRefs");
// renderedCellInfoRef.current = {};
// };
// const calculateAutoWidths = () => {
// RAF(() => {
// const newColumnMeasurements = {};
// Object.values(renderedCellInfoRef.current).forEach(({ column, el }) => {
// if (!el) {
// return;
// }
// let measurement = 0;
// const measureChildren = children => {
// if (children) {
// [].slice.call(children).forEach(child => {
// measurement = Math.max(
// measurement,
// Math.ceil(child.offsetWidth) || 0
// );
// measureChildren(child.children);
// });
// }
// return measurement;
// };
// const parentDims = getElementDimensions(el);
// measureChildren(el.children);
// newColumnMeasurements[column.id] = Math.max(
// newColumnMeasurements[column.id] || 0,
// measurement + parentDims.paddingLeft + parentDims.paddingRight
// );
// });
// const oldKeys = Object.keys(columnMeasurements);
// const newKeys = Object.keys(newColumnMeasurements);
// const needsUpdate =
// oldKeys.length !== newKeys.length ||
// oldKeys.some(key => {
// return columnMeasurements[key] !== newColumnMeasurements[key];
// });
// if (needsUpdate) {
// setState(old => {
// return {
// ...old,
// columnMeasurements: newColumnMeasurements
// };
// }, actions.updateAutoWidth);
// }
// });
// };
@@ -3,19 +3,17 @@ import PropTypes from 'prop-types'
import * as aggregations from '../aggregations'
import { addActions, actions } from '../actions'
import { defaultState } from './useTableState'
import { defaultState } from '../hooks/useTableState'
import {
mergeProps,
applyPropHooks,
defaultGroupByFn,
getFirstDefined
getFirstDefined,
} from '../utils'
defaultState.groupBy = []
addActions({
toggleGroupBy: '__toggleGroupBy__'
})
addActions('toggleGroupBy')
const propTypes = {
// General
@@ -23,12 +21,12 @@ const propTypes = {
PropTypes.shape({
aggregate: PropTypes.func,
canGroupBy: PropTypes.bool,
Aggregated: PropTypes.any
Aggregated: PropTypes.any,
})
),
groupByFn: PropTypes.func,
manualGrouping: PropTypes.bool,
aggregations: PropTypes.object
aggregations: PropTypes.object,
}
export const useGroupBy = props => {
@@ -43,7 +41,7 @@ export const useGroupBy = props => {
disableGrouping,
aggregations: userAggregations = {},
hooks,
state: [{ groupBy }, setState]
state: [{ groupBy }, setState],
} = props
columns.forEach(column => {
@@ -52,10 +50,10 @@ export const useGroupBy = props => {
column.canGroupBy = accessor
? getFirstDefined(
canGroupBy,
disableGrouping === true ? false : undefined,
true
)
canGroupBy,
disableGrouping === true ? false : undefined,
true
)
: false
column.Aggregated = column.Aggregated || column.Cell
@@ -68,12 +66,12 @@ export const useGroupBy = props => {
if (resolvedToggle) {
return {
...old,
groupBy: [...groupBy, id]
groupBy: [...groupBy, id],
}
}
return {
...old,
groupBy: groupBy.filter(d => d !== id)
groupBy: groupBy.filter(d => d !== id),
}
}, actions.toggleGroupBy)
}
@@ -97,14 +95,14 @@ export const useGroupBy = props => {
{
onClick: canGroupBy
? e => {
e.persist()
column.toggleGroupBy()
}
e.persist()
column.toggleGroupBy()
}
: undefined,
style: {
cursor: canGroupBy ? 'pointer' : undefined
cursor: canGroupBy ? 'pointer' : undefined,
},
title: 'Toggle GroupBy'
title: 'Toggle GroupBy',
},
applyPropHooks(api.hooks.getGroupByToggleProps, column, api),
props
@@ -169,7 +167,7 @@ export const useGroupBy = props => {
values,
subRows,
depth,
index
index,
}
return row
}
@@ -180,10 +178,18 @@ export const useGroupBy = props => {
// Assign the new data
return groupRecursively(rows, groupBy)
}, [rows, groupBy, columns, manualGroupBy])
}, [
manualGroupBy,
groupBy,
debug,
rows,
columns,
userAggregations,
groupByFn,
])
return {
...props,
rows: groupedRows
rows: groupedRows,
}
}
@@ -1,28 +1,33 @@
import { useMemo, useLayoutEffect } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
//
import { addActions, actions } from '../actions'
import { defaultState } from './useTableState'
import { defaultState } from '../hooks/useTableState'
defaultState.pageSize = 10
defaultState.pageIndex = 0
addActions({
pageChange: '__pageChange__'
})
addActions('pageChange', 'pageSizeChange')
const propTypes = {
// General
manualPagination: PropTypes.bool
manualPagination: PropTypes.bool,
}
// SSR has issues with useLayoutEffect still, so pony-fill with useEffect during SSR
let useLayoutEffect =
typeof window !== 'undefined' && process.env.NODE_ENV === 'production'
? React.useLayoutEffect
: React.useEffect
export const usePagination = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'usePagination')
const {
rows,
manualPagination,
disablePageResetOnDataChange,
debug,
state: [
{
@@ -31,27 +36,34 @@ export const usePagination = props => {
pageCount: userPageCount,
filters,
groupBy,
sortBy
sortBy,
},
setState
]
setState,
],
} = props
const pageOptions = React.useMemo(
() => [...new Array(userPageCount)].map((d, i) => i),
[userPageCount]
)
const rowDep = disablePageResetOnDataChange ? null : rows
useLayoutEffect(() => {
setState(
old => ({
...old,
pageIndex: 0
pageIndex: 0,
}),
actions.pageChange
)
}, [rows, filters, groupBy, sortBy])
}, [setState, rowDep, filters, groupBy, sortBy])
const { pages, pageCount } = useMemo(() => {
const { pages, pageCount } = React.useMemo(() => {
if (manualPagination) {
return {
pages: [rows],
pageCount: userPageCount
pageCount: userPageCount,
}
}
if (debug) console.info('getPages')
@@ -72,11 +84,10 @@ export const usePagination = props => {
return {
pages,
pageCount,
pageOptions
pageOptions,
}
}, [rows, pageSize, userPageCount])
}, [manualPagination, debug, rows, pageOptions, userPageCount, pageSize])
const pageOptions = [...new Array(pageCount)].map((d, i) => i)
const page = manualPagination ? rows : pages[pageIndex] || []
const canPreviousPage = pageIndex > 0
const canNextPage = pageIndex < pageCount - 1
@@ -89,7 +100,7 @@ export const usePagination = props => {
}
return {
...old,
pageIndex
pageIndex,
}
}, actions.pageChange)
}
@@ -109,15 +120,16 @@ export const usePagination = props => {
return {
...old,
pageIndex,
pageSize
pageSize,
}
}, actions.setPageSize)
}, actions.pageSizeChange)
}
return {
...props,
pages,
pageOptions,
pageCount,
page,
canPreviousPage,
canNextPage,
@@ -126,6 +138,6 @@ export const usePagination = props => {
nextPage,
setPageSize,
pageIndex,
pageSize
pageSize,
}
}
@@ -2,34 +2,37 @@ import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { addActions, actions } from '../actions'
import { defaultState } from './useTableState'
import { defaultState } from '../hooks/useTableState'
import * as sortTypes from '../sortTypes'
import {
mergeProps,
applyPropHooks,
getFirstDefined,
defaultOrderByFn,
defaultSortByFn
isFunction,
} from '../utils'
defaultState.sortBy = []
addActions({
sortByChange: '__sortByChange__'
})
addActions('sortByChange')
const propTypes = {
// General
columns: PropTypes.arrayOf(
PropTypes.shape({
sortByFn: PropTypes.func,
defaultSortDesc: PropTypes.bool
sortBy: PropTypes.func,
defaultSortDesc: PropTypes.bool,
})
),
sortByFn: PropTypes.func,
orderByFn: PropTypes.func,
sortTypes: PropTypes.object,
defaultSortType: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
manualSorting: PropTypes.bool,
disableSorting: PropTypes.bool,
defaultSortDesc: PropTypes.bool,
disableMultiSort: PropTypes.bool
disableMultiSort: PropTypes.bool,
disableSortRemove: PropTypes.bool,
disableMultiRemove: PropTypes.bool,
}
export const useSortBy = props => {
@@ -40,14 +43,33 @@ export const useSortBy = props => {
rows,
columns,
orderByFn = defaultOrderByFn,
sortByFn = defaultSortByFn,
defaultSort = 'alphanumeric',
sortTypes: userSortTypes,
manualSorting,
disableSorting,
defaultSortDesc,
disableSortRemove,
disableMultiRemove,
disableMultiSort,
hooks,
state: [{ sortBy }, setState]
state: [{ sortBy }, setState],
plugins,
} = props
// 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!'
)
}
columns.forEach(column => {
const { accessor, canSortBy } = column
column.canSortBy = accessor
@@ -73,78 +95,77 @@ export const useSortBy = props => {
// Find any existing sortBy for this column
const existingSortBy = sortBy.find(d => d.id === columnID)
const existingIndex = sortBy.findIndex(d => d.id == columnID)
const hasDescDefined = typeof desc !== 'undefined' && desc !== null
let newSortBy = []
// What should we do with this filter?
// What should we do with this sort action?
let action
if (!multi) {
if (sortBy.length <= 1 && existingSortBy) {
if ((existingSortBy.desc && !resolvedDefaultSortDesc) ||
(!existingSortBy.desc && resolvedDefaultSortDesc)) {
action = 'remove'
} else {
action = 'toggle'
}
if (!disableMultiSort && multi) {
if (existingSortBy) {
action = 'toggle'
} else {
action = 'add'
}
} else {
// Normal mode
if (existingIndex !== sortBy.length - 1) {
action = 'replace'
} else if (existingSortBy) {
action = 'toggle'
} else {
action = 'replace'
}
} else {
if (!existingSortBy) {
action = 'add'
} else {
if (hasDescDefined) {
action = 'set'
} else {
action = 'toggle'
}
}
}
// Handle toggle states that will remove the sortBy
if (
action === 'toggle' && // Must be toggling
!disableSortRemove && // If disableSortRemove, disable in general
!hasDescDefined && // Must not be setting desc
(multi ? !disableMultiRemove : true) && // If multi, don't allow if disableMultiRemove
((existingSortBy && // Finally, detect if it should indeed be removed
(existingSortBy.desc && !resolvedDefaultSortDesc)) ||
(!existingSortBy.desc && resolvedDefaultSortDesc))
) {
action = 'remove'
}
if (action === 'replace') {
newSortBy = [
{
id: columnID,
desc: hasDescDefined ? desc : resolvedDefaultSortDesc
}
desc: hasDescDefined ? desc : resolvedDefaultSortDesc,
},
]
} else if (action === 'add') {
newSortBy = [
...sortBy,
{
id: columnID,
desc: hasDescDefined ? desc : resolvedDefaultSortDesc
}
desc: hasDescDefined ? desc : resolvedDefaultSortDesc,
},
]
} else if (action === 'set') {
newSortBy = sortBy.map(d => {
if (d.id === columnID) {
return {
...d,
desc
}
}
return d
})
} else if (action === 'toggle') {
// This flips (or sets) the
newSortBy = sortBy.map(d => {
if (d.id === columnID) {
return {
...d,
desc: !existingSortBy.desc
desc: hasDescDefined ? desc : !existingSortBy.desc,
}
}
return d
})
} else if (action === 'remove') {
newSortBy = []
newSortBy = sortBy.filter(d => d.id !== columnID)
}
return {
...old,
sortBy: newSortBy
sortBy: newSortBy,
}
}, actions.sortByChange)
}
@@ -177,9 +198,9 @@ export const useSortBy = props => {
}
: undefined,
style: {
cursor: canSortBy ? 'pointer' : undefined
cursor: canSortBy ? 'pointer' : undefined,
},
title: 'Toggle SortBy'
title: 'Toggle SortBy',
},
applyPropHooks(api.hooks.getSortByToggleProps, column, api),
props
@@ -206,13 +227,11 @@ export const useSortBy = props => {
}
if (debug) console.info('getSortedRows')
const sortMethodsByColumnID = {}
const sortTypesByColumnID = {}
columns
.filter(col => col.sortMethod)
.forEach(col => {
sortMethodsByColumnID[col.id] = col.sortMethod
})
columns.forEach(col => {
sortTypesByColumnID[col.id] = col.sortBy
})
const sortData = rows => {
// Use the orderByFn to compose multiple sortBy's together.
@@ -222,21 +241,32 @@ export const useSortBy = props => {
rows,
sortBy.map(sort => {
// Support custom sorting methods for each column
const columnSortBy = sortMethodsByColumnID[sort.id]
const columnSort = sortTypesByColumnID[sort.id]
// Look up sortBy functions in this order:
// column function
// column string lookup on user sortType
// column string lookup on built-in sortType
// default function
// default string lookup on user sortType
// default string lookup on built-in sortType
const sortMethod =
isFunction(columnSort) ||
(userSortTypes || {})[columnSort] ||
sortTypes[columnSort] ||
isFunction(defaultSort) ||
(userSortTypes || {})[defaultSort] ||
sortTypes[defaultSort]
// Return the correct sortFn
return (a, b) =>
(columnSortBy || sortByFn)(
a.values[sort.id],
b.values[sort.id],
sort.desc
)
sortMethod(a.values[sort.id], b.values[sort.id], sort.desc)
}),
// Map the directions
sortBy.map(d => !d.desc)
)
// TODO: this should be optimized. Not good to loop again
// If there are sub-rows, sort them
sortedData.forEach(row => {
if (!row.subRows) {
return
@@ -248,10 +278,19 @@ export const useSortBy = props => {
}
return sortData(rows)
}, [manualSorting, sortBy, debug, columns, rows, orderByFn, sortByFn])
}, [
manualSorting,
sortBy,
debug,
columns,
rows,
orderByFn,
userSortTypes,
defaultSort,
])
return {
...props,
rows: sortedRows
rows: sortedRows,
}
}
@@ -1,13 +1,13 @@
import { useState } from 'react'
import React from 'react'
// Token pagination behaves a bit differently from
// index based pagination. This hook aids in that process.
export const useTokenPagination = () => {
const [pageToken, setPageToken] = useState()
const [nextPageToken, setNextPageToken] = useState()
const [previousPageTokens, setPreviousPageTokens] = useState([])
const [pageIndex, setPageIndex] = useState(0)
const [pageToken, setPageToken] = React.useState()
const [nextPageToken, setNextPageToken] = React.useState()
const [previousPageTokens, setPreviousPageTokens] = React.useState([])
const [pageIndex, setPageIndex] = React.useState(0)
// Since we're using pagination tokens intead of index, we need
// to be a bit clever with page-like navigation here.
@@ -46,6 +46,6 @@ export const useTokenPagination = () => {
nextPage,
canPreviousPage,
canNextPage,
resetPagination
resetPagination,
}
}
+77
View File
@@ -0,0 +1,77 @@
const reSplitAlphaNumeric = /([0-9]+)/gm
// Mixed sorting is slow, but very inclusive of many edge cases.
// It handles numbers, mixed alphanumeric combinations, and even
// null, undefined, and Infinity
export const alphanumeric = (a, b) => {
// Force to strings (or "" for unsupported types)
a = toString(a)
b = toString(b)
// Split on number groups, but keep the delimiter
// Then remove falsey split values
a = a.split(reSplitAlphaNumeric).filter(Boolean)
b = b.split(reSplitAlphaNumeric).filter(Boolean)
// While
while (a.length && b.length) {
let aa = a.shift()
let bb = b.shift()
const an = parseInt(aa, 10)
const bn = parseInt(bb, 10)
const combo = [an, bn].sort()
// Both are string
if (isNaN(combo[0])) {
if (aa > bb) {
return 1
}
if (bb > aa) {
return -1
}
continue
}
// One is a string, one is a number
if (isNaN(combo[1])) {
return isNaN(an) ? -1 : 1
}
// Both are numbers
if (an > bn) {
return 1
}
if (bn > an) {
return -1
}
}
return a.length - b.length
}
export function datetime(a, b) {
a = a.getTime()
b = b.getTime()
return numeric(a, b)
}
export function numeric(a, b) {
return a === b ? 0 : a > b ? 1 : -1
}
// Utils
function toString(a) {
if (typeof a === 'number') {
if (isNaN(a) || a === Infinity || a === -Infinity) {
return ''
}
return String(a)
}
if (typeof a === 'string') {
return a
}
return ''
}
+17 -40
View File
@@ -28,25 +28,6 @@ export function defaultOrderByFn(arr, funcs, dirs) {
})
}
export function defaultSortByFn(a, b, desc) {
// force null and undefined to the bottom
a = a === null || a === undefined ? '' : a
b = b === null || b === undefined ? '' : b
// force any string values to lowercase
a = typeof a === 'string' ? a.toLowerCase() : a
b = typeof b === 'string' ? b.toLowerCase() : b
// Return either 1 or -1 to indicate a sort priority
if (a > b) {
return 1
}
if (a < b) {
return -1
}
// returning 0, undefined or any falsey value will defer to the next
// sorting mechanism or eventually the columns index via the orderByFn
return 0
}
export function getFirstDefined(...args) {
for (let i = 0; i < args.length; i += 1) {
if (typeof args[i] !== 'undefined') {
@@ -67,14 +48,6 @@ export function defaultGroupByFn(rows, grouper) {
}, {})
}
export function defaultFilterFn(row, id, value, column) {
return row.values[id] !== undefined
? String(row.values[id])
.toLowerCase()
.includes(String(value).toLowerCase())
: true
}
export function setBy(obj = {}, path, value) {
const recurse = (obj, depth = 0) => {
const key = path[depth]
@@ -83,7 +56,7 @@ export function setBy(obj = {}, path, value) {
depth === path.length - 1 ? value : recurse(target, depth + 1)
return {
...obj,
[key]: subValue
[key]: subValue,
}
}
@@ -95,11 +68,11 @@ export function getElementDimensions(element) {
const style = window.getComputedStyle(element)
const margins = {
left: parseInt(style.marginLeft),
right: parseInt(style.marginRight)
right: parseInt(style.marginRight),
}
const padding = {
left: parseInt(style.paddingLeft),
right: parseInt(style.paddingRight)
right: parseInt(style.paddingRight),
}
return {
left: Math.ceil(rect.left),
@@ -111,17 +84,13 @@ export function getElementDimensions(element) {
marginRight: margins.right,
paddingLeft: padding.left,
paddingRight: padding.right,
scrollWidth: element.scrollWidth
scrollWidth: element.scrollWidth,
}
}
export function flexRender(Comp, props) {
if (typeof Comp === 'function') {
return Object.getPrototypeOf(Comp).isReactComponent ? (
<Comp {...props} />
) : (
Comp(props)
)
if (typeof Comp === 'function' || typeof Comp === 'object') {
return <Comp {...props} />
}
return Comp
}
@@ -134,9 +103,9 @@ export const mergeProps = (...groups) => {
...rest,
style: {
...(props.style || {}),
...style
...style,
},
className: [props.className, className].filter(Boolean).join(' ')
className: [props.className, className].filter(Boolean).join(' '),
}
})
return props
@@ -152,7 +121,7 @@ export const warnUnknownProps = props => {
if (Object.keys(props).length) {
throw new Error(
`Unknown options passed to useReactTable:
${JSON.stringify(props, null, 2)}`
)
}
@@ -162,6 +131,14 @@ export function sum(arr) {
return arr.reduce((prev, curr) => prev + curr, 0)
}
export function isFunction(a) {
if (typeof a === 'function') {
return a
}
}
//
function makePathArray(obj) {
return flattenDeep(obj)
.join('.')
View File
+3440 -73
View File
File diff suppressed because it is too large Load Diff