Skip to content

Instantly share code, notes, and snippets.

@mlienau
Created May 15, 2018 14:08
Show Gist options
  • Save mlienau/a27efe6f2008d6db33195cb7adcc0144 to your computer and use it in GitHub Desktop.
Save mlienau/a27efe6f2008d6db33195cb7adcc0144 to your computer and use it in GitHub Desktop.
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