fix: fix path getters, better plugin hook integration, renaming things

This commit is contained in:
tannerlinsley 2019-08-19 16:38:42 -06:00
parent 93524d0701
commit f59efde6fe
15 changed files with 208 additions and 112 deletions

View File

@ -688,7 +688,7 @@ The following properties are available on every `Column` object returned by the
- `canGroupBy: Boolean`
- If `true`, this column is able to be grouped.
- This is resolved from the column having a valid accessor / data model, and not being manually disabled via other `useGroupBy` related options
- `grouped: Boolean`
- `isGrouped: Boolean`
- If `true`, this column is currently being grouped
- `groupedIndex: Int`
- If this column is currently being grouped, this integer is the index of this column's ID in the table state's `groupBy` array.
@ -719,17 +719,19 @@ The following properties are available on every `Row` object returned by the tab
- `path: Array<String|Int>`
- Similar to normal `Row` objects, materialized grouping rows also have a path array. The keys inside it though are not integers like nested normal rows though. Since they are not rows that can be traced back to an original data row, they are given a unique path based on their `groupByVal`
- If a row is a grouping row, it will have a path like `['Single']` or `['Complicated', 'Anderson']`, where `Single`, `Complicated`, and `Anderson` would all be derived from their row's `groupByVal`.
- `isAggregated: Bool`
- Will be `true` if the row is an aggregated row
### Cell Properties
The following additional properties are available on every `Cell` object returned in an array of `cells` on every row object.
- `grouped: Bool`
- `isGrouped: Bool`
- If `true`, this cell is a grouped cell, meaning it contains a grouping value and should usually display and expander.
- `repeatedValue: Bool`
- `isRepeatedValue: Bool`
- If `true`, this cell is a repeated value cell, meaning it contains a value that is already being displayed elsewhere (usually by a parent row's cell).
- Most of the time, this cell is not required to be displayed and can safely be hidden during rendering
- `aggregated: Bool`
- `isAggregated: Bool`
- If `true`, this cell's value has been aggregated and should probably be rendered with the `Aggregated` cell renderer.
### Example
@ -918,10 +920,6 @@ The following options are supported via the main options object passed to `useTa
- `subRowsKey: String`
- Optional
- See the [useTable hook](#table-options) for more details
- `nestExpandedRows: Bool`
- Optional
- Defaults to `false`
- If set to `false`, expanded rows will not be paginated. Thus, any expanded subrows would potentially increase the size of any given page by the amount of total expanded subrows on the page.
- `manualExpandedKey: String`
- Optional
- Defaults to `expanded`
@ -1098,13 +1096,17 @@ The following options are supported via the main options object passed to `useTa
- Defaults to `false`
- Normally, any changes detected to `rows`, `state.filters`, `state.groupBy`, or `state.sortBy` will trigger the `pageIndex` to be reset to `0`
- If set to `true`, the `pageIndex` will not be automatically set to `0` when these dependencies change.
- `paginateExpandedRows: Bool`
- Optional
- Only applies when using the `useExpanded` plugin hook simultaneously
- Defaults to `true`
- If set to `true`, expanded rows are paginated along with normal rows. This results in stable page sizes across every page.
- If set to `false`, expanded rows will be spliced in after pagination. This means that the total number of rows in a page can potentially be larger than the page size, depending on how many subrows are expanded.
### Instance Properties
The following values are provided to the table `instance`:
- `pages: Array<page>`
- An array of every generated `page`, each containing its respective rows.
- `page: Array<row>`
- An array of rows for the **current** page, determined by the current `pageIndex` value.
- `pageCount: Int`
@ -1119,7 +1121,7 @@ The following values are provided to the table `instance`:
- If there are pages and the current `pageIndex` is less than `pageCount`, this will be `true`
- `gotoPage: Function(pageIndex)`
- This function, when called with a valid `pageIndex`, will set `pageIndex` to that value.
- If the passed index is outside of the valid `pageIndex` range, then this function will do nothing.
- If the aginateassed index is outside of the valid `pageIndex` range, then this function will do nothing.
- `previousPage: Function`
- This function decreases `state.pageIndex` by one.
- If there are no pages or `canPreviousPage` is false, this function will do nothing.
@ -1335,7 +1337,7 @@ The following options are supported via the main options object passed to `useTa
- If a row's path key (eg. a row path of `[1, 3, 2]` would have a path key of `1.3.2`) is found in this array, it will have a selected state.
- `manualRowSelectedKey: String`
- Optional
- Defaults to `selected`
- Defaults to `isSelected`
- If this key is found on the **original** data row, and it is true, this row will be manually selected
### Instance Properties
@ -1359,6 +1361,16 @@ The following values are provided to the table `instance`:
- Will be `true` if all rows are selected.
- If at least one row is not selected, will be `false`
### Row Properties
The following additional properties are available on every **prepared** `row` object returned by the table instance.
- `isSelected: Bool`
- Will be `true` if the row is currently selected
- `toggleRowSelected: Function(?set)`
- Use this function to toggle this row's selected state.
- Optionally pass `true` or `false` to set it to that state
### Example
```js
@ -1609,12 +1621,9 @@ export default function MyTable({ manualPageIndex }) {
const state = useTableState(initialState, overrides)
// You can use effects to observe changes to the state
React.useEffect(
() => {
console.log('Page Size Changed!', initialState.pageSize)
},
[initialState.pageSize]
)
React.useEffect(() => {
console.log('Page Size Changed!', initialState.pageSize)
}, [initialState.pageSize])
const { rows } = useTable({
state,

View File

@ -46,20 +46,28 @@ const Styles = styled.div`
// Create an editable cell renderer
const EditableCell = ({
value: initialValue,
cell: { value: initialValue },
row: { index },
column: { id },
updateMyData, // This is a custom function that we supplied to our table instance
}) => {
// We need to keep and update the state of the cell normally
const [value, setValue] = React.useState(initialValue)
const onChange = e => {
setValue(e.target.value)
}
// We'll only update the external data when the input is blurred
const onBlur = () => {
updateMyData(index, id, value)
}
// If the initialValue is changed externall, sync it up with our state
React.useEffect(() => {
setValue(initialValue)
}, [initialValue])
return <input value={value} onChange={onChange} onBlur={onBlur} />
}
@ -221,6 +229,7 @@ function App() {
)
const [data, setData] = React.useState(() => makeData(20))
const [originalData] = React.useState(data)
// We need to keep the table from resetting the pageIndex when we
// Update data. So we can keep track of that flag with a ref.
@ -232,8 +241,8 @@ function App() {
const updateMyData = (rowIndex, columnID, value) => {
// We also turn on the flag to not reset the page
skipPageResetRef.current = true
setData(old => {
return old.filter((row, index) => {
setData(old =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex],
@ -242,22 +251,19 @@ function App() {
}
return row
})
})
)
}
// After data chagnes, we turn the flag back off
// so that if data actually changes when we're not
// editing it, the page is reset
React.useEffect(
() => {
skipPageResetRef.current = false
},
[data]
)
React.useEffect(() => {
skipPageResetRef.current = false
}, [data])
// Let's add a data resetter/randomizer to help
// illustrate that flow...
const resetData = () => setData(makeData(20))
const resetData = () => setData(originalData)
return (
<Styles>

View File

@ -68,7 +68,7 @@ function Table({ columns, data }) {
{column.canGroupBy ? (
// If the column can be grouped, let's add a toggle
<span {...column.getGroupByToggleProps()}>
{column.grouped ? '🛑 ' : '👊 '}
{column.isGrouped ? '🛑 ' : '👊 '}
</span>
) : null}
{column.render('Header')}
@ -90,16 +90,16 @@ function Table({ columns, data }) {
// from the useGroupBy hook
{...cell.getCellProps()}
style={{
background: cell.grouped
background: cell.isGrouped
? '#0aff0082'
: cell.aggregated
: cell.isAggregated
? '#ffa50078'
: cell.repeatedValue
: cell.isRepeatedValue
? '#ff000042'
: 'white',
}}
>
{cell.grouped ? (
{cell.isGrouped ? (
// If it's a grouped cell, add an expander and row count
<>
<span {...row.getExpandedToggleProps()}>
@ -107,11 +107,11 @@ function Table({ columns, data }) {
</span>{' '}
{cell.render('Cell')} ({row.subRows.length})
</>
) : cell.aggregated ? (
) : cell.isAggregated ? (
// If the cell is aggregated, use the Aggregated
// renderer for cell
cell.render('Aggregated')
) : cell.repeatedValue ? null : ( // For cells with repeated values, render null
) : cell.isRepeatedValue ? null : ( // For cells with repeated values, render null
// Otherwise, just render the regular cell
cell.render('Cell')
)}
@ -196,7 +196,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',
@ -206,7 +206,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`,
},
],
},
@ -218,14 +218,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',
@ -236,7 +236,7 @@ function App() {
accessor: 'progress',
// Use our custom roundedMedian aggregator
aggregate: roundedMedian,
Aggregated: ({ value }) => `${value} (med)`,
Aggregated: ({ cell: { value } }) => `${value} (med)`,
},
],
},

View File

@ -10,12 +10,8 @@ const range = len => {
const newPerson = () => {
const statusChance = Math.random()
let firstName = namor.generate({ words: 1, numbers: 0 })
firstName = firstName.slice(0, 1) + '.' + firstName.slice(1, firstName.length)
return {
firstName,
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),

View File

@ -37,7 +37,7 @@ function MyTable() {
+ <th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
+ <span>
+ {column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''}
+ {column.isSorted ? (column.isSortedDesc ? ' 🔽' : ' 🔼') : ''}
+ </span>
</th>
))}

View File

@ -65,7 +65,11 @@ function Table({ columns, data }) {
{column.render('Header')}
{/* Add a sort direction indicator */}
<span>
{column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</span>
</th>
))}

View File

@ -73,7 +73,7 @@ function Table({ columns, data }) {
{column.canGroupBy ? (
// If the column can be grouped, let's add a toggle
<span {...column.getGroupByToggleProps()}>
{column.grouped ? '🛑' : '👊'}
{column.isGrouped ? '🛑' : '👊'}
</span>
) : null}
{column.render('Header')}
@ -90,7 +90,7 @@ function Table({ columns, data }) {
{row.cells.map(cell => {
return (
<td {...cell.getCellProps()}>
{cell.grouped ? (
{cell.isGrouped ? (
<>
<span
style={{
@ -102,9 +102,9 @@ function Table({ columns, data }) {
</span>
{cell.render('Cell')} ({row.subRows.length})
</>
) : cell.aggregated ? (
) : cell.isAggregated ? (
cell.render('Aggregated')
) : cell.repeatedValue ? null : (
) : cell.isRepeatedValue ? null : (
cell.render('Cell')
)}
</td>

View File

@ -120,7 +120,7 @@ function App() {
{
id: 'selectedStatus',
Cell: ({ row }) => (
<div>{row.selected ? 'Selected' : 'Not Selected'}</div>
<div>{row.isSelected ? 'Selected' : 'Not Selected'}</div>
),
},
{

View File

@ -56,7 +56,7 @@ function Table({ columns, data }) {
{column.render('Header')}
{/* Add a sort direction indicator */}
<span>
{column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''}
{column.isSorted ? (column.isSortedDesc ? ' 🔽' : ' 🔼') : ''}
</span>
</th>
))}

View File

@ -17,7 +17,7 @@ addActions('toggleExpanded', 'useExpanded')
const propTypes = {
manualExpandedKey: PropTypes.string,
nestExpandedRows: PropTypes.bool,
paginateExpandedRows: PropTypes.bool,
}
export const useExpanded = hooks => {
@ -34,16 +34,16 @@ function useMain(instance) {
debug,
rows,
manualExpandedKey = 'expanded',
paginateExpandedRows = true,
hooks,
state: [{ expanded }, setState],
nestExpandedRows,
} = instance
const toggleExpandedByPath = (path, set) => {
return setState(old => {
const { expanded } = old
const existing = getBy(expanded, path)
set = getFirstDefined(set, !existing)
set = getFirstDefined(set, existing ? undefined : true)
return {
...old,
expanded: setBy(expanded, path, set),
@ -85,13 +85,16 @@ function useMain(instance) {
(row.original && row.original[manualExpandedKey]) ||
getBy(expanded, row.path)
if (!nestExpandedRows || (nestExpandedRows && row.depth === 0)) {
expandedRows.push(row)
}
expandedRows.push(row)
row.canExpand = row.subRows && !!row.subRows.length
if (row.isExpanded && row.subRows && row.subRows.length) {
if (
paginateExpandedRows &&
row.isExpanded &&
row.subRows &&
row.subRows.length
) {
row.subRows.forEach(handleRow)
}
@ -101,7 +104,7 @@ function useMain(instance) {
rows.forEach(handleRow)
return expandedRows
}, [debug, rows, manualExpandedKey, expanded, nestExpandedRows])
}, [debug, rows, manualExpandedKey, expanded, paginateExpandedRows])
const expandedDepth = findExpandedDepth(expanded)

View File

@ -9,6 +9,7 @@ import {
applyPropHooks,
defaultGroupByFn,
getFirstDefined,
ensurePluginOrder,
} from '../utils'
defaultState.groupBy = []
@ -50,9 +51,18 @@ useGroupBy.pluginName = 'useGroupBy'
function columnsBeforeHeaderGroups(columns, { state: [{ groupBy }] }) {
// Sort grouped columns to the start of the column list
// before the headers are built
const groupByColumns = groupBy.map(g => columns.find(col => col.id === g))
const nonGroupByColumns = columns.filter(col => !groupBy.includes(col.id))
// If a groupByBoundary column is found, place the groupBy's after it
const groupByBoundaryColumnIndex =
columns.findIndex(column => column.groupByBoundary) + 1
return [
...groupBy.map(g => columns.find(col => col.id === g)),
...columns.filter(col => !groupBy.includes(col.id)),
...nonGroupByColumns.slice(0, groupByBoundaryColumnIndex),
...groupByColumns,
...nonGroupByColumns.slice(groupByBoundaryColumnIndex),
]
}
@ -69,12 +79,15 @@ function useMain(instance) {
disableGrouping,
aggregations: userAggregations = {},
hooks,
plugins,
state: [{ groupBy }, setState],
} = instance
ensurePluginOrder(plugins, [], 'useGroupBy', ['useExpanded'])
columns.forEach(column => {
const { id, accessor, disableGrouping: columnDisableGrouping } = column
column.grouped = groupBy.includes(id)
column.isGrouped = groupBy.includes(id)
column.groupedIndex = groupBy.indexOf(id)
column.canGroupBy = accessor
@ -137,11 +150,12 @@ function useMain(instance) {
hooks.prepareRow.push(row => {
row.cells.forEach(cell => {
// Grouped cells are in the groupBy and the pivot cell for the row
cell.grouped = cell.column.grouped && cell.column.id === row.groupByID
cell.isGrouped = cell.column.isGrouped && cell.column.id === row.groupByID
// Repeated cells are any columns in the groupBy that are not grouped
cell.repeatedValue = !cell.grouped && cell.column.grouped
cell.isRepeatedValue = !cell.isGrouped && cell.column.isGrouped
// Aggregated cells are not grouped, not repeated, but still have subRows
cell.aggregated = !cell.grouped && !cell.repeatedValue && row.canExpand
cell.isAggregated =
!cell.isGrouped && !cell.isRepeatedValue && row.canExpand
})
return row
})
@ -218,7 +232,7 @@ function useMain(instance) {
// Recurse to sub rows before aggregation
groupedRows = Object.entries(groupedRows).map(
([groupByVal, subRows], index) => {
const path = [...parentPath, groupByVal]
const path = [...parentPath, `${columnID}:${groupByVal}`]
subRows = groupRecursively(subRows, depth + 1, path)
@ -228,6 +242,7 @@ function useMain(instance) {
)
const row = {
isAggregated: true,
groupByID: columnID,
groupByVal,
values,

View File

@ -14,6 +14,7 @@ addActions('pageChange', 'pageSizeChange')
const propTypes = {
// General
manualPagination: PropTypes.bool,
paginateExpandedRows: PropTypes.bool,
}
// SSR has issues with useLayoutEffect still, so use useEffect during SSR
@ -32,28 +33,30 @@ function useMain(instance) {
PropTypes.checkPropTypes(propTypes, instance, 'property', 'usePagination')
const {
data,
rows,
manualPagination,
disablePageResetOnDataChange,
debug,
plugins,
pageCount: userPageCount,
paginateExpandedRows = true,
state: [{ pageSize, pageIndex, filters, groupBy, sortBy }, setState],
} = instance
ensurePluginOrder(
plugins,
['useFilters', 'useGroupBy', 'useSortBy'],
['useFilters', 'useGroupBy', 'useSortBy', 'useExpanded'],
'usePagination',
[]
)
const rowDep = manualPagination || disablePageResetOnDataChange ? null : rows
const rowDep = manualPagination ? null : data
const isPageIndexMountedRef = React.useRef()
useLayoutEffect(() => {
if (isPageIndexMountedRef.current) {
if (isPageIndexMountedRef.current && !disablePageResetOnDataChange) {
setState(
old => ({
...old,
@ -63,38 +66,51 @@ function useMain(instance) {
)
}
isPageIndexMountedRef.current = true
}, [setState, rowDep, filters, groupBy, sortBy])
}, [setState, rowDep, filters, groupBy, sortBy, disablePageResetOnDataChange])
const pages = React.useMemo(() => {
if (manualPagination) {
return undefined
}
if (process.env.NODE_ENV === 'development' && debug)
console.info('getPages')
// Create a new pages with the first page ready to go.
const pages = rows.length ? [] : [[]]
// Start the pageIndex and currentPage cursors
let cursor = 0
while (cursor < rows.length) {
const end = cursor + pageSize
pages.push(rows.slice(cursor, end))
cursor = end
}
return pages
}, [debug, manualPagination, pageSize, rows])
const pageCount = manualPagination ? userPageCount : pages.length
const pageCount = manualPagination
? userPageCount
: Math.ceil(rows.length / pageSize)
const pageOptions = React.useMemo(
() => (pageCount > 0 ? [...new Array(pageCount)].map((d, i) => i) : []),
[pageCount]
)
const page = manualPagination ? rows : pages[pageIndex]
const page = React.useMemo(() => {
let page
if (manualPagination) {
page = rows
} else {
if (process.env.NODE_ENV === 'development' && debug)
console.info('getPage')
const pageStart = pageSize * pageIndex
const pageEnd = pageStart + pageSize
page = rows.slice(pageStart, pageEnd)
}
if (paginateExpandedRows) {
return page
}
const expandedPage = []
const handleRow = row => {
expandedPage.push(row)
if (row.subRows && row.subRows.length && row.isExpanded) {
row.subRows.forEach(handleRow)
}
}
page.forEach(handleRow)
return expandedPage
}, [debug, manualPagination, pageIndex, pageSize, paginateExpandedRows, rows])
const canPreviousPage = pageIndex > 0
const canNextPage = pageCount === -1 || pageIndex < pageCount - 1
@ -134,7 +150,6 @@ function useMain(instance) {
return {
...instance,
pages,
pageOptions,
pageCount,
page,

View File

@ -25,7 +25,7 @@ function useMain(instance) {
const {
hooks,
manualRowSelectedKey = 'selected',
manualRowSelectedKey = 'isSelected',
plugins,
rowPaths,
state: [{ selectedRows }, setState],
@ -59,7 +59,7 @@ function useMain(instance) {
// in a flat object
const exists = old.selectedRows.includes(key)
const shouldExist = typeof set !== 'undefined' ? set : !exists
let newSelectedRows = new Set(selectedRows)
let newSelectedRows = new Set(old.selectedRows)
if (!exists && shouldExist) {
newSelectedRows.add(key)
@ -94,10 +94,49 @@ function useMain(instance) {
}
hooks.prepareRow.push(row => {
row.canSelect = !!row.original
// Aggregate rows have entirely different select logic
if (row.isAggregated) {
const subRowPaths = row.subRows.map(row => row.path)
row.isSelected = subRowPaths.every(path =>
selectedRows.includes(path.join('.'))
)
row.toggleRowSelected = set => {
set = typeof set !== 'undefined' ? set : !row.isSelected
console.log(subRowPaths)
subRowPaths.forEach(path => {
toggleRowSelected(path, set)
})
}
row.getToggleRowSelectedProps = props => {
let checked = false
if (row.canSelect) {
row.selected = selectedRows.includes(row.path.join('.'))
if (row.original && row.original[manualRowSelectedKey]) {
checked = true
} else {
checked = row.isSelected
}
return mergeProps(
{
onChange: e => {
row.toggleRowSelected(e.target.checked)
},
style: {
cursor: 'pointer',
},
checked,
title: 'Toggle Row Selected',
},
applyPropHooks(
instance.hooks.getToggleRowSelectedProps,
row,
instance
),
props
)
}
} else {
row.isSelected = selectedRows.includes(row.path.join('.'))
row.toggleRowSelected = set => toggleRowSelected(row.path, set)
row.getToggleRowSelectedProps = props => {
let checked = false
@ -105,7 +144,7 @@ function useMain(instance) {
if (row.original && row.original[manualRowSelectedKey]) {
checked = true
} else {
checked = selectedRows.includes(row.path.join('.'))
checked = row.isSelected
}
return mergeProps(

View File

@ -64,7 +64,7 @@ function useMain(instance) {
plugins,
} = instance
ensurePluginOrder(plugins, [], 'useSortBy', ['useFilters'])
ensurePluginOrder(plugins, ['useFilters'], 'useSortBy', [])
// Add custom hooks
hooks.getSortByToggleProps = []
@ -197,9 +197,10 @@ function useMain(instance) {
)
}
column.sorted = sortBy.find(d => d.id === id)
const columnSort = sortBy.find(d => d.id === id)
column.isSorted = !!columnSort
column.sortedIndex = sortBy.findIndex(d => d.id === id)
column.sortedDesc = column.sorted ? column.sorted.desc : undefined
column.isSortedDesc = column.isSorted ? columnSort.desc : undefined
})
const sortedRows = React.useMemo(() => {

View File

@ -196,6 +196,7 @@ export function defaultGroupByFn(rows, columnID) {
}
export function setBy(obj = {}, path, value) {
path = makePathArray(path)
const recurse = (obj, depth = 0) => {
const key = path[depth]
const target = typeof obj[key] !== 'object' ? {} : obj[key]
@ -347,11 +348,18 @@ This usually means you need to need to name your plugin hook by setting the 'plu
//
function makePathArray(obj) {
return flattenDeep(obj)
.join('.')
.replace(/\[/g, '.')
.replace(/\]/g, '')
.split('.')
return (
flattenDeep(obj)
// remove all periods in parts
.map(d => String(d).replace('.', '_'))
// join parts using period
.join('.')
// replace brackets with periods
.replace(/\[/g, '.')
.replace(/\]/g, '')
// split it back out on periods
.split('.')
)
}
function flattenDeep(arr, newArr = []) {