Add global filtering support via useGlobalFilter

This commit is contained in:
Tanner Linsley 2019-12-18 13:22:58 -07:00
parent 63d54b1e87
commit d18f1ba4d8
14 changed files with 1158 additions and 246 deletions

View File

@ -1,20 +1,20 @@
{ {
"dist/index.js": { "dist/index.js": {
"bundled": 108756, "bundled": 112754,
"minified": 50398, "minified": 52324,
"gzipped": 13421 "gzipped": 13765
}, },
"dist/index.es.js": { "dist/index.es.js": {
"bundled": 107845, "bundled": 111817,
"minified": 49586, "minified": 51488,
"gzipped": 13253, "gzipped": 13599,
"treeshaked": { "treeshaked": {
"rollup": { "rollup": {
"code": 80, "code": 80,
"import_statements": 21 "import_statements": 21
}, },
"webpack": { "webpack": {
"code": 8168 "code": 8484
} }
} }
}, },

View File

@ -1,3 +1,10 @@
## 7.0.0-rc.15
- Added `useGlobalFilter` hook for performing table-wide filtering
- Filter function signature has changed to supply an array of column IDs (to support both the tranditional single column style and the new multi-column search style introduced with `useGlobalFilter`).
- Removed the `column` parameter from the filter function signature as it was unused and no longer made sense with the array of IDs change above.
- Updated the `filtering` example to use a global filter in addition to the column filters
## 7.0.0-rc.14 ## 7.0.0-rc.14
- Changed the function signature for all propGetter hooks to accept a single object of named meta properties instead of a variable length of meta arguments. The user props object has also been added as a property to all prop getters. For example, `hooks.getRowProps.push((props, instance, row) => [...])` is now written `hooks.getRowProps.push((props, { instance, row, userProps }) => [...])` - Changed the function signature for all propGetter hooks to accept a single object of named meta properties instead of a variable length of meta arguments. The user props object has also been added as a property to all prop getters. For example, `hooks.getRowProps.push((props, instance, row) => [...])` is now written `hooks.getRowProps.push((props, { instance, row, userProps }) => [...])`

View File

@ -3,7 +3,7 @@
- Plugin Hook - Plugin Hook
- Optional - Optional
`useFilters` is the hook that implements **row filtering**. `useFilters` is the hook that implements **row filtering** and can even be used in conjunction with `useGlobalFilter`. It's also important to note that this hook can be used either **before or after** `useGlobalFilter`, depending on the performance characteristics you want to code for.
### Table Options ### Table Options

View File

@ -0,0 +1,52 @@
# `useGlobalFilter`
- Plugin Hook
- Optional
`useGlobalFilter` is the hook that implements **global row filtering** and can even be used in conjunction with `useFilters`. It's also important to note that this hook can be used either **before or after** `useFilters`, depending on the performance characteristics you want to code for.
### Table Options
The following options are supported via the main options object passed to `useTable(options)`
- `initialState.globalFilter: any`
- Must be **memoized**
- An array of objects containing columnId's and their corresponding filter values. This information is stored in state since the table is allowed to manipulate the filter through user interaction.
- `globalFilter: String | Function`
- Optional
- Defaults to `text`
- The resolved function from the this string/function will be used to filter the table's data.
- If a `string` is passed, the function with that name located on either the custom `filterTypes` option or the built-in filtering types object will be used. If
- If a `function` is passed, it will be used directly.
- For more information on filter types, see Filtering
- If a **function** is passed, it must be **memoized**
- `manualGlobalFilter: Bool`
- Enables filter detection functionality, but does not automatically perform row filtering.
- Turn this on if you wish to implement your own row filter outside of the table (eg. server-side or manual row grouping/nesting)
- `filterTypes: Object<filterKey: filterType>`
- Must be **memoized**
- Allows overriding or adding additional filter types for the table to use. If the globalFilter type isn't found on this object, it will default to using the built-in filter types.
- For more information on filter types, see Filtering
- `autoResetGlobalFilter: Boolean`
- Defaults to `true`
- When `true`, the `globalFilter` state will automatically reset if any of the following conditions are met:
- `data` is changed
- To disable, set to `false`
- For more information see the FAQ ["How do I stop my table state from automatically resetting when my data changes?"](./faq#how-do-i-stop-my-table-state-from-automatically-resetting-when-my-data-changes)
### Instance Properties
The following values are provided to the table `instance`:
- `rows: Array<Row>`
- An array of **filtered** rows.
- `preGlobalFilteredRows: 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.
- `setGlobalFilter: Function(columnId, filterValue) => void`
- An instance-level function used to update the filter value for a specific column.
### Example
- [Source](https://github.com/tannerlinsley/react-table/tree/master/examples/filtering)
- [Open in CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/filtering)

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { useTable, useFilters } from 'react-table' import { useTable, useFilters, useGlobalFilter } from 'react-table'
// A great library for fuzzy filtering/sorting items // A great library for fuzzy filtering/sorting items
import matchSorter from 'match-sorter' import matchSorter from 'match-sorter'
@ -35,6 +35,32 @@ const Styles = styled.div`
} }
` `
// Define a default UI for filtering
function GlobalFilter({
preGlobalFilteredRows,
globalFilter,
setGlobalFilter,
}) {
const count = preGlobalFilteredRows.length
return (
<span>
Search:{' '}
<input
value={globalFilter || ''}
onChange={e => {
setGlobalFilter(e.target.value || undefined) // Set undefined to remove the filter entirely
}}
placeholder={`${count} records...`}
style={{
fontSize: '1.1rem',
border: '0',
}}
/>
</span>
)
}
// Define a default UI for filtering // Define a default UI for filtering
function DefaultColumnFilter({ function DefaultColumnFilter({
column: { filterValue, preFilteredRows, setFilter }, column: { filterValue, preFilteredRows, setFilter },
@ -217,6 +243,9 @@ function Table({ columns, data }) {
rows, rows,
prepareRow, prepareRow,
state, state,
flatColumns,
preGlobalFilteredRows,
setGlobalFilter,
} = useTable( } = useTable(
{ {
columns, columns,
@ -224,7 +253,8 @@ function Table({ columns, data }) {
defaultColumn, // Be sure to pass the defaultColumn option defaultColumn, // Be sure to pass the defaultColumn option
filterTypes, filterTypes,
}, },
useFilters // useFilters! useFilters, // useFilters!
useGlobalFilter // useGlobalFilter!
) )
// We don't want to render all of the rows for this example, so cap // We don't want to render all of the rows for this example, so cap
@ -233,11 +263,6 @@ function Table({ columns, data }) {
return ( return (
<> <>
<div>
<pre>
<code>{JSON.stringify(state.filters, null, 2)}</code>
</pre>
</div>
<table {...getTableProps()}> <table {...getTableProps()}>
<thead> <thead>
{headerGroups.map(headerGroup => ( {headerGroups.map(headerGroup => (
@ -251,25 +276,41 @@ function Table({ columns, data }) {
))} ))}
</tr> </tr>
))} ))}
<tr>
<th
colSpan={flatColumns.length}
style={{
textAlign: 'left',
}}
>
<GlobalFilter
preGlobalFilteredRows={preGlobalFilteredRows}
globalFilter={state.globalFilter}
setGlobalFilter={setGlobalFilter}
/>
</th>
</tr>
</thead> </thead>
<tbody {...getTableBodyProps()}> <tbody {...getTableBodyProps()}>
{firstPageRows.map( {firstPageRows.map((row, i) => {
(row, i) => { prepareRow(row)
prepareRow(row); return (
return ( <tr {...row.getRowProps()}>
<tr {...row.getRowProps()}> {row.cells.map(cell => {
{row.cells.map(cell => { return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
return ( })}
<td {...cell.getCellProps()}>{cell.render('Cell')}</td> </tr>
) )
})} })}
</tr>
)}
)}
</tbody> </tbody>
</table> </table>
<br /> <br />
<div>Showing the first 20 results of {rows.length} rows</div> <div>Showing the first 20 results of {rows.length} rows</div>
<div>
<pre>
<code>{JSON.stringify(state.filters, null, 2)}</code>
</pre>
</div>
</> </>
) )
} }

