import utils from "hoctable/utils"; import * as React from "react"; import * as ReactDOM from "react-dom"; export interface PaginationState { current : number; total : number; size : number; } export type UpdateCallback = () => void; export type DataLoadedCallback = (result : DataResult) => void; export interface PaginationProps { move : (new_page : number, callback : UpdateCallback) => void; pagination : PaginationState; } export interface ButtonProps { label : string; handler : () => void; } export interface ColumnDefinition { rel : string; name : string; sortable? : boolean; style? : any; classes? : Array; } export interface ColumnFlags { active : boolean; } export interface ColumnProps { sort : (column : ColumnDefinition, callback : UpdateCallback) => void; column : ColumnDefinition; flags : ColumnFlags; } export interface DataResult { rows : Array; total : number; } export interface TableDelegate { rows : (pagination : PaginationState, sorting : ColumnDefinition, callback : DataLoadedCallback) => void; columns : () => Array; } export interface TableProps { delegate : TableDelegate; pagination : PaginationState | null; sorting : ColumnDefinition | null; } export type RowTransclusion = React.ComponentClass; export type ColumnTransclusion = React.ComponentClass; export type ComposedTable = React.ComponentClass; export type ComposedPager = React.ComponentClass; export const CLASSES = { PAGINATION : "hoctable__pagination", PAGINATION_EMPTY : "hoctable__pagination--empty", PAGINATION_CONTAINER : "hoctable__table-pagination", PAGINATION_CONTROLS : "hoctable__pagination-controls", PAGINATION_NEXT : "hoctable__pagination-next", PAGINATION_PREVIOUS : "hoctable__pagination-previous", PAGINATION_TOTAL : "hoctable__pagination-total", PAGINATION_INFO : "hoctable__pagination-info", TABLE : "hoctable__table", TABLE_TBODY : "hoctable__table-tbody", TABLE_HEAD : "hoctable__table-head", TABLE_CONTAINER : "hoctable__table-container", TABLE_HEADER_CELL : "hoctable__table-header-cell", TABLE_HEADER_CELL_ACTIVE : "hoctable__table-header-cell--active", TABLE_HEADER_CELL_SORTABLE : "hoctable__table-header-cell--sortable" }; // This component is used by the table to handle updating it's pagination references and subsequently re-rendering. export class Pagination extends React.Component { constructor(props) { super(props); } render() : React.ReactElement { const { move, pagination } = this.props; const { total, size, current } = pagination; const update = this.forceUpdate.bind(this); // Do nothing but render out an empty div if we have no pagination information if(total >= 1 === false) { return (
); } const first = current * size; const end = first + size; function go(amt : number) : void { move(current + amt, update); } function previous() : void { return go(-1); } function next() : void { return go(1); } const buttons = []; // If we're no longer on the first page, render our back button if(current >= 1) { buttons.push(previous); } /* If we're both able to identify the last index we "would" render, and that index is less than the total amount of * items that our delegate told us it has, render our next button. */ if(end && end < total) { buttons.push(next); } // Calculate the max page if the delegate had a non-zero positive total and we're rendering more than one item. const max_page = total >= 1 && size >= 1 ? Math.ceil(total / size) : 0; return (

page {current + 1} of {max_page} ({total} results)

