diff --git a/README.md b/README.md index 158ba69..4808cbd 100644 --- a/README.md +++ b/README.md @@ -351,13 +351,13 @@ The following options are supported on any column object you can pass to `column - The data model for hidden columns is still calculated including sorting, filters, and grouping. - `Header: String | Function | React.Component => JSX` - Optional - - Defaults to `({ id }) => id` + - Defaults to `() => null` - Receives the table instance and column model as props - Must either be a **string or return valid JSX** - If a function/component is passed, it will be used for formatting the header value, eg. You can use a `Header` function to dynamically format the header using any table or column state. - `Cell: Function | React.Component => JSX` - Optional - - Defaults to `({ value }) => value` + - Defaults to `({ cell: { value } }) => value` - Receives the table instance and cell model as props - 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. @@ -424,7 +424,8 @@ The following properties are available on every `Column` object returned by the - `visible: Boolean` - The resolved visible state for the column, derived from the column's `show` property - `render: Function(type: String | Function | Component, ?props)` - - This function is used to render content in context of a column. + - This function is used to render content with the added context of a column. + - The entire table `instance` will be passed to the renderer with the addition of a `column` property, containing a reference to the column - If `type` is a string, will render using the `column[type]` renderer. React Table ships with default `Header` renderers. Other renderers like `Filter` are available via hooks like `useFilters`. - If a function or component is passed instead of a string, it will be be passed the table instance and column model as props and is expected to return any valid JSX. - `getHeaderProps: Function(?props)` @@ -479,7 +480,8 @@ The following additional properties are available on every `Cell` object returne - Custom props may be passed. **NOTE: Custom props will override built-in table props, so be careful!** - `render: Function(type: String | Function | Component, ?props)` - **Required** - - This function is used to render content in context of a cell. + - This function is used to render content with the added context of a cell. + - The entire table `instance` will be passed to the renderer with the addition of `column`, `row` and `cell` properties, containing a reference to each respective item. - If `type` is a string, will render using the `column[type]` renderer. React Table ships with a default `Cell` renderer. Other renderers like `Aggregated` are available via hooks like `useFilters`. - If a function or component is passed instead of a string, it will be be passed the table instance and cell model as props and is expected to return any valid JSX. @@ -797,7 +799,7 @@ The following properties are available on every `Column` object returned by the - An column-level function used to update the filter value for this column - `filterValue: any` - The current filter value for this column, resolved from the table state's `filters` object -- `preFilteredRows: Array` +- `preFilteredColumnRows: Array` - The array of rows that were originally passed to this columns filter **before** they were filtered. - This array of rows can be useful if building faceted filter options. @@ -1064,7 +1066,7 @@ function App() { // then sum any of those counts if they are // aggregated further aggregate: ['sum', 'count'], - Aggregated: ({ value }) => `${value} Names`, + Aggregated: ({ cell: { value } }) => `${value} Names`, }, { Header: 'Last Name', @@ -1074,7 +1076,7 @@ function App() { // being aggregated, then sum those counts if // they are aggregated further aggregate: ['sum', 'uniqueCount'], - Aggregated: ({ value }) => `${value} Unique Names`, + Aggregated: ({ cell: { value } }) => `${value} Unique Names`, }, ], }, @@ -1086,14 +1088,14 @@ function App() { accessor: 'age', // Aggregate the average age of visitors aggregate: 'average', - Aggregated: ({ value }) => `${value} (avg)`, + Aggregated: ({ cell: { value } }) => `${value} (avg)`, }, { Header: 'Visits', accessor: 'visits', // Aggregate the sum of all visits aggregate: 'sum', - Aggregated: ({ value }) => `${value} (total)`, + Aggregated: ({ cell: { value } }) => `${value} (total)`, }, { Header: 'Status', @@ -1104,7 +1106,7 @@ function App() { accessor: 'progress', // Use our custom roundedMedian aggregator aggregate: roundedMedian, - Aggregated: ({ value }) => `${value} (med)`, + Aggregated: ({ cell: { value } }) => `${value} (med)`, }, ], }, diff --git a/examples/filtering/src/App.js b/examples/filtering/src/App.js index d510680..80d31f6 100644 --- a/examples/filtering/src/App.js +++ b/examples/filtering/src/App.js @@ -36,14 +36,16 @@ const Styles = styled.div` ` // Define a default UI for filtering -function DefaultColumnFilter({ filterValue, setFilter }) { +function DefaultColumnFilter({ filterValue, preFilteredRows, setFilter }) { + const count = preFilteredRows.length + return ( { setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely }} - placeholder="Search..." + placeholder={`Search ${count} records...`} /> ) } @@ -88,6 +90,7 @@ function SelectColumnFilter({ filterValue, setFilter, preFilteredRows, id }) { function SliderColumnFilter({ filterValue, setFilter, preFilteredRows, id }) { // Calculate the min and max // using the preFilteredRows + const [min, max] = React.useMemo( () => { let min = 0 @@ -120,7 +123,25 @@ function SliderColumnFilter({ filterValue, setFilter, preFilteredRows, id }) { // 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 }) { +function NumberRangeColumnFilter({ + filterValue = [], + preFilteredRows, + setFilter, + id, +}) { + 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 (
[val ? parseInt(val, 10) : undefined, old[1]]) }} - placeholder="Min" + placeholder={`Min (${min})`} style={{ width: '70px', marginRight: '0.5rem', @@ -148,7 +169,7 @@ function NumberRangeColumnFilter({ filterValue = [], setFilter }) { const val = e.target.value setFilter((old = []) => [old[0], val ? parseInt(val, 10) : undefined]) }} - placeholder="Max" + placeholder={`Max (${max})`} style={{ width: '70px', marginLeft: '0.5rem', diff --git a/src/hooks/useTable.js b/src/hooks/useTable.js index 50470ce..dd4d684 100755 --- a/src/hooks/useTable.js +++ b/src/hooks/useTable.js @@ -102,27 +102,30 @@ export const useTable = (props, ...plugins) => { ]) // Allow hooks to decorate columns (and trigger this memoization via deps) - columns = React.useMemo(() => { - if (process.env.NODE_ENV === 'development' && debug) - console.time('hooks.columnsBeforeHeaderGroups') - const newColumns = applyHooks( - instanceRef.current.hooks.columnsBeforeHeaderGroups, + columns = React.useMemo( + () => { + if (process.env.NODE_ENV === 'development' && debug) + console.time('hooks.columnsBeforeHeaderGroups') + const newColumns = applyHooks( + instanceRef.current.hooks.columnsBeforeHeaderGroups, + columns, + instanceRef.current + ) + if (process.env.NODE_ENV === 'development' && debug) + console.timeEnd('hooks.columnsBeforeHeaderGroups') + return newColumns + }, + [ columns, - instanceRef.current - ) - if (process.env.NODE_ENV === 'development' && debug) - console.timeEnd('hooks.columnsBeforeHeaderGroups') - return newColumns - }, [ - columns, - debug, - // eslint-disable-next-line react-hooks/exhaustive-deps - ...applyHooks( - instanceRef.current.hooks.columnsBeforeHeaderGroupsDeps, - [], - instanceRef.current - ), - ]) + debug, + // eslint-disable-next-line react-hooks/exhaustive-deps + ...applyHooks( + instanceRef.current.hooks.columnsBeforeHeaderGroupsDeps, + [], + instanceRef.current + ), + ] + ) // Make the headerGroups const headerGroups = React.useMemo( @@ -141,69 +144,72 @@ export const useTable = (props, ...plugins) => { }) // Access the row model - const [rows, rowPaths, flatRows] = React.useMemo(() => { - if (process.env.NODE_ENV === 'development' && debug) - console.time('getAccessedRows') + const [rows, rowPaths, flatRows] = React.useMemo( + () => { + if (process.env.NODE_ENV === 'development' && debug) + console.time('getAccessedRows') - let flatRows = 0 - const rowPaths = [] + let flatRows = 0 + const rowPaths = [] - // Access the row's data - const accessRow = (originalRow, i, depth = 0, parentPath = []) => { - // Keep the original reference around - const original = originalRow + // Access the row's data + const accessRow = (originalRow, i, depth = 0, parentPath = []) => { + // Keep the original reference around + const original = originalRow - // Make the new path for the row - const path = [...parentPath, i] + // Make the new path for the row + const path = [...parentPath, i] - flatRows++ - rowPaths.push(path.join('.')) + flatRows++ + rowPaths.push(path.join('.')) - // Process any subRows - const subRows = originalRow[subRowsKey] - ? originalRow[subRowsKey].map((d, i) => - accessRow(d, i, depth + 1, path) + // Process any subRows + const subRows = originalRow[subRowsKey] + ? originalRow[subRowsKey].map((d, i) => + accessRow(d, i, depth + 1, path) + ) + : [] + + const row = { + original, + index: i, + path, // used to create a key for each row even if not nested + subRows, + depth, + cells: [{}], // This is a dummy cell + } + + // Override common array functions (and the dummy cell's getCellProps function) + // to show an error if it is accessed without calling prepareRow + const unpreparedAccessWarning = () => { + throw new Error( + 'React-Table: You have not called prepareRow(row) one or more rows you are attempting to render.' ) - : [] + } + row.cells.map = unpreparedAccessWarning + row.cells.filter = unpreparedAccessWarning + row.cells.forEach = unpreparedAccessWarning + row.cells[0].getCellProps = unpreparedAccessWarning - const row = { - original, - index: i, - path, // used to create a key for each row even if not nested - subRows, - depth, - cells: [{}], // This is a dummy cell + // Create the cells and values + row.values = {} + instanceRef.current.columns.forEach(column => { + row.values[column.id] = column.accessor + ? column.accessor(originalRow, i, { subRows, depth, data }) + : undefined + }) + + return row } - // Override common array functions (and the dummy cell's getCellProps function) - // to show an error if it is accessed without calling prepareRow - const unpreparedAccessWarning = () => { - throw new Error( - 'React-Table: You have not called prepareRow(row) one or more rows you are attempting to render.' - ) - } - row.cells.map = unpreparedAccessWarning - row.cells.filter = unpreparedAccessWarning - row.cells.forEach = unpreparedAccessWarning - row.cells[0].getCellProps = unpreparedAccessWarning - - // Create the cells and values - row.values = {} - instanceRef.current.columns.forEach(column => { - row.values[column.id] = column.accessor - ? column.accessor(originalRow, i, { subRows, depth, data }) - : undefined - }) - - return row - } - - // Use the resolved data - const accessedData = data.map((d, i) => accessRow(d, i)) - if (process.env.NODE_ENV === 'development' && debug) - console.timeEnd('getAccessedRows') - return [accessedData, rowPaths, flatRows] - }, [debug, data, subRowsKey]) + // Use the resolved data + const accessedData = data.map((d, i) => accessRow(d, i)) + if (process.env.NODE_ENV === 'development' && debug) + console.timeEnd('getAccessedRows') + return [accessedData, rowPaths, flatRows] + }, + [debug, data, subRowsKey] + ) instanceRef.current.rows = rows instanceRef.current.rowPaths = rowPaths @@ -237,7 +243,7 @@ export const useTable = (props, ...plugins) => { return flexRender(Comp, { ...instanceRef.current, - ...column, + column, ...userProps, }) } @@ -361,7 +367,9 @@ export const useTable = (props, ...plugins) => { return flexRender(Comp, { ...instanceRef.current, - ...cell, + column, + row, + cell, ...userProps, }) } diff --git a/src/plugin-hooks/tests/useFilters.test.js b/src/plugin-hooks/tests/useFilters.test.js index 8bdd748..faf821a 100644 --- a/src/plugin-hooks/tests/useFilters.test.js +++ b/src/plugin-hooks/tests/useFilters.test.js @@ -39,8 +39,8 @@ const data = [ ] const defaultColumn = { - Cell: ({ value, column: { id } }) => `${id}: ${value}`, - Filter: ({ filterValue, setFilter }) => ( + Cell: ({ cell: { value }, column: { id } }) => `${id}: ${value}`, + Filter: ({ column: { filterValue, setFilter } }) => ( { diff --git a/src/plugin-hooks/tests/useGroupBy.test.js b/src/plugin-hooks/tests/useGroupBy.test.js index 127af19..1043b1e 100644 --- a/src/plugin-hooks/tests/useGroupBy.test.js +++ b/src/plugin-hooks/tests/useGroupBy.test.js @@ -40,7 +40,7 @@ const data = [ ] const defaultColumn = { - Cell: ({ value, column: { id } }) => `${id}: ${value}`, + Cell: ({ cell: { value }, column: { id } }) => `${id}: ${value}`, Filter: ({ filterValue, setFilter }) => ( `${value} Names`, + Aggregated: ({ cell: { value } }) => `${value} Names`, }, { Header: 'Last Name', accessor: 'lastName', aggregate: ['sum', 'uniqueCount'], - Aggregated: ({ value }) => `${value} Unique Names`, + Aggregated: ({ cell: { value } }) => `${value} Unique Names`, }, ], }, @@ -157,13 +157,13 @@ function App() { Header: 'Age', accessor: 'age', aggregate: 'average', - Aggregated: ({ value }) => `${value} (avg)`, + Aggregated: ({ cell: { value } }) => `${value} (avg)`, }, { Header: 'Visits', accessor: 'visits', aggregate: 'sum', - Aggregated: ({ value }) => `${value} (total)`, + Aggregated: ({ cell: { value } }) => `${value} (total)`, }, { Header: 'Status', @@ -173,7 +173,7 @@ function App() { Header: 'Profile Progress', accessor: 'progress', aggregate: roundedMedian, - Aggregated: ({ value }) => `${value} (med)`, + Aggregated: ({ cell: { value } }) => `${value} (med)`, }, ], }, diff --git a/src/plugin-hooks/tests/useSortBy.test.js b/src/plugin-hooks/tests/useSortBy.test.js index 1631748..2e4cc2d 100644 --- a/src/plugin-hooks/tests/useSortBy.test.js +++ b/src/plugin-hooks/tests/useSortBy.test.js @@ -31,7 +31,7 @@ const data = [ ] const defaultColumn = { - Cell: ({ value, column: { id } }) => `${id}: ${value}`, + Cell: ({ cell: { value }, column: { id } }) => `${id}: ${value}`, } function Table({ columns, data }) { diff --git a/src/plugin-hooks/useFilters.js b/src/plugin-hooks/useFilters.js index 8aa63f0..4325161 100755 --- a/src/plugin-hooks/useFilters.js +++ b/src/plugin-hooks/useFilters.js @@ -11,7 +11,6 @@ defaultState.filters = {} addActions('setFilter', 'setAllFilters') const propTypes = { - // General columns: PropTypes.arrayOf( PropTypes.shape({ disableFilters: PropTypes.bool, @@ -41,9 +40,15 @@ function useMain(instance) { state: [{ filters }, setState], } = instance + const preFilteredRows = rows + const setFilter = (id, updater) => { const column = columns.find(d => d.id === id) + if (!column) { + throw new Error(`React-Table: Could not find a column with id: ${id}`) + } + const filterMethod = getFilterMethod( column.filter, userFilterTypes || {}, @@ -124,74 +129,98 @@ function useMain(instance) { // cache for each row group (top-level rows, and each row's recursive subrows) // This would make multi-filtering a lot faster though. Too far? - const filteredRows = React.useMemo(() => { - if (manualFilters || !Object.keys(filters).length) { - return rows - } + const filteredRows = React.useMemo( + () => { + if (manualFilters || !Object.keys(filters).length) { + return rows + } - if (process.env.NODE_ENV === 'development' && debug) - console.info('getFilteredRows') + if (process.env.NODE_ENV === 'development' && debug) + console.info('getFilteredRows') - // Filters top level and nested rows - const filterRows = rows => { - let filteredRows = rows + // Filters top level and nested rows + const filterRows = (rows, depth = 0) => { + let filteredRows = rows - filteredRows = Object.entries(filters).reduce( - (filteredSoFar, [columnID, filterValue]) => { - // Find the filters column - const column = columns.find(d => d.id === columnID) + filteredRows = Object.entries(filters).reduce( + (filteredSoFar, [columnID, filterValue]) => { + // Find the filters column + const column = columns.find(d => d.id === columnID) - if (!column) { - return filteredSoFar - } + if (depth === 0) { + column.preFilteredRows = filteredSoFar + } - column.preFilteredRows = filteredSoFar + if (!column) { + return filteredSoFar + } - const filterMethod = getFilterMethod( - column.filter, - userFilterTypes || {}, - filterTypes - ) - - if (!filterMethod) { - console.warn( - `Could not find a valid 'column.filter' for column with the ID: ${column.id}.` + const filterMethod = getFilterMethod( + column.filter, + userFilterTypes || {}, + filterTypes ) - return filteredSoFar - } - // Pass the rows, id, filterValue and column to the filterMethod - // to get the filtered rows back - return filterMethod(filteredSoFar, columnID, filterValue, column) - }, - rows + 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: + row.subRows && row.subRows.length > 0 + ? filterRows(row.subRows, depth + 1) + : row.subRows, + } + }) + + return filteredRows + } + + const filteredRows = filterRows(rows) + + // Now that each filtered column has it's partially filtered rows, + // lets assign the final filtered rows to all of the other columns + const nonFilteredColumns = columns.filter( + column => !Object.keys(filters).includes(column.id) ) - // 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), - } + // This essentially enables faceted filter options to be built easily + // using every column's preFilteredRows value + nonFilteredColumns.forEach(column => { + column.preFilteredRows = filteredRows }) return filteredRows - } - - return filterRows(rows) - }, [manualFilters, filters, debug, rows, columns, userFilterTypes]) + }, + [manualFilters, filters, debug, rows, columns, userFilterTypes] + ) return { ...instance, setFilter, setAllFilters, - preFilteredRows: rows, + preFilteredRows, rows: filteredRows, } } diff --git a/src/utils.js b/src/utils.js index 51ab106..cb61c3d 100755 --- a/src/utils.js +++ b/src/utils.js @@ -39,7 +39,7 @@ export function decorateColumn(column, defaultColumn, parent, depth, index) { column = { Header: () => null, - Cell: ({ value = '' }) => value, + Cell: ({ cell: { value = '' } }) => value, show: true, ...column, id,