View File

@ -8094,10 +8094,10 @@ react-scripts@3.0.1:
optionalDependencies: optionalDependencies:
fsevents "2.0.6" fsevents "2.0.6"
react-table@next: react-table@latest:
version "7.0.0-alpha.7" version "7.0.0-rc.14"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-alpha.7.tgz#0cb6da6f32adb397e68505b7cdd4880d15d73017" resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.14.tgz#bb4171fbdd56d78dd5f6b9e18694a36c00bd6261"
integrity sha512-oXE9RRkE2CFk1OloNCSTPQ9qxOdujgkCoW5b/srbJsBog/ySkWuozBTQkxH1wGNmnSxGyTrTxJqXdXPQam7VAw== integrity sha512-9NyzAE0kLH8HA+DK86ynxVDxniO5ZdggAqI7nDKauWXQGVYNFId8+JJSescJ2vwP9nR2JKyCEDG9c+CjIaLNkA==
react@^16.8.6: react@^16.8.6:
version "16.8.6" version "16.8.6"

View File

@ -1,79 +1,93 @@
export const text = (rows, id, filterValue) => { export const text = (rows, ids, filterValue) => {
rows = rows.filter(row => { rows = rows.filter(row => {
const rowValue = row.values[id] return ids.some(id => {
return String(rowValue) const rowValue = row.values[id]
.toLowerCase() return String(rowValue)
.includes(String(filterValue).toLowerCase()) .toLowerCase()
.includes(String(filterValue).toLowerCase())
})
}) })
return rows return rows
} }
text.autoRemove = val => !val text.autoRemove = val => !val
export const exactText = (rows, id, filterValue) => { export const exactText = (rows, ids, filterValue) => {
return rows.filter(row => { return rows.filter(row => {
const rowValue = row.values[id] return ids.some(id => {
return rowValue !== undefined const rowValue = row.values[id]
? String(rowValue).toLowerCase() === String(filterValue).toLowerCase() return rowValue !== undefined
: true ? String(rowValue).toLowerCase() === String(filterValue).toLowerCase()
: true
})
}) })
} }
exactText.autoRemove = val => !val exactText.autoRemove = val => !val
export const exactTextCase = (rows, id, filterValue) => { export const exactTextCase = (rows, ids, filterValue) => {
return rows.filter(row => { return rows.filter(row => {
const rowValue = row.values[id] return ids.some(id => {
return rowValue !== undefined const rowValue = row.values[id]
? String(rowValue) === String(filterValue) return rowValue !== undefined
: true ? String(rowValue) === String(filterValue)
: true
})
}) })
} }
exactTextCase.autoRemove = val => !val exactTextCase.autoRemove = val => !val
export const includes = (rows, id, filterValue) => { export const includes = (rows, ids, filterValue) => {
return rows.filter(row => { return rows.filter(row => {
const rowValue = row.values[id] return ids.some(id => {
return filterValue.includes(rowValue) const rowValue = row.values[id]
return filterValue.includes(rowValue)
})
}) })
} }
includes.autoRemove = val => !val || !val.length includes.autoRemove = val => !val || !val.length
export const includesAll = (rows, id, filterValue) => { export const includesAll = (rows, ids, filterValue) => {
return rows.filter(row => { return rows.filter(row => {
const rowValue = row.values[id] return ids.some(id => {
return ( const rowValue = row.values[id]
rowValue && return (
rowValue.length && rowValue &&
filterValue.every(val => rowValue.includes(val)) rowValue.length &&
) filterValue.every(val => rowValue.includes(val))
)
})
}) })
} }
includesAll.autoRemove = val => !val || !val.length includesAll.autoRemove = val => !val || !val.length
export const exact = (rows, id, filterValue) => { export const exact = (rows, ids, filterValue) => {
return rows.filter(row => { return rows.filter(row => {
const rowValue = row.values[id] return ids.some(id => {
return rowValue === filterValue const rowValue = row.values[id]
return rowValue === filterValue
})
}) })
} }
exact.autoRemove = val => typeof val === 'undefined' exact.autoRemove = val => typeof val === 'undefined'
export const equals = (rows, id, filterValue) => { export const equals = (rows, ids, filterValue) => {
return rows.filter(row => { return rows.filter(row => {
const rowValue = row.values[id] return ids.some(id => {
// eslint-disable-next-line eqeqeq const rowValue = row.values[id]
return rowValue == filterValue // eslint-disable-next-line eqeqeq
return rowValue == filterValue
})
}) })
} }
equals.autoRemove = val => val == null equals.autoRemove = val => val == null
export const between = (rows, id, filterValue) => { export const between = (rows, ids, filterValue) => {
let [min, max] = filterValue || [] let [min, max] = filterValue || []
min = typeof min === 'number' ? min : -Infinity min = typeof min === 'number' ? min : -Infinity
@ -86,8 +100,10 @@ export const between = (rows, id, filterValue) => {
} }
return rows.filter(row => { return rows.filter(row => {
const rowValue = row.values[id] return ids.some(id => {
return rowValue >= min && rowValue <= max const rowValue = row.values[id]
return rowValue >= min && rowValue <= max
})
}) })
} }

