import { functionalUpdate, memo, RequiredKeys } from '../utils' import { Updater, TableOptionsResolved, TableState, Table, InitialTableState, Row, Column, RowModel, ColumnDef, TableOptions, RowData, TableMeta, ColumnDefResolved, GroupColumnDef, } from '../types' // import { createColumn } from './column' import { Headers } from './headers' // import { ColumnSizing } from '../features/ColumnSizing' import { Expanding } from '../features/Expanding' import { Filters } from '../features/Filters' import { Grouping } from '../features/Grouping' import { Ordering } from '../features/Ordering' import { Pagination } from '../features/Pagination' import { Pinning } from '../features/Pinning' import { RowSelection } from '../features/RowSelection' import { Sorting } from '../features/Sorting' import { Visibility } from '../features/Visibility' export interface TableFeature { getDefaultOptions?: (table: any) => any getInitialState?: (initialState?: InitialTableState) => any createTable?: (table: any) => any getDefaultColumnDef?: () => any createColumn?: (column: any, table: any) => any createHeader?: (column: any, table: any) => any createCell?: (cell: any, column: any, row: any, table: any) => any createRow?: (row: any, table: any) => any } const features = [ Headers, Visibility, Ordering, Pinning, Filters, Sorting, Grouping, Expanding, Pagination, RowSelection, ColumnSizing, ] as const // export interface CoreTableState {} export interface CoreOptions { data: TData[] state: Partial onStateChange: (updater: Updater) => void debugAll?: boolean debugTable?: boolean debugHeaders?: boolean debugColumns?: boolean debugRows?: boolean initialState?: InitialTableState autoResetAll?: boolean mergeOptions?: ( defaultOptions: TableOptions, options: Partial> ) => TableOptions meta?: TableMeta getCoreRowModel: (table: Table) => () => RowModel getSubRows?: (originalRow: TData, index: number) => undefined | TData[] getRowId?: (originalRow: TData, index: number, parent?: Row) => string columns: ColumnDef[] defaultColumn?: Partial> renderFallbackValue: any } export interface CoreInstance { initialState: TableState reset: () => void options: RequiredKeys, 'state'> setOptions: (newOptions: Updater>) => void getState: () => TableState setState: (updater: Updater) => void _features: readonly TableFeature[] _queue: (cb: () => void) => void _getRowId: (_: TData, index: number, parent?: Row) => string getCoreRowModel: () => RowModel _getCoreRowModel?: () => RowModel getRowModel: () => RowModel getRow: (id: string) => Row _getDefaultColumnDef: () => Partial> _getColumnDefs: () => ColumnDef[] _getAllFlatColumnsById: () => Record> getAllColumns: () => Column[] getAllFlatColumns: () => Column[] getAllLeafColumns: () => Column[] getColumn: (columnId: string) => Column | undefined } export function createTable( options: TableOptionsResolved ): Table { if (options.debugAll || options.debugTable) { console.info('Creating Table Instance...') } let table = { _features: features } as unknown as Table const defaultOptions = table._features.reduce((obj, feature) => { return Object.assign(obj, feature.getDefaultOptions?.(table)) }, {}) as TableOptionsResolved const mergeOptions = (options: TableOptionsResolved) => { if (table.options.mergeOptions) { return table.options.mergeOptions(defaultOptions, options) } return { ...defaultOptions, ...options, } } const coreInitialState: CoreTableState = {} let initialState = { ...coreInitialState, ...(options.initialState ?? {}), } as TableState table._features.forEach(feature => { initialState = feature.getInitialState?.(initialState) ?? initialState }) const queued: (() => void)[] = [] let queuedTimeout = false const coreInstance: CoreInstance = { _features: features, options: { ...defaultOptions, ...options, }, initialState, _queue: cb => { queued.push(cb) if (!queuedTimeout) { queuedTimeout = true // Schedule a microtask to run the queued callbacks after // the current call stack (render, etc) has finished. Promise.resolve() .then(() => { while (queued.length) { queued.shift()!() } queuedTimeout = false }) .catch(error => setTimeout(() => { throw error }) ) } }, reset: () => { table.setState(table.initialState) }, setOptions: updater => { const newOptions = functionalUpdate(updater, table.options) table.options = mergeOptions(newOptions) as RequiredKeys< TableOptionsResolved, 'state' > }, getState: () => { return table.options.state as TableState }, setState: (updater: Updater) => { table.options.onStateChange?.(updater) }, _getRowId: (row: TData, index: number, parent?: Row) => table.options.getRowId?.(row, index, parent) ?? `${parent ? [parent.id, index].join('.') : index}`, getCoreRowModel: () => { if (!table._getCoreRowModel) { table._getCoreRowModel = table.options.getCoreRowModel(table) } return table._getCoreRowModel!() }, // The final calls start at the bottom of the model, // expanded rows, which then work their way up getRowModel: () => { return table.getPaginationRowModel() }, getRow: (id: string) => { const row = table.getRowModel().rowsById[id] if (!row) { if (process.env.NODE_ENV !== 'production') { throw new Error(`getRow expected an ID, but got ${id}`) } throw new Error() } return row }, _getDefaultColumnDef: memo( () => [table.options.defaultColumn], defaultColumn => { defaultColumn = (defaultColumn ?? {}) as Partial< ColumnDef > return { header: props => { const resolvedColumnDef = props.header.column .columnDef as ColumnDefResolved if (resolvedColumnDef.accessorKey) { return resolvedColumnDef.accessorKey } if (resolvedColumnDef.accessorFn) { return resolvedColumnDef.id } return null }, // footer: props => props.header.column.id, cell: props => props.renderValue()?.toString?.() ?? null, ...table._features.reduce((obj, feature) => { return Object.assign(obj, feature.getDefaultColumnDef?.()) }, {}), ...defaultColumn, } as Partial> }, { debug: () => table.options.debugAll ?? table.options.debugColumns, key: process.env.NODE_ENV === 'development' && 'getDefaultColumnDef', } ), _getColumnDefs: () => table.options.columns, getAllColumns: memo( () => [table._getColumnDefs()], columnDefs => { const recurseColumns = ( columnDefs: ColumnDef[], parent?: Column, depth = 0 ): Column[] => { return columnDefs.map(columnDef => { const column = createColumn(table, columnDef, depth, parent) const groupingColumnDef = columnDef as GroupColumnDef< TData, unknown > column.columns = groupingColumnDef.columns ? recurseColumns(groupingColumnDef.columns, column, depth + 1) : [] return column }) } return recurseColumns(columnDefs) }, { key: process.env.NODE_ENV === 'development' && 'getAllColumns', debug: () => table.options.debugAll ?? table.options.debugColumns, } ), getAllFlatColumns: memo( () => [table.getAllColumns()], allColumns => { return allColumns.flatMap(column => { return column.getFlatColumns() }) }, { key: process.env.NODE_ENV === 'development' && 'getAllFlatColumns', debug: () => table.options.debugAll ?? table.options.debugColumns, } ), _getAllFlatColumnsById: memo( () => [table.getAllFlatColumns()], flatColumns => { return flatColumns.reduce((acc, column) => { acc[column.id] = column return acc }, {} as Record>) }, { key: process.env.NODE_ENV === 'development' && 'getAllFlatColumnsById', debug: () => table.options.debugAll ?? table.options.debugColumns, } ), getAllLeafColumns: memo( () => [table.getAllColumns(), table._getOrderColumnsFn()], (allColumns, orderColumns) => { let leafColumns = allColumns.flatMap(column => column.getLeafColumns()) return orderColumns(leafColumns) }, { key: process.env.NODE_ENV === 'development' && 'getAllLeafColumns', debug: () => table.options.debugAll ?? table.options.debugColumns, } ), getColumn: columnId => { const column = table._getAllFlatColumnsById()[columnId] if (process.env.NODE_ENV !== 'production' && !column) { console.error(`[Table] Column with id '${columnId}' does not exist.`) } return column }, } Object.assign(table, coreInstance) table._features.forEach(feature => { return Object.assign(table, feature.createTable?.(table)) }) return table }