refactor: improve renderer function ergonomics

The renderer function for headers, columns, cells, aggregates, filters, etc used to mix properties
from all of those contexts, including rows. Now thow contexts are located on their own reserved
properties, eg. `Cell: ({ cell: { value}, row, column, ...instance }) => value`

BREAKING CHANGE: The renderer function for headers, columns, cells, aggregates, filters, etc used
This commit is contained in:
tannerlinsley 2019-08-15 14:16:52 -06:00
parent d50ec588bc
commit d9a4b6bd85
8 changed files with 212 additions and 152 deletions

View File

@ -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<row>`
- `preFilteredColumnRows: Array<row>`
- 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)`,
},
],
},

View File

@ -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 (
<input
value={filterValue || ''}
onChange={e => {
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 (
<div
style={{
@ -134,7 +155,7 @@ function NumberRangeColumnFilter({ filterValue = [], setFilter }) {
const val = e.target.value
setFilter((old = []) => [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',

View File

@ -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,
})
}

View File

@ -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 } }) => (
<input
value={filterValue || ''}
onChange={e => {

View File

@ -40,7 +40,7 @@ const data = [
]
const defaultColumn = {
Cell: ({ value, column: { id } }) => `${id}: ${value}`,
Cell: ({ cell: { value }, column: { id } }) => `${id}: ${value}`,
Filter: ({ filterValue, setFilter }) => (
<input
value={filterValue || ''}
@ -140,13 +140,13 @@ function App() {
Header: 'First Name',
accessor: 'firstName',
aggregate: ['sum', 'count'],
Aggregated: ({ value }) => `${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)`,
},
],
},

View File

@ -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 }) {

View File

@ -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,
}
}

View File

@ -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,