View File

@ -79,8 +79,6 @@ function reducer(state, action, previousState, instance) {
? action.value ? action.value
: !state.hiddenColumns.includes(action.columnId) : !state.hiddenColumns.includes(action.columnId)
console.log(action, should)
const hiddenColumns = should const hiddenColumns = should
? [...state.hiddenColumns, action.columnId] ? [...state.hiddenColumns, action.columnId]
: state.hiddenColumns.filter(d => d !== action.columnId) : state.hiddenColumns.filter(d => d !== action.columnId)

View File

@ -2,6 +2,7 @@ export * from './publicUtils'
export { useTable } from './hooks/useTable' export { useTable } from './hooks/useTable'
export { useExpanded } from './plugin-hooks/useExpanded' export { useExpanded } from './plugin-hooks/useExpanded'
export { useFilters } from './plugin-hooks/useFilters' export { useFilters } from './plugin-hooks/useFilters'
export { useGlobalFilter } from './plugin-hooks/useGlobalFilter'
export { useGroupBy } from './plugin-hooks/useGroupBy' export { useGroupBy } from './plugin-hooks/useGroupBy'
export { useSortBy } from './plugin-hooks/useSortBy' export { useSortBy } from './plugin-hooks/useSortBy'
export { usePagination } from './plugin-hooks/usePagination' export { usePagination } from './plugin-hooks/usePagination'

View File

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

View File

@ -2,6 +2,7 @@ import React from 'react'
import { render, fireEvent } from '@testing-library/react' import { render, fireEvent } from '@testing-library/react'
import { useTable } from '../../hooks/useTable' import { useTable } from '../../hooks/useTable'
import { useFilters } from '../useFilters' import { useFilters } from '../useFilters'
import { useGlobalFilter } from '../useGlobalFilter'
const data = [ const data = [
{ {
@ -58,13 +59,17 @@ function Table({ columns, data }) {
headerGroups, headerGroups,
rows, rows,
prepareRow, prepareRow,
flatColumns,
state,
setGlobalFilter,
} = useTable( } = useTable(
{ {
columns, columns,
data, data,
defaultColumn, defaultColumn,
}, },
useFilters useFilters,
useGlobalFilter
) )
return ( return (
@ -80,6 +85,28 @@ function Table({ columns, data }) {
))} ))}
</tr> </tr>
))} ))}
<tr>
<th
colSpan={flatColumns.length}
style={{
textAlign: 'left',
}}
>
<span>
<input
value={state.globalFilter || ''}
onChange={e => {
setGlobalFilter(e.target.value || undefined) // Set undefined to remove the filter entirely
}}
placeholder={`Global search...`}
style={{
fontSize: '1.1rem',
border: '0',
}}
/>
</span>
</th>
</tr>
</thead> </thead>
<tbody {...getTableBodyProps()}> <tbody {...getTableBodyProps()}>
{rows.map( {rows.map(
@ -142,8 +169,11 @@ function App() {
} }
test('renders a filterable table', () => { test('renders a filterable table', () => {
const { getAllByPlaceholderText, asFragment } = render(<App />) const { getAllByPlaceholderText, getByPlaceholderText, asFragment } = render(
<App />
)
const globalFilterInput = getByPlaceholderText('Global search...')
const filterInputs = getAllByPlaceholderText('Search...') const filterInputs = getAllByPlaceholderText('Search...')
const beforeFilter = asFragment() const beforeFilter = asFragment()
@ -156,6 +186,17 @@ test('renders a filterable table', () => {
const afterFilter2 = asFragment() const afterFilter2 = asFragment()
expect(beforeFilter).toMatchDiffSnapshot(afterFilter1) fireEvent.change(filterInputs[1], { target: { value: '' } })
expect(afterFilter1).toMatchDiffSnapshot(afterFilter2)
const afterFilter3 = asFragment()
fireEvent.change(globalFilterInput, { target: { value: 'li' } })
const afterFilter4 = asFragment()
expect(beforeFilter).toMatchSnapshot()
expect(afterFilter1).toMatchSnapshot()
expect(afterFilter2).toMatchSnapshot()
expect(afterFilter3).toMatchSnapshot()
expect(afterFilter4).toMatchSnapshot()
}) })

View File

@ -3,10 +3,11 @@ import React from 'react'
import { import {
actions, actions,
getFirstDefined, getFirstDefined,
isFunction, getFilterMethod,
useMountedLayoutEffect, useMountedLayoutEffect,
functionalUpdate, functionalUpdate,
useGetLatest, useGetLatest,
shouldAutoRemoveFilter,
} from '../utils' } from '../utils'
import * as filterTypes from '../filterTypes' import * as filterTypes from '../filterTypes'
@ -63,7 +64,7 @@ function reducer(state, action, previousState, instance) {
) )
// //
if (shouldAutoRemove(filterMethod.autoRemove, newFilter)) { if (shouldAutoRemoveFilter(filterMethod.autoRemove, newFilter)) {
return { return {
...state, ...state,
filters: state.filters.filter(d => d.id !== columnId), filters: state.filters.filter(d => d.id !== columnId),
@ -103,7 +104,7 @@ function reducer(state, action, previousState, instance) {
filterTypes filterTypes
) )
if (shouldAutoRemove(filterMethod.autoRemove, filter.value)) { if (shouldAutoRemoveFilter(filterMethod.autoRemove, filter.value)) {
return false return false
} }
return true return true
@ -164,17 +165,9 @@ function useInstance(instance) {
column.filterValue = found && found.value column.filterValue = found && found.value
}) })
// TODO: Create a filter cache for incremental high speed multi-filtering const [filteredRows, filteredFlatRows] = React.useMemo(() => {
// 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, filteredFlatRows } = React.useMemo(() => {
if (manualFilters || !filters.length) { if (manualFilters || !filters.length) {
return { return [rows, flatRows]
filteredRows: rows,
filteredFlatRows: flatRows,
}
} }
const filteredFlatRows = [] const filteredFlatRows = []
@ -213,9 +206,8 @@ function useInstance(instance) {
// to get the filtered rows back // to get the filtered rows back
column.filteredRows = filterMethod( column.filteredRows = filterMethod(
filteredSoFar, filteredSoFar,
columnId, [columnId],
filterValue, filterValue
column
) )
return column.filteredRows return column.filteredRows
@ -244,10 +236,7 @@ function useInstance(instance) {
return filteredRows return filteredRows
} }
return { return [filterRows(rows), filteredFlatRows]
filteredRows: filterRows(rows),
filteredFlatRows,
}
}, [manualFilters, filters, rows, flatRows, flatColumns, userFilterTypes]) }, [manualFilters, filters, rows, flatRows, flatColumns, userFilterTypes])
React.useMemo(() => { React.useMemo(() => {
@ -284,16 +273,3 @@ function useInstance(instance) {
setAllFilters, setAllFilters,
}) })
} }
function shouldAutoRemove(autoRemove, value) {
return autoRemove ? autoRemove(value) : typeof value === 'undefined'
}
function getFilterMethod(filter, userFilterTypes, filterTypes) {
return (
isFunction(filter) ||
userFilterTypes[filter] ||
filterTypes[filter] ||
filterTypes.text
)
}

