Created
July 20, 2025 09:40
-
-
Save yarinsa/f49c50329175f0f469d996558cc1e619 to your computer and use it in GitHub Desktop.
react-table builder
This file contains hidden or 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
// table-builder.ts - Functional Programming with Fluent Interface | |
import { type ColumnDef } from "@tanstack/react-table" | |
import { | |
createDragDropPlugin, | |
createSelectionPlugin, | |
createFilteringPlugin, | |
createGroupingPlugin, | |
addDragHandleColumn, | |
addSelectionColumn, | |
enableColumnGrouping, | |
type TablePlugin | |
} from "./table-plugins" | |
interface TableConfig<TData> { | |
columns: ColumnDef<TData>[] | |
plugins: TablePlugin<TData>[] | |
} | |
// This is where the magic happens - pure transformation functions | |
// Each function takes a config and returns a new config with added functionality | |
type TableTransform<TData> = (config: TableConfig<TData>) => TableConfig<TData> | |
// Selection plugin transformer - adds checkbox column + bulk actions | |
const withSelection = <TData>(config: { | |
enableBulkActions?: boolean | |
bulkActions?: React.ComponentType<{ selectedRows: TData[]; table: any }> | |
} = {}): TableTransform<TData> => | |
({ columns, plugins }) => ({ | |
columns: addSelectionColumn(columns), // Inject checkbox column | |
plugins: [...plugins, createSelectionPlugin(config)] // Add selection behavior | |
}) | |
// Drag & drop transformer - adds grip handle + reorder functionality | |
const withDragDrop = <TData>(config: { | |
onReorder?: (data: TData[]) => void | |
getRowId?: (row: TData) => string | number | |
}): TableTransform<TData> => | |
({ columns, plugins }) => ({ | |
columns: addDragHandleColumn(columns), // Inject drag handle column | |
plugins: [...plugins, createDragDropPlugin(config)] // Add drag behavior | |
}) | |
// Filtering transformer - no column changes, just adds search/filter behavior | |
const withFiltering = <TData>(config: { | |
globalSearch?: boolean | |
globalSearchPlaceholder?: string | |
columnFilters?: Record<string, any> | |
} = {}): TableTransform<TData> => | |
({ columns, plugins }) => ({ | |
columns, // No column changes needed | |
plugins: [...plugins, createFilteringPlugin(config)] | |
}) | |
// Grouping transformer - marks columns as groupable + adds grouping behavior | |
const withGrouping = <TData>(config: { | |
groupableColumns: string[] | |
groupingLabels?: Record<string, string> | |
onGroupClick?: (groupValue: string, groupData: TData[]) => void | |
}): TableTransform<TData> => | |
({ columns, plugins }) => ({ | |
columns: enableColumnGrouping(columns, config.groupableColumns), // Mark columns as groupable | |
plugins: [...plugins, createGroupingPlugin(config)] | |
}) | |
// Classic functional composition - this is what makes it all work together | |
const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T => | |
fns.reduce((acc, fn) => fn(acc), value) | |
// The fluent interface that developers actually use | |
// Internally collects pure transformations, applies them all at once in build() | |
class TableBuilder<TData> { | |
private transforms: TableTransform<TData>[] = [] | |
constructor(private initialColumns: ColumnDef<TData>[]) {} | |
// Each method just collects the transformation, doesn't execute immediately | |
addSelection(config: Parameters<typeof withSelection<TData>>[0] = {}) { | |
this.transforms.push(withSelection(config)) | |
return this // Fluent interface | |
} | |
addDragDrop(config: Parameters<typeof withDragDrop<TData>>[0]) { | |
this.transforms.push(withDragDrop(config)) | |
return this | |
} | |
addFiltering(config: Parameters<typeof withFiltering<TData>>[0] = {}) { | |
this.transforms.push(withFiltering(config)) | |
return this | |
} | |
addGrouping(config: Parameters<typeof withGrouping<TData>>[0]) { | |
this.transforms.push(withGrouping(config)) | |
return this | |
} | |
// This is where all transformations get applied via functional composition | |
build(): TableConfig<TData> { | |
return pipe(...this.transforms)({ | |
columns: this.initialColumns, | |
plugins: [] | |
}) | |
} | |
} | |
// Simple factory function - keeps the interface clean | |
export const createTableBuilder = <TData>(columns: ColumnDef<TData>[]) => | |
new TableBuilder(columns) | |
// Usage Examples - How I actually use this in production: | |
// The API that finally makes sense - clean, readable, and composable | |
const { columns, plugins } = createTableBuilder(baseColumns) | |
.addSelection({ enableBulkActions: true }) | |
.addDragDrop({ onReorder: setData }) | |
.addFiltering({ globalSearch: true }) | |
.addGrouping({ groupableColumns: ["role"] }) | |
.build() | |
// Conditional features work beautifully - no more prop drilling hell | |
const builder = createTableBuilder(baseColumns) | |
.addSelection({ enableBulkActions: true }) | |
.addFiltering({ globalSearch: true }) | |
// Only add drag & drop if user has permissions | |
if (userCanReorder) { | |
builder.addDragDrop({ onReorder: setData }) | |
} | |
const { columns, plugins } = builder.build() | |
// Drop it into the table component - that's it! | |
// <DataTable columns={columns} data={data} plugins={plugins} /> | |
// Pro tip: You can even create reusable builder configurations | |
const createAdminTable = (baseColumns) => | |
createTableBuilder(baseColumns) | |
.addSelection({ enableBulkActions: true }) | |
.addDragDrop({ onReorder: handleReorder }) | |
.addFiltering({ globalSearch: true }) | |
const createReadOnlyTable = (baseColumns) => | |
createTableBuilder(baseColumns) | |
.addFiltering({ globalSearch: true }) | |
.addGrouping({ groupableColumns: ["status", "category"] }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment