typed grid
import * as React from 'react'
import Grid from './Grid'
import './App.css'
interface BaseLineItem {
id: string
name: string
cost: number
}
interface LineItemPackage extends BaseLineItem {
type: 'package'
lineItems: BaseLineItem[]
}
const DataGrid: React.ComponentType<Grid.Props<BaseLineItem>> = Grid
function Column(props: Grid.Column.Props) {
return props.children
}
export default props =>
<DataGrid
data={[
{ id: 'a', name: 'AAAAAA', cost: 3 },
{ id: 'b', name: 'BBBBBB', cost: 4 },
{ id: 'b', name: 'BBBBBB', type: 'package',
lineItems: [
{ id: 'a', name: 'AAAAAA', cost: 3 },
{ id: 'b', name: 'BBBBBB', cost: 4 }
],
get cost() {
return 2
}
} as LineItemPackage
]}
columnOrdering={['id', 'cost', 'name']}
rowComponent={(props: Grid.Row.Props<LineItemPackage>) =>
props.record.type === 'package'
? <>
<td colSpan={100}>Package {props.record.name} {props.record.cost}</td>
{props.record.lineItems.map(lineItem => {
const Cell: Grid.Cell = (props) => {
const { type: Col, props: colProps } = props.children
return <Grid.Cell {...props}>
<Col {...colProps}>{lineItem[props.columnName]}</Col>
</Grid.Cell>
}
return <Grid.Row {...props} record={lineItem} cellComponent={Cell}/>
})}
</>
: <Grid.Row {...props} />
}
columns={{
id: {
header: 'ID',
component: Column
},
name: {
header: 'Name',
component: Column
},
cost: {
header: 'Cost',
component: Column
}
}}
onClick={() => false}
/>
import * as React from 'react'
import { ReactElement, ComponentType, ReactNode } from 'react'
// "context" in the sense that these props get passed down from the grid to the cell, not via the actual react context
type RowContext<Record=any> = {
isSelected?: boolean
index: number
id: number | string
record: Record
}
type ColumnContext = {
columnName: string
}
module Grid {
export type Props<Record, Keys extends string = keyof Record> =
Pick<Row.Props<Record>, 'onClick'> &
{
rowComponent?: Row // main extension point
headerComponent?: Header
columns: {
[colName in Keys]:
{
header: string
component: Column
}
}
keyBy?: string
data: Record[]
columnOrdering: Array<Keys>
}
export type Component<Record> = ComponentType<Grid.Props<Record>>
export namespace Column {
export type Props<ValueType=any> =
RowContext &
ColumnContext &
{
children: ValueType
}
}
export type Column<ValueType=any> = ComponentType<Column.Props<ValueType>>
export namespace Cell {
export type Props<ValueType=any> =
RowContext &
ColumnContext &
{
columnComponent?: Column<ValueType>
children: ReactElement<Props>
}
}
export type Cell<ValueType=any> = ComponentType<Cell.Props<ValueType>>
export namespace Row {
export type Props<Record=any, Children=Array<ReactElement<Cell.Props>>> =
RowContext<Record> &
{
onClick(cell: any, col: string, row: Record): boolean | void
cellComponent?: Cell
isSelectable?: boolean
children: Children
}
}
export type Row<
Col0=any, Col1=any, Col2=any, Col3=any, Col4=any, Col5=any, Col6=any
> = ComponentType<
Row.Props &
{
children: Row.Props['children'] & {
0?: ReactElement<Cell.Props<Col0>>
1?: ReactElement<Cell.Props<Col1>>
2?: ReactElement<Cell.Props<Col2>>
3?: ReactElement<Cell.Props<Col3>>
4?: ReactElement<Cell.Props<Col4>>
5?: ReactElement<Cell.Props<Col5>>
6?: ReactElement<Cell.Props<Col6>>
}
}
>
export namespace Header {
export type Props = {
children: ColumnContext['columnName']
}
}
export type Header = ComponentType<Header.Props>
export namespace Table {
export type Props<Row=ReactElement<Row.Props>> = {
headerElements: ReactNode
children: Array<Row>
}
}
}
class Row extends React.Component<Grid.Row.Props> {
static defaultProps = {
cellComponent(props: Grid.Cell.Props) {
const Column = props.columnComponent as Grid.Column
props = { ...props }
props.columnComponent = undefined
return <td><Column {...props} /></td>
}
}
render() {
const Cell = this.props.cellComponent as Grid.Cell
return <>
{this.props.children.map((cell: ReactElement<Grid.Cell.Props>) =>
<Cell {...cell.props} columnComponent={cell.type as Grid.Cell} />
)}
</>
}
}
class Grid<Record> extends React.Component<Grid.Props<Record>> {
// default components to reuse when override
static Cell: Grid.Cell = Row.defaultProps.cellComponent
static Row: Grid.Row = Row
static Header(props) {
return props.children
}
static Table(props: Grid.Table.Props) {
const { map } = React.Children
return <table>
<tr>{map(props.headerElements, col => <th>{col}</th>)}</tr>
<tbody>{map(props.children, row => <tr>{row}</tr>)}</tbody>
</table>
}
static defaultProps = {
rowComponent: Grid.Row,
headerComponent: Grid.Header,
keyBy: 'id',
onClick() {}
}
private renderRow = (record: any, index: number) => {
const Row = this.props.rowComponent as Grid.Row
const id = '' + record[this.props.keyBy as any]
const rowContext = { index, id, record }
const props: Partial<Grid.Row.Props> = {
...rowContext,
children: this.props.columnOrdering.map((col: string) => {
const {header, component: Column} = this.props.columns[col]
return <Column {...rowContext} columnName={header} key={header}>
{record[col] as any}
</Column>
})
}
return <Row key={id} {...props as Grid.Row.Props} />
}
render() {
const Header = this.props.headerComponent as Grid.Header
const props = {
headerElements: this.props.columnOrdering.map((col: string) =>
<th><Header children={this.props.columns[col].header} /></th>
),
onClick: this.props.onClick,
children: this.props.data.map(this.renderRow)
}
return <Grid.Table {...props} />
}
}
export default Grid