View File

@ -0,0 +1,152 @@
import React from 'react'
import {
actions,
getFilterMethod,
useMountedLayoutEffect,
functionalUpdate,
useGetLatest,
shouldAutoRemoveFilter,
ensurePluginOrder,
} from '../utils'
import * as filterTypes from '../filterTypes'
// Actions
actions.resetGlobalFilter = 'resetGlobalFilter'
actions.setGlobalFilter = 'setGlobalFilter'
export const useGlobalFilter = hooks => {
hooks.stateReducers.push(reducer)
hooks.useInstance.push(useInstance)
}
useGlobalFilter.pluginName = 'useGlobalFilter'
function reducer(state, action, previousState, instance) {
if (action.type === actions.resetGlobalFilter) {
return {
...state,
globalFilter: instance.initialState.globalFilter || undefined,
}
}
if (action.type === actions.setGlobalFilter) {
const { filterValue } = action
const { userFilterTypes } = instance
const filterMethod = getFilterMethod(
instance.globalFilter,
userFilterTypes || {},
filterTypes
)
const newFilter = functionalUpdate(filterValue, state.globalFilter)
//
if (shouldAutoRemoveFilter(filterMethod.autoRemove, newFilter)) {
const { globalFilter, ...stateWithoutGlobalFilter } = state
return stateWithoutGlobalFilter
}
return {
...state,
globalFilter: newFilter,
}
}
}
function useInstance(instance) {
const {
data,
rows,
flatRows,
flatColumns,
filterTypes: userFilterTypes,
globalFilter,
manualGlobalFilter,
state: { globalFilter: globalFilterValue },
dispatch,
autoResetGlobalFilters = true,
plugins,
} = instance
ensurePluginOrder(plugins, [], 'useGlobalFilter', [
'useSortBy',
'useExpanded',
])
const setGlobalFilter = filterValue => {
dispatch({ type: actions.setGlobalFilter, filterValue })
}
// 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 [globalFilteredRows, globalFilteredFlatRows] = React.useMemo(() => {
if (manualGlobalFilter || typeof globalFilterValue === 'undefined') {
return [rows, flatRows]
}
const filteredFlatRows = []
const filterMethod = getFilterMethod(
globalFilter,
userFilterTypes || {},
filterTypes
)
if (!filterMethod) {
console.warn(`Could not find a valid 'globalFilter' option.`)
return rows
}
// Filters top level and nested rows
const filterRows = filteredRows => {
return filterMethod(
filteredRows,
flatColumns.map(d => d.id),
globalFilterValue
).map(row => {
filteredFlatRows.push(row)
return {
...row,
subRows:
row.subRows && row.subRows.length
? filterRows(row.subRows)
: row.subRows,
}
})
}
return [filterRows(rows), filteredFlatRows]
}, [
manualGlobalFilter,
globalFilter,
userFilterTypes,
rows,
flatRows,
flatColumns,
globalFilterValue,
])
const getAutoResetGlobalFilters = useGetLatest(autoResetGlobalFilters)
useMountedLayoutEffect(() => {
if (getAutoResetGlobalFilters()) {
dispatch({ type: actions.resetGlobalFilter })
}
}, [dispatch, manualGlobalFilter ? null : data])
Object.assign(instance, {
preGlobalFilteredRows: rows,
preGlobalFilteredFlatRows: flatRows,
globalFilteredRows,
globalFilteredFlatRows,
rows: globalFilteredRows,
flatRows: globalFilteredFlatRows,
setGlobalFilter,
})
}

View File

@ -267,6 +267,19 @@ export function expandRows(
return expandedRows return expandedRows
} }
export function getFilterMethod(filter, userFilterTypes, filterTypes) {
return (
isFunction(filter) ||
userFilterTypes[filter] ||
filterTypes[filter] ||
filterTypes.text
)
}
export function shouldAutoRemoveFilter(autoRemove, value) {
return autoRemove ? autoRemove(value) : typeof value === 'undefined'
}
// //
const reOpenBracket = /\[/g const reOpenBracket = /\[/g