Created
May 15, 2018 14:08
-
-
Save mlienau/a27efe6f2008d6db33195cb7adcc0144 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from "react"; | |
export interface Column<TItem> { | |
title?: React.ReactNode; | |
tdProps?: React.HTMLProps<HTMLTableCellElement>; | |
thProps?: React.HTMLProps<HTMLTableHeaderCellElement>; | |
renderer?: (item: TItem, index?: number, array?: TItem[]) => React.ReactNode | JSX.Element; | |
key: keyof (TItem); | |
/** | |
* @param: The lower case representation of the searchable value | |
*/ | |
searchableValue?: (i: TItem) => string; | |
sortableValue?: (i: TItem) => number | string; | |
disableSort?: boolean; | |
align?: "left" | "center" | "right"; | |
} | |
export interface TablesorterProps<TItem> { | |
title?: string; | |
pageSize?: number; | |
numberOfPages?: number; | |
tableProps?: React.HTMLProps<HTMLTableElement>; | |
items: TItem[] | false; | |
columns: Column<TItem>[]; | |
searchable?: boolean; | |
sortable?: boolean; | |
initialSortColumn?: keyof TItem; | |
initialSortAsc?: boolean; | |
onRowClick?: (i: TItem) => void; | |
filterPlaceholder?: string; | |
} | |
interface TablesorterState<TItem> { | |
currentPage?: number; | |
searchValue?: string; | |
sortColumn?: keyof TItem; | |
sortAsc?: boolean; | |
} | |
interface PageItemProps extends React.Props<PageItemProps> { | |
pageIndex: number; | |
isActive?: boolean; | |
isDisabled?: boolean; | |
onClick: (pageIndex: number) => void; | |
} | |
function PageItem(props: PageItemProps) { | |
const onClick = (e: React.MouseEvent<HTMLAnchorElement>) => { | |
e.preventDefault(); | |
if (props.isDisabled) { | |
return; | |
} | |
props.onClick(props.pageIndex); | |
}; | |
return ( | |
<a href="#" onClick={onClick} | |
className={props.isActive ? "active" : props.isDisabled ? "disabled" : ""}> | |
{props.children} | |
</a> | |
); | |
} | |
export default class Tablesorter<TItem> extends React.Component<TablesorterProps<TItem>, TablesorterState<TItem>> { | |
static defaultProps = { | |
pageSize: 25, | |
numberOfPages: 10, | |
searchable: false, | |
sortable: false | |
}; | |
constructor(props: TablesorterProps<TItem>) { | |
super(props); | |
if (props.searchable && props.columns.every(c => c.searchableValue === undefined)) { | |
throw new Error("TableSorter is defined as searchable, but no columns implement searchableValue"); | |
} | |
this.handlePageChange = this.handlePageChange.bind(this); | |
this.state = { | |
currentPage: 0, | |
sortAsc: typeof props.initialSortAsc === "boolean" ? props.initialSortAsc : true, | |
sortColumn: props.initialSortColumn, | |
searchValue: "" | |
}; | |
} | |
componentWillReceiveProps(nextProps: TablesorterProps<TItem>) { | |
const newState: TablesorterState<TItem> = {}; | |
if (this.props.items !== nextProps.items) { | |
newState.currentPage = 0; | |
// this.setState({ currentPage: 0 }); | |
} | |
const { sortColumn } = this.state; | |
const { columns: newColumns } = nextProps; | |
if (nextProps.sortable && !newColumns.some(c => c.key === sortColumn)) { | |
newState.sortColumn = nextProps.initialSortColumn || newColumns[0].key; | |
} | |
if (Object.keys(newState).length > 0) { | |
this.setState(newState); | |
} | |
} | |
handlePageChange(pageIndex: number) { | |
this.setState({ currentPage: pageIndex }); | |
} | |
handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |
this.setState({ | |
searchValue: e.target.value, | |
currentPage: 0 | |
}); | |
} | |
handleSortChange(sortColumn: keyof TItem, e: React.MouseEvent<HTMLAnchorElement>) { | |
const { sortColumn: currentSortColumn } = this.state; | |
if (currentSortColumn === sortColumn) { | |
this.setState({ sortAsc: !this.state.sortAsc }); | |
return; | |
} | |
this.setState({ sortColumn, sortAsc: true }); | |
} | |
filter = (item: TItem, index: number, array: TItem[]) => { | |
const { searchValue } = this.state; | |
if (!searchValue) { | |
return true; | |
} | |
return this.props.columns.filter(c => c.searchableValue) | |
.some(c => c.searchableValue(item).indexOf(searchValue.toLowerCase()) >= 0); | |
} | |
render() { | |
const { items } = this.props; | |
const columns = this.props.columns.filter(c => c); | |
if (!columns || columns.length === 0) { | |
return (<p>No data</p>); | |
} | |
if (!items || items.length === 0) { | |
const placeholder = !items ? "Loading..." : <span>No Results</span>; | |
return ( | |
<table className="table"> | |
<thead> | |
<tr> | |
{columns.map((c, index) => { | |
const { align } = c; | |
const { className, ...rest } = (c.thProps || { className: "" }); | |
return ( | |
<th key={c.key} className={`${className} ${align ? `text-${align}` : ""}`} {...rest}> | |
{c.title} | |
</th> | |
); | |
})} | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td colSpan={columns.length} style={{ textAlign: "center", padding: "20px" }}> | |
{placeholder} | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
); | |
} | |
const { pageSize, numberOfPages, onRowClick, tableProps, searchable, sortable, filterPlaceholder } = this.props; | |
const { currentPage, searchValue, sortAsc, sortColumn } = this.state; | |
const sort = (a: TItem, b: TItem) => { | |
if (!sortColumn) { | |
return 1; | |
} | |
const valueGetter = columns.find(c => c.key === sortColumn).sortableValue; | |
let aValue = valueGetter ? valueGetter(a) : a[sortColumn]; | |
let bValue = valueGetter ? valueGetter(b) : b[sortColumn]; | |
if (typeof aValue === "number" && typeof bValue === "number") { | |
if (sortAsc) { | |
return aValue - bValue; | |
} | |
return bValue - aValue; | |
} | |
if (valueGetter) { | |
if (sortAsc) { | |
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; | |
} | |
return bValue < aValue ? -1 : bValue > aValue ? 1 : 0; | |
} | |
aValue = (aValue || "").toString().toLowerCase(); | |
bValue = (bValue || "").toString().toLowerCase(); | |
if (sortAsc) { | |
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; | |
} | |
return bValue < aValue ? -1 : bValue > aValue ? 1 : 0; | |
}; | |
const start = currentPage * pageSize; | |
const filteredItems = items.filter(this.filter); | |
if (sortable && sortColumn) { | |
filteredItems.sort(sort); | |
} | |
const numPages = Math.ceil(filteredItems.length / pageSize); | |
const pagedItems = filteredItems.slice(start, start + pageSize); | |
const pageStart = numberOfPages * Math.floor(currentPage / numberOfPages); | |
const pageEnd = pageStart + pageSize; | |
return ( | |
<table className="table" {...tableProps}> | |
<thead> | |
{searchable && ( | |
<tr> | |
<th className="text-right" colSpan={columns.length} style={{ | |
border: "none", | |
padding: 0, | |
paddingBottom: "2rem" | |
}}> | |
<div className="material-form" style={{ display: "flex", justifyContent: "flex-end" }}> | |
<div className="input-group" style={{ maxWidth: 300 }}> | |
<div className="input-group-addon pl-0 align-items-center"> | |
<i className="fa fa-search" /> | |
</div> | |
<input type="search" | |
value={searchValue} | |
onChange={this.handleFilterChange} | |
className="form-control" | |
placeholder={filterPlaceholder || "Filter Results"} /> | |
</div> | |
</div> | |
</th> | |
</tr> | |
)} | |
<tr> | |
{columns.map((c, index) => { | |
const { align } = c; | |
const { className, ...rest } = (c.thProps || { className: "" }); | |
return ( | |
<th key={c.key} className={`${className} ${align ? `text-${align}` : ""}`} {...rest}> | |
{sortable && !c.disableSort ? <a onClick={this.handleSortChange.bind(this, c.key)}> | |
{c.title} | |
{c.key === sortColumn && ( | |
<i className={`fa ml-2 fa-angle-${sortAsc ? "up" : "down"}`} style={{ color: "#5b6770", verticalAlign: "baseline" }}></i> | |
)} | |
</a> : c.title} | |
</th> | |
); | |
})} | |
</tr> | |
</thead> | |
<tbody> | |
{pagedItems.map((item, index) => ( | |
<tr key={index} onClick={onRowClick ? () => { onRowClick(item); } : undefined}> | |
{columns.map(c => { | |
const { align } = c; | |
const { className, ...rest } = (c.tdProps || { className: "" }); | |
return ( | |
<td key={c.key} className={`${className} ${align ? `text-${align}` : ""}`} {...rest}> | |
{c.renderer ? c.renderer(item, index + start) : (item[c.key] === null ? "" : item[c.key]).toString()} | |
</td> | |
); | |
})} | |
</tr> | |
))} | |
</tbody> | |
{(numPages > 1 || (searchable && items.length > pageSize)) && ( | |
<tfoot> | |
<tr> | |
<td colSpan={columns.length} className="text-center" style={{ verticalAlign: "middle" }}> | |
<div className="paginator"> | |
<PageItem pageIndex={0} | |
isDisabled={currentPage === 0} | |
onClick={this.handlePageChange}> | |
<i className="fa fa-angle-double-left" /> | |
</PageItem> | |
<PageItem pageIndex={Math.max(currentPage - 1, 0)} | |
isDisabled={currentPage === 0} | |
onClick={this.handlePageChange}> | |
<i className="fa fa-angle-left" /> | |
</PageItem> | |
{Array.from({ length: pageEnd > numPages ? numPages - pageStart : Math.min(numberOfPages, numPages) }).map((p, pageIndex) => { | |
const actualPage = pageIndex + pageStart; | |
return ( | |
<PageItem key={actualPage} | |
isActive={currentPage === actualPage} | |
pageIndex={actualPage} | |
onClick={this.handlePageChange}> | |
{actualPage + 1} | |
</PageItem> | |
); | |
})} | |
<PageItem pageIndex={currentPage + 1} | |
isDisabled={(currentPage + 1) === numPages} | |
onClick={this.handlePageChange}> | |
<i className="fa fa-angle-right" /> | |
</PageItem> | |
</div> | |
</td> | |
</tr> | |
</tfoot> | |
)} | |
</table> | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment