mirror of
https://github.com/gosticks/react-table.git
synced 2026-08-12 13:00:26 +00:00
Add column filtering (#147)
* Add column filtering. * Fix javascript warning from yarn test. Compile storybook and docs. * Pass standard linting * Add support for filtering pivot columns. * Build distribution files.
This commit is contained in:
committed by
Tanner Linsley
parent
bd8b273dea
commit
23a031b48a
+13
-1
@@ -21,6 +21,12 @@ export default {
|
||||
collapseOnDataChange: true,
|
||||
freezeWhenExpanded: false,
|
||||
defaultSorting: [],
|
||||
showFilters: false,
|
||||
defaultFiltering: [],
|
||||
defaultFilterMethod: (filter, row, column) => {
|
||||
const id = filter.pivotId || filter.id
|
||||
return row[id] !== undefined ? String(row[id]).startsWith(filter.value) : true
|
||||
},
|
||||
|
||||
// Controlled State Overrides
|
||||
// page: undefined,
|
||||
@@ -32,6 +38,7 @@ export default {
|
||||
onPageChange: undefined,
|
||||
onPageSizeChange: undefined,
|
||||
onSortingChange: undefined,
|
||||
onFilteringChange: undefined,
|
||||
|
||||
// Pivoting
|
||||
pivotBy: undefined,
|
||||
@@ -62,6 +69,9 @@ export default {
|
||||
getTheadProps: emptyObj,
|
||||
getTheadTrProps: emptyObj,
|
||||
getTheadThProps: emptyObj,
|
||||
getTheadFilterProps: emptyObj,
|
||||
getTheadFilterTrProps: emptyObj,
|
||||
getTheadFilterThProps: emptyObj,
|
||||
getTbodyProps: emptyObj,
|
||||
getTrGroupProps: emptyObj,
|
||||
getTrProps: emptyObj,
|
||||
@@ -92,7 +102,9 @@ export default {
|
||||
footer: undefined,
|
||||
footerClassName: '',
|
||||
footerStyle: {},
|
||||
getFooterProps: emptyObj
|
||||
getFooterProps: emptyObj,
|
||||
filterMethod: undefined,
|
||||
hideFilter: false
|
||||
},
|
||||
|
||||
// Text
|
||||
|
||||
+154
-25
@@ -26,6 +26,9 @@ export default React.createClass({
|
||||
getTheadProps,
|
||||
getTheadTrProps,
|
||||
getTheadThProps,
|
||||
getTheadFilterProps,
|
||||
getTheadFilterTrProps,
|
||||
getTheadFilterThProps,
|
||||
getTbodyProps,
|
||||
getTrGroupProps,
|
||||
getTrProps,
|
||||
@@ -41,11 +44,13 @@ export default React.createClass({
|
||||
manual,
|
||||
loadingText,
|
||||
noDataText,
|
||||
showFilters,
|
||||
// State
|
||||
loading,
|
||||
pageSize,
|
||||
page,
|
||||
sorting,
|
||||
filtering,
|
||||
pages,
|
||||
// Pivoting State
|
||||
pivotValKey,
|
||||
@@ -82,7 +87,7 @@ export default React.createClass({
|
||||
const minRows = this.getMinRows()
|
||||
const padRows = pages > 1 ? _.range(pageSize - pageRows.length)
|
||||
: minRows ? _.range(Math.max(minRows - pageRows.length, 0))
|
||||
: []
|
||||
: []
|
||||
|
||||
const hasColumnFooter = allVisibleColumns.some(d => d.footer)
|
||||
|
||||
@@ -360,6 +365,144 @@ export default React.createClass({
|
||||
)
|
||||
}
|
||||
|
||||
const makeFilters = () => {
|
||||
const theadFilterProps = _.splitProps(getTheadFilterProps(finalState, undefined, undefined, this))
|
||||
const theadFilterTrProps = _.splitProps(getTheadFilterTrProps(finalState, undefined, undefined, this))
|
||||
return (
|
||||
<TheadComponent
|
||||
className={classnames('-filters', theadFilterProps.className)}
|
||||
style={{
|
||||
...theadFilterProps.style,
|
||||
minWidth: `${rowMinWidth}px`
|
||||
}}
|
||||
{...theadFilterProps.rest}
|
||||
>
|
||||
<TrComponent
|
||||
className={theadFilterTrProps.className}
|
||||
style={theadFilterTrProps.style}
|
||||
{...theadFilterTrProps.rest}
|
||||
>
|
||||
{allVisibleColumns.map(makeFilter)}
|
||||
</TrComponent>
|
||||
</TheadComponent>
|
||||
)
|
||||
}
|
||||
|
||||
const makeFilter = (column, i) => {
|
||||
const width = _.getFirstDefined(column.width, column.minWidth)
|
||||
const maxWidth = _.getFirstDefined(column.width, column.maxWidth)
|
||||
const theadFilterThProps = _.splitProps(getTheadFilterThProps(finalState, undefined, column, this))
|
||||
const columnHeaderProps = _.splitProps(column.getHeaderProps(finalState, undefined, column, this))
|
||||
|
||||
const classes = [
|
||||
column.headerClassName,
|
||||
theadFilterThProps.className,
|
||||
columnHeaderProps.className
|
||||
]
|
||||
|
||||
const styles = {
|
||||
...column.headerStyle,
|
||||
...theadFilterThProps.style,
|
||||
...columnHeaderProps.style
|
||||
}
|
||||
|
||||
const rest = {
|
||||
...theadFilterThProps.rest,
|
||||
...columnHeaderProps.rest
|
||||
}
|
||||
|
||||
if (column.expander) {
|
||||
if (column.pivotColumns) {
|
||||
const pivotCols = []
|
||||
for (let i = 0; i < column.pivotColumns.length; i++) {
|
||||
const col = column.pivotColumns[i]
|
||||
const filter = filtering.find(filter => filter.id === column.id && filter.pivotId === col.id)
|
||||
pivotCols.push(
|
||||
<span key={col.id}
|
||||
style={{display: 'flex', alignContent: 'flex-end', flex: 1}}>
|
||||
{!col.hideFilter ? (
|
||||
<input type='text'
|
||||
style={{
|
||||
flex: 1,
|
||||
width: 20
|
||||
}}
|
||||
value={filter ? filter.value : ''}
|
||||
onChange={(event) => this.filterColumn(column, event, col)}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
if (i < column.pivotColumns.length - 1) {
|
||||
pivotCols.push(<ExpanderComponent key={col.id + '-' + i} />)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ThComponent
|
||||
key={i}
|
||||
className={classnames(
|
||||
'rt-pivot-header',
|
||||
column.sortable && '-cursor-pointer',
|
||||
classes
|
||||
)}
|
||||
style={{
|
||||
...styles,
|
||||
flex: `${width} 0 auto`,
|
||||
width: `${width}px`,
|
||||
maxWidth: `${maxWidth}px`,
|
||||
display: 'flex'
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{pivotCols}
|
||||
</ThComponent>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ThComponent
|
||||
key={i}
|
||||
className={classnames(
|
||||
'rt-expander-header',
|
||||
classes
|
||||
)}
|
||||
style={{
|
||||
...styles,
|
||||
flex: `0 0 auto`,
|
||||
width: `${expanderColumnWidth}px`
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const filter = filtering.find(filter => filter.id === column.id)
|
||||
|
||||
return (
|
||||
<ThComponent
|
||||
key={i}
|
||||
className={classnames(
|
||||
classes
|
||||
)}
|
||||
style={{
|
||||
...styles,
|
||||
flex: `${width} 0 auto`,
|
||||
width: `${width}px`,
|
||||
maxWidth: `${maxWidth}px`
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{!column.hideFilter ? (
|
||||
<input type='text'
|
||||
style={{
|
||||
width: `100%`
|
||||
}}
|
||||
value={filter ? filter.value : ''}
|
||||
onChange={(event) => this.filterColumn(column, event)}
|
||||
/>
|
||||
) : null}
|
||||
</ThComponent>
|
||||
)
|
||||
}
|
||||
|
||||
const makePageRow = (row, i, path = []) => {
|
||||
const rowInfo = {
|
||||
row: row.__original,
|
||||
@@ -452,15 +595,15 @@ export default React.createClass({
|
||||
{...rowInfo}
|
||||
value={rowInfo.rowValues[pivotValKey]}
|
||||
/>
|
||||
) : <span>{row[pivotValKey]} ({rowInfo.subRows.length})</span>}
|
||||
) : <span>{row[pivotValKey]} ({rowInfo.subRows.length})</span>}
|
||||
</span>
|
||||
) : SubComponent ? (
|
||||
<span>
|
||||
<ExpanderComponent
|
||||
isExpanded={isExpanded}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
) : SubComponent ? (
|
||||
<span>
|
||||
<ExpanderComponent
|
||||
isExpanded={isExpanded}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</TdComponent>
|
||||
)
|
||||
}
|
||||
@@ -526,7 +669,6 @@ export default React.createClass({
|
||||
const makePadRow = (row, i) => {
|
||||
const trGroupProps = getTrGroupProps(finalState, undefined, undefined, this)
|
||||
const trProps = _.splitProps(getTrProps(finalState, undefined, undefined, this))
|
||||
const tdProps = _.splitProps(getTdProps(finalState, undefined, undefined, this))
|
||||
return (
|
||||
<TrGroupComponent
|
||||
key={i}
|
||||
@@ -539,20 +681,6 @@ export default React.createClass({
|
||||
)}
|
||||
style={trProps.style || {}}
|
||||
>
|
||||
{SubComponent && (
|
||||
<ThComponent
|
||||
className={classnames(
|
||||
'rt-expander-header',
|
||||
tdProps.className
|
||||
)}
|
||||
style={{
|
||||
...tdProps.style,
|
||||
flex: `0 0 auto`,
|
||||
width: `${expanderColumnWidth}px`
|
||||
}}
|
||||
{...tdProps.rest}
|
||||
/>
|
||||
)}
|
||||
{allVisibleColumns.map((column, i2) => {
|
||||
const show = typeof column.show === 'function' ? column.show() : column.show
|
||||
const width = _.getFirstDefined(column.width, column.minWidth)
|
||||
@@ -735,6 +863,7 @@ export default React.createClass({
|
||||
>
|
||||
{hasHeaderGroups ? makeHeaderGroups() : null}
|
||||
{makeHeaders()}
|
||||
{showFilters ? makeFilters() : null}
|
||||
<TbodyComponent
|
||||
className={classnames(tBodyProps.className)}
|
||||
style={{
|
||||
@@ -760,7 +889,7 @@ export default React.createClass({
|
||||
style={paginationProps.style}
|
||||
{...paginationProps.rest}
|
||||
/>
|
||||
) : null}
|
||||
) : null}
|
||||
{!pageRows.length && (
|
||||
<NoDataComponent
|
||||
{...noDataProps}
|
||||
|
||||
@@ -22,6 +22,12 @@ $expandSize = 7px
|
||||
background: alpha(black, .03)
|
||||
border-bottom: 1px solid alpha(black, .05)
|
||||
|
||||
&.-filters
|
||||
border-bottom: 1px solid alpha(black, 0.05)
|
||||
|
||||
.rt-th
|
||||
border-right: 1px solid alpha(black, 0.02)
|
||||
|
||||
&.-header
|
||||
box-shadow: 0 2px 15px 0px alpha(black, .15)
|
||||
|
||||
|
||||
+17
-5
@@ -11,7 +11,8 @@ export default {
|
||||
page: 0,
|
||||
pageSize: this.props.defaultPageSize || 10,
|
||||
sorting: this.props.defaultSorting,
|
||||
expandedRows: {}
|
||||
expandedRows: {},
|
||||
filtering: this.props.defaultFiltering
|
||||
}
|
||||
},
|
||||
|
||||
@@ -41,12 +42,19 @@ export default {
|
||||
newState.sorting = newState.defaultSorting
|
||||
}
|
||||
|
||||
if ((oldState.showFilters !== newState.showFilters) ||
|
||||
(oldState.showFilters !== newState.showFilters)) {
|
||||
newState.filtering = newState.defaultFiltering
|
||||
}
|
||||
|
||||
// Props that trigger a data update
|
||||
if (
|
||||
oldState.data !== newState.data ||
|
||||
oldState.columns !== newState.columns ||
|
||||
oldState.pivotBy !== newState.pivotBy ||
|
||||
oldState.sorting !== newState.sorting
|
||||
oldState.sorting !== newState.sorting ||
|
||||
oldState.showFilters !== newState.showFilters ||
|
||||
oldState.filtering !== newState.filtering
|
||||
) {
|
||||
this.setStateWithData(this.getDataModel(newState))
|
||||
}
|
||||
@@ -55,7 +63,7 @@ export default {
|
||||
setStateWithData (newState, cb) {
|
||||
const oldState = this.getResolvedState()
|
||||
const newResolvedState = this.getResolvedState({}, newState)
|
||||
const { freezeWhenExpanded } = newResolvedState
|
||||
const {freezeWhenExpanded} = newResolvedState
|
||||
|
||||
// Default to unfrozen state
|
||||
newResolvedState.frozen = false
|
||||
@@ -77,11 +85,15 @@ export default {
|
||||
if (
|
||||
(oldState.frozen && !newResolvedState.frozen) ||
|
||||
oldState.sorting !== newResolvedState.sorting ||
|
||||
oldState.filtering !== newResolvedState.filtering ||
|
||||
oldState.showFilters !== newResolvedState.showFilters ||
|
||||
(!newResolvedState.frozen && oldState.resolvedData !== newResolvedState.resolvedData)
|
||||
) {
|
||||
// Handle collapseOnSortingChange & collapseOnDataChange
|
||||
if (
|
||||
(oldState.sorting !== newResolvedState.sorting && this.props.collapseOnSortingChange) ||
|
||||
(oldState.filtering !== newResolvedState.filtering) ||
|
||||
(oldState.showFilters !== newResolvedState.showFilters) ||
|
||||
(!newResolvedState.frozen && oldState.resolvedData !== newResolvedState.resolvedData && this.props.collapseOnDataChange)
|
||||
) {
|
||||
newResolvedState.expandedRows = {}
|
||||
@@ -91,8 +103,8 @@ export default {
|
||||
}
|
||||
|
||||
// Calculate pageSize all the time
|
||||
if (newResolvedState.resolvedData) {
|
||||
newResolvedState.pages = newResolvedState.manual ? newResolvedState.pages : Math.ceil(newResolvedState.resolvedData.length / newResolvedState.pageSize)
|
||||
if (newResolvedState.sortedData) {
|
||||
newResolvedState.pages = newResolvedState.manual ? newResolvedState.pages : Math.ceil(newResolvedState.sortedData.length / newResolvedState.pageSize)
|
||||
}
|
||||
|
||||
return this.setState(newResolvedState, cb)
|
||||
|
||||
+104
-20
@@ -192,15 +192,14 @@ export default {
|
||||
// Group the rows together for this level
|
||||
let groupedRows = Object.entries(
|
||||
_.groupBy(rows, keys[i]))
|
||||
.map(([key, value]) => {
|
||||
return {
|
||||
[pivotIDKey]: keys[i],
|
||||
[pivotValKey]: key,
|
||||
[keys[i]]: key,
|
||||
[subRowsKey]: value
|
||||
}
|
||||
.map(([key, value]) => {
|
||||
return {
|
||||
[pivotIDKey]: keys[i],
|
||||
[pivotValKey]: key,
|
||||
[keys[i]]: key,
|
||||
[subRowsKey]: value
|
||||
}
|
||||
)
|
||||
})
|
||||
// Recurse into the subRows
|
||||
groupedRows = groupedRows.map(rowGroup => {
|
||||
let subRows = groupRecursively(rowGroup[subRowsKey], keys, i + 1)
|
||||
@@ -233,15 +232,18 @@ export default {
|
||||
const {
|
||||
manual,
|
||||
sorting,
|
||||
resolvedData
|
||||
filtering,
|
||||
showFilters,
|
||||
defaultFilterMethod,
|
||||
resolvedData,
|
||||
allVisibleColumns
|
||||
} = resolvedState
|
||||
|
||||
// Resolve the data from either manual data or sorted data
|
||||
return {
|
||||
sortedData: manual ? resolvedData : this.sortData(resolvedData, sorting)
|
||||
sortedData: manual ? resolvedData : this.sortData(this.filterData(resolvedData, showFilters, filtering, defaultFilterMethod, allVisibleColumns), sorting)
|
||||
}
|
||||
},
|
||||
|
||||
fireOnChange () {
|
||||
this.props.onChange(this.getResolvedState(), this)
|
||||
},
|
||||
@@ -251,10 +253,56 @@ export default {
|
||||
getStateOrProp (key) {
|
||||
return _.getFirstDefined(this.state[key], this.props[key])
|
||||
},
|
||||
filterData (data, showFilters, filtering, defaultFilterMethod, allVisibleColumns) {
|
||||
let filteredData = data
|
||||
|
||||
if (showFilters && filtering.length) {
|
||||
filteredData = filtering.reduce(
|
||||
(filteredSoFar, nextFilter) => {
|
||||
return filteredSoFar.filter(
|
||||
(row) => {
|
||||
let column
|
||||
|
||||
if (nextFilter.pivotId) {
|
||||
const parentColumn = allVisibleColumns.find(x => x.id === nextFilter.id)
|
||||
column = parentColumn.pivotColumns.find(x => x.id === nextFilter.pivotId)
|
||||
} else {
|
||||
column = allVisibleColumns.find(x => x.id === nextFilter.id)
|
||||
}
|
||||
|
||||
const filterMethod = column.filterMethod || defaultFilterMethod
|
||||
|
||||
return filterMethod(nextFilter, row, column)
|
||||
})
|
||||
}
|
||||
, filteredData
|
||||
)
|
||||
|
||||
// Apply the filter to the subrows if we are pivoting, and then
|
||||
// filter any rows without subcolumns because it would be strange to show
|
||||
filteredData = filteredData.map(row => {
|
||||
if (!row[this.props.subRowsKey]) {
|
||||
return row
|
||||
}
|
||||
return {
|
||||
...row,
|
||||
[this.props.subRowsKey]: this.filterData(row[this.props.subRowsKey], showFilters, filtering, defaultFilterMethod, allVisibleColumns)
|
||||
}
|
||||
}).filter(row => {
|
||||
if (!row[this.props.subRowsKey]) {
|
||||
return true
|
||||
}
|
||||
return row[this.props.subRowsKey].length > 0
|
||||
})
|
||||
}
|
||||
|
||||
return filteredData
|
||||
},
|
||||
sortData (data, sorting) {
|
||||
if (!sorting.length) {
|
||||
return data
|
||||
}
|
||||
|
||||
const sorted = _.orderBy(data, sorting.map(sort => {
|
||||
return row => {
|
||||
if (row[sort.id] === null || row[sort.id] === undefined) {
|
||||
@@ -281,23 +329,23 @@ export default {
|
||||
|
||||
// User actions
|
||||
onPageChange (page) {
|
||||
const { onPageChange, collapseOnPageChange } = this.props
|
||||
const {onPageChange, collapseOnPageChange} = this.props
|
||||
if (onPageChange) {
|
||||
return onPageChange(page)
|
||||
}
|
||||
const newState = { page }
|
||||
const newState = {page}
|
||||
if (collapseOnPageChange) {
|
||||
newState.expandedRows = {}
|
||||
}
|
||||
this.setStateWithData(
|
||||
newState
|
||||
, () => {
|
||||
this.fireOnChange()
|
||||
})
|
||||
, () => {
|
||||
this.fireOnChange()
|
||||
})
|
||||
},
|
||||
onPageSizeChange (newPageSize) {
|
||||
const { onPageSizeChange } = this.props
|
||||
const { pageSize, page } = this.getResolvedState()
|
||||
const {onPageSizeChange} = this.props
|
||||
const {pageSize, page} = this.getResolvedState()
|
||||
|
||||
// Normalize the page to display
|
||||
const currentRow = pageSize * page
|
||||
@@ -315,8 +363,8 @@ export default {
|
||||
})
|
||||
},
|
||||
sortColumn (column, additive) {
|
||||
const { sorting } = this.getResolvedState()
|
||||
const { onSortingChange } = this.props
|
||||
const {sorting} = this.getResolvedState()
|
||||
const {onSortingChange} = this.props
|
||||
if (onSortingChange) {
|
||||
return onSortingChange(column, additive)
|
||||
}
|
||||
@@ -398,5 +446,41 @@ export default {
|
||||
}, () => {
|
||||
this.fireOnChange()
|
||||
})
|
||||
},
|
||||
filterColumn (column, event, pivotColumn) {
|
||||
const {filtering} = this.getResolvedState()
|
||||
const {onFilteringChange} = this.props
|
||||
|
||||
if (onFilteringChange) {
|
||||
return onFilteringChange(column, event)
|
||||
}
|
||||
|
||||
// Remove old filter first if it exists
|
||||
const newFiltering = (filtering || []).filter(x => {
|
||||
if (x.id !== column.id) {
|
||||
return true
|
||||
}
|
||||
if (x.pivotId) {
|
||||
if (pivotColumn) {
|
||||
return x.pivotId !== pivotColumn.id
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
if (event.target.value !== '') {
|
||||
newFiltering.push({
|
||||
id: column.id,
|
||||
value: event.target.value,
|
||||
pivotId: pivotColumn ? pivotColumn.id : undefined
|
||||
})
|
||||
}
|
||||
|
||||
this.setStateWithData({
|
||||
page: 0,
|
||||
filtering: newFiltering
|
||||
}, () => {
|
||||
this.fireOnChange()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user