{buttons}
); } } /* In order to allow users to _optionally_ provide a column header transclusion, the table delegates the construction * of a column header component to this factory. */ function ColumnFactory(Transclusion? : ColumnTransclusion) : ColumnTransclusion { class ColumnHeader extends React.Component { constructor(props) { super(props); } sort() : void { const { sort, column } = this.props; const update = this.forceUpdate.bind(this); if(column.sortable !== true) { return; } sort(column, update); } render() : React.ReactElement { const { column, flags } = this.props; const { name, sortable, classes: user_classes } = column; const class_list = (user_classes || []).concat(CLASSES["TABLE_HEADER_CELL"]); // If this column's sort relationship matched that of the table, add our table header active class to the ele if(flags && flags.active === true) { class_list.push(CLASSES["TABLE_HEADER_CELL_ACTIVE"]); } if(sortable === true) { class_list.push(CLASSES["TABLE_HEADER_CELL_SORTABLE"]); } let content =
{name}
; // If there was user-provided transclusion, replace the content with a new instance of that transclusion. if(Transclusion) { content = ; } return {content}; } } return ColumnHeader; } /* This factory defines a high order table component that accepts transclusions for both it's row component and it's * table header cell component. The table's delegate is relied upon to provide the column definition objects as well * as the data that will ultimately be used to render instances of the transcluded row component. */ function TableFactory(Row : RowTransclusion, Column? : ColumnTransclusion) : ComposedTable { // Create the header cell by transcluding the user-provided component to our column factory const HeaderCell = ColumnFactory(Column); class PagedTable extends React.Component { private bodies : Array; private render_request : string; constructor(props : TableProps) { super(props); /* Prepare an array to hold of the html nodes that we create during our renders so that we can delete them and * unmount components later. */ this.bodies = [ ]; // If initial sorting + pagination information was provided, update to align w/ that information this.state = { sorting : props.sorting || null, pagination : props.pagination || { size: 10, current: 0, total: 0 } }; } componentWillUnmount() : void { this.render_request = null; } /* During the rendering of the table, it will attempt to assign this function to the table element's 'ref' react * property. Once called, this function will attempt to load in the delegate's row data via it's `rows` function * and subsequently iterate over each elment, rendering it into place. */ transclude(table : HTMLElement) : void { const { props, bodies, refs, state } = this; const { sorting, pagination } = state; const { delegate } = props; if(!table) { return; } const pagination_props = { pagination, move: this.move.bind(this) }; const current_request = this.render_request = utils.uuid(); const render = (data : DataResult) : void => { const { rows, total } = data; const { render_request } = this; // Prevent rendering if not latest. if(render_request !== current_request) { return; } // Remove the previous pagination component. const pager = refs["pager"] as HTMLElement; ReactDOM.unmountComponentAtNode(pager); // Cleanup previous tbody elements. while(bodies.length) { const [next] = bodies.splice(0, 1); ReactDOM.unmountComponentAtNode(next); utils.dom.remove(next); } /* Iterate over the row data, creating a container for them and rendering a new instance of the `Row` * transclusion into it; preserving the strict html table hierachy rules. */ for(let i = 0, c = rows.length; i < c; i++) { const row = rows[i]; const body = utils.dom.create("tbody", null, [CLASSES["TABLE_TBODY"]]); ReactDOM.render(, body); table.appendChild(body); bodies.push(body); } // Update our pagination's reference to the total amount of data (as returned by the delegate) pagination.total = total; // Render the pagination component ReactDOM.render(, pager); }; // Start the data source loading delegate.rows(pagination, sorting, render); } move(new_page : number, done : UpdateCallback) : void { const { state } = this; const pagination = { ...state.pagination, current: new_page }; this.setState({ pagination }); done(); } sort(column : ColumnDefinition, done : UpdateCallback) : void { const { rel } = column; let { sorting } = this.state; let { direction } = sorting || { direction: false }; if(sorting && sorting.rel === rel) { direction = !direction; } sorting = { rel, direction }; this.setState({ sorting }); done(); } render() : React.ReactElement { const { sorting } = this.state; const { delegate } = this.props; const head = { cols: [ ], cells: [ ] }; /* Ask the delegate for it's columns and iterate over each, creating a table header cell and a colgroup * element in order to keep the body's column width equal to that of the header's. */ for(let i = 0, columns = delegate.columns(), c = columns.length; i < c; i++) { const column = columns[i]; const active = sorting && column.rel === sorting.rel; const flags = { active }; // Create and insert the column (which may itself have a transclusion). head.cells.push(); // Create a element for the colgroup with the style (if any) specified on the column object. head.cols.push(); } return (
{head.cols}{head.cells}
); } } return PagedTable; } export default TableFactory;