Last active
January 18, 2018 12:36
-
-
Save sonhanguyen/d34f2171947bd45d59cfaa736b71c6e8 to your computer and use it in GitHub Desktop.
typed grid
This file contains 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' | |
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} | |
/> |
This file contains 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' | |
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 |
Author
sonhanguyen
commented
Jan 18, 2018
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment