Skip to content

Instantly share code, notes, and snippets.

@sonhanguyen
Last active January 18, 2018 12:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sonhanguyen/d34f2171947bd45d59cfaa736b71c6e8 to your computer and use it in GitHub Desktop.
Save sonhanguyen/d34f2171947bd45d59cfaa736b71c6e8 to your computer and use it in GitHub Desktop.
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
@sonhanguyen
Copy link
Author

sonhanguyen commented Dec 31, 2017

type WithOptionalMapper<Props> =
  Props &
  {
    mapProps?: (prop: Props) => Props
  }

/**
 * like recompose's mapProps but in an ad-hoc way
 * works effectively like declaring both a static and a custom factory for every prop 
 * @param {React.Component}
 * @return {React.Component} with optional "mapProps" props that take the original props as input
 * and return a new set of props which is what the input component actually receives
 */
export function withDependentProps<Props>(
  Component: ComponentType<Props>
): ComponentType<WithOptionalMapper<Props>> {
  return (props: WithOptionalMapper<Props>) => {
    // @ts-ignore
    const { mapProps } = props
    if (mapProps) props = mapProps(props)
    return <Component { ...props } />
  }
}

@sonhanguyen
Copy link
Author

enum Modes { Readonly, AllowDuplicate, Editable }

/**
 * Turn an enum into composable bit flags
 * @param {Object} Enum: (mutable)
 * @param {string} all: key for all flags, must be existing on Enum, optional
 * @param {string} none: key to turn all flags off, default to first
 */
+function makeFlags(Enum: {}, all?: string, none: any = 0) {
  let flagCount = 0
  for (const key in Enum) {
    const idx = Enum[key]
    if (idx !== none && typeof idx == 'number') {
      Enum[key] = key == none ? 0 : 1 << idx
      flagCount++
    }
  }
  if (all) Enum[all] = ~(~0 << flagCount)
}(Modes, 'Editable')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment