Upgrade filter types to support configurable autoRemoval

This commit is contained in:
tannerlinsley 2019-07-26 10:19:38 -06:00
parent fd8a686b30
commit 90922f8637
25 changed files with 10987 additions and 38 deletions

View File

@ -333,7 +333,7 @@ The following options are supported on any column object you can pass to `column
- Must return valid JSX
- This function (or component) is primarily used for formatting the column value, eg. If your column accessor returns a date object, you can use a `Cell` function to format that date to a readable format.
### Table Output
### `Instance` Properties
The following properties are available on the table instance returned from `useTable`
@ -531,7 +531,7 @@ The following options are supported via the main options object passed to `useTa
- Must be **memoized**
- Allows overriding or adding additional aggregation functions for use when grouping/aggregating row values. If an aggregation key isn't found on this object, it will default to using the [built-in aggregation functions](TODO)
### Instance Variables
### `Instance` Properties
The following values are provided to the table `instance`:
@ -589,12 +589,15 @@ The following options are supported via the main options object passed to `useTa
- Allows overriding or adding additional filter types for columns to use. If a column's filter type isn't found on this object, it will default to using the [built-in filter types](TODO).
- For mor information on filter types, see [Filtering](TODO)
### Instance Variables
### `Instance` Properties
The following values are provided to the table `instance`:
- `rows: Array<Row>`
- An array of **filtered** rows.
- `preFilteredRows: Array<Row>`
- The array of rows **used right before filtering**.
- Among many other use-cases, these rows are directly useful for building option lists in filters, since the resulting filtered `rows` do not contain every possible option.
- `setFilter: Function(columnID, filterValue) => void`
- An instance-level function used to update the filter value for a specific column.
- `setAllFilters: Function(filtersObject) => void`
@ -627,16 +630,20 @@ const filterTypes = React.useMemo(() => ({
}
}), [matchSorter])
// Override the default column filter to be our new `fuzzyText` filter type
const defaultColumn = React.useMemo(() => ({
filter: 'fuzzyText'
}))
const { rows } = useTable(
{
// state[0].groupBy === ['firstName']
state,
// Override the default filter to be our new `fuzzyText` filter type
defaultFilter: 'fuzzyText',
manualFilters: false,
disableFilters: false,
// Pass our custom filter types
filterTypes,
defaultColumn
},
useFilters
)

View File

@ -45,7 +45,7 @@ function Table({ columns, data }) {
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getRowProps()}>
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,36 @@
{
"private": true,
"scripts": {
"start": "rescripts start",
"build": "rescripts build",
"test": "rescripts test",
"eject": "rescripts eject"
},
"dependencies": {
"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

View File

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

View File

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

View File

@ -0,0 +1,323 @@
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) {
console.log(rows, 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

View File

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

View File

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

View File

@ -0,0 +1,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();

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()
}

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

View File

@ -46,7 +46,7 @@ function Table({ columns, data }) {
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getRowProps()}>
<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

View File

@ -9,6 +9,8 @@ export const text = (rows, id, filterValue) => {
})
}
text.autoRemove = val => !val
export const exactText = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
@ -18,6 +20,8 @@ export const exactText = (rows, id, filterValue) => {
})
}
exactText.autoRemove = val => !val
export const exactTextCase = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
@ -27,6 +31,8 @@ export const exactTextCase = (rows, id, filterValue) => {
})
}
exactTextCase.autoRemove = val => !val
export const includes = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
@ -34,6 +40,8 @@ export const includes = (rows, id, filterValue) => {
})
}
includes.autoRemove = val => !val || !val.length
export const includesAll = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
@ -41,6 +49,8 @@ export const includesAll = (rows, id, filterValue) => {
})
}
includesAll.autoRemove = val => !val || !val.length
export const exact = (rows, id, filterValue) => {
return rows.filter(row => {
const rowValue = row.values[id]
@ -48,9 +58,36 @@ export const exact = (rows, id, filterValue) => {
})
}
export const between = (rows, id, 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[0] && rowValue <= filterValue[1]
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')

View File

@ -57,6 +57,7 @@ export const useTable = (props, ...plugins) => {
data,
state,
hooks,
plugins,
}
if (debug) console.time('hooks')

View File

@ -1,4 +1,4 @@
import { useMemo } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { getFirstDefined, isFunction } from '../utils'
@ -33,40 +33,68 @@ export const useFilters = props => {
rows,
columns,
filterTypes: userFilterTypes,
defaultFilter = filterTypes.text,
manualFilters,
disableFilters,
hooks,
state: [{ filters }, setState],
} = props
const setFilter = (id, val) => {
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 => {
if (typeof val === 'undefined') {
const { [id]: prev, ...rest } = filters
const newFilter =
typeof updater === 'function' ? updater(old.filters[id]) : updater
//
if (shouldAutoRemove(autoRemove, newFilter)) {
const { [id]: remove, ...newFilters } = old.filters
return {
...old,
filters: {
...rest,
},
filters: newFilters,
}
}
return {
...old,
filters: {
...filters,
[id]: val,
...old.filters,
[id]: newFilter,
},
}
}, actions.setFilter)
}
const setAllFilters = filters => {
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,
filters: newFilters,
}
}, actions.setAllFilters)
}
@ -95,7 +123,12 @@ export const useFilters = props => {
return columns
})
const filteredRows = useMemo(() => {
// 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
}
@ -111,11 +144,15 @@ export const useFilters = props => {
// 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
@ -123,13 +160,11 @@ export const useFilters = props => {
// default function
// default string lookup on user filters
// default string lookup on built-in filters
const filterMethod =
isFunction(column.filter) ||
(userFilterTypes || {})[column.filter] ||
filterTypes[column.filter] ||
isFunction(defaultFilter) ||
(userFilterTypes || {})[defaultFilter] ||
filterTypes[defaultFilter]
const filterMethod = getFilterMethod(
columnFilter,
userFilterTypes || {},
filterTypes
)
if (!filterMethod) {
console.warn(
@ -148,6 +183,9 @@ export const useFilters = props => {
)
// 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
@ -170,20 +208,21 @@ export const useFilters = props => {
}
return filterRows(rows)
}, [
manualFilters,
filters,
debug,
rows,
columns,
userFilterTypes,
defaultFilter,
])
}, [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]
}

View File

@ -53,8 +53,23 @@ export const useSortBy = props => {
disableMultiSort,
hooks,
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

View File

@ -137,6 +137,8 @@ export function isFunction(a) {
}
}
//
function makePathArray(obj) {
return flattenDeep(obj)
.join('.')