@tanstack/solid-table
Version:
Headless UI for building powerful tables & datagrids for Solid.
324 lines (322 loc) • 9.87 kB
JavaScript
import { FlexRender } from "./FlexRender.js";
import { createTable } from "./createTable.js";
import { createColumnHelper } from "@tanstack/table-core";
import { createContext, mergeProps, useContext } from "solid-js";
import { createComponent as createComponent$1 } from "solid-js/web";
//#region src/createTableHook.tsx
/**
* Creates a custom table hook with pre-bound components for composition.
*
* This is the table equivalent of TanStack Form's `createFormHook`. It allows you to:
* - Define features, row models, and default options once, shared across all tables
* - Register reusable table, cell, and header components
* - Access table/cell/header instances via context in those components
* - Get a `createAppTable` hook that returns an extended table with App wrapper components
* - Get a `createAppColumnHelper` function pre-bound to your features
*
* @example
* ```tsx
* // hooks/table.ts
* export const {
* createAppTable,
* createAppColumnHelper,
* useTableContext,
* useCellContext,
* useHeaderContext,
* } = createTableHook({
* features: tableFeatures({
* rowPaginationFeature,
* rowSortingFeature,
* columnFilteringFeature,
* paginatedRowModel: createPaginatedRowModel(),
* sortedRowModel: createSortedRowModel(),
* filteredRowModel: createFilteredRowModel(),
* sortFns,
* filterFns,
* }),
* tableComponents: { PaginationControls, RowCount },
* cellComponents: { TextCell, NumberCell },
* headerComponents: { SortIndicator, ColumnFilter },
* })
*
* // Create column helper with TFeatures already bound
* const columnHelper = createAppColumnHelper<Person>()
*
* // components/table-components.tsx
* function PaginationControls() {
* const table = useTableContext() // TFeatures already known!
* return (
* <table.Subscribe>
* {(atoms) => <span>Page {atoms.pagination.get().pageIndex + 1}</span>}
* </table.Subscribe>
* )
* }
*
* // features/users.tsx
* function UsersTable({ data }: { data: Person[] }) {
* const table = createAppTable({
* columns,
* data, // TData inferred from Person[]
* })
*
* return (
* <table.AppTable>
* <table>
* <thead>
* <For each={table.getHeaderGroups()}>
* {(headerGroup) => (
* <tr>
* <For each={headerGroup.headers}>
* {(h) => (
* <table.AppHeader header={h}>
* {(header) => (
* <th>
* <header.FlexRender />
* <header.SortIndicator />
* </th>
* )}
* </table.AppHeader>
* )}
* </For>
* </tr>
* )}
* </For>
* </thead>
* <tbody>
* <For each={table.getRowModel().rows}>
* {(row) => (
* <tr>
* <For each={row.getAllCells()}>
* {(c) => (
* <table.AppCell cell={c}>
* {(cell) => <td><cell.TextCell /></td>}
* </table.AppCell>
* )}
* </For>
* </tr>
* )}
* </For>
* </tbody>
* </table>
* <table.PaginationControls />
* </table.AppTable>
* )
* }
* ```
*/
function createTableHook({ tableComponents, cellComponents, headerComponents, ...defaultTableOptions }) {
const TableContext = createContext(null);
const CellContext = createContext(null);
const HeaderContext = createContext(null);
/**
* Create a column helper pre-bound to the features and components configured in this table hook.
* The cell, header, and footer contexts include pre-bound components (e.g., `cell.TextCell`).
* @example
* ```tsx
* const columnHelper = createAppColumnHelper<Person>()
*
* const columns = [
* columnHelper.accessor('firstName', {
* header: 'First Name',
* cell: ({ cell }) => <cell.TextCell />, // cell has pre-bound components!
* }),
* columnHelper.accessor('age', {
* header: 'Age',
* cell: ({ cell }) => <cell.NumberCell />,
* }),
* ]
* ```
*/
function createAppColumnHelper() {
return createColumnHelper();
}
/**
* Access the table instance from within an `AppTable` wrapper.
* Use this in custom `tableComponents` passed to `createTableHook`.
* TFeatures is already known from the createTableHook call.
*
* @example
* ```tsx
* function PaginationControls() {
* const table = useTableContext()
* return (
* <table.Subscribe>
* {(atoms) => {
* const pagination = atoms.pagination.get()
* return (
* <div>
* <button onClick={() => table.previousPage()}>Prev</button>
* <span>Page {pagination.pageIndex + 1}</span>
* <button onClick={() => table.nextPage()}>Next</button>
* </div>
* )}}
* </table.Subscribe>
* )
* }
* ```
*/
function useTableContext() {
const table = useContext(TableContext);
if (!table) throw new Error("`useTableContext` must be used within an `AppTable` component. Make sure your component is wrapped with `<table.AppTable>...</table.AppTable>`.");
return table;
}
/**
* Access the cell instance from within an `AppCell` wrapper.
* Use this in custom `cellComponents` passed to `createTableHook`.
* TFeatures is already known from the createTableHook call.
*
* @example
* ```tsx
* function TextCell() {
* const cell = useCellContext<string>()
* return <span>{cell.getValue()}</span>
* }
*
* function NumberCell({ format }: { format?: Intl.NumberFormatOptions }) {
* const cell = useCellContext<number>()
* return <span>{cell.getValue().toLocaleString(undefined, format)}</span>
* }
* ```
*/
function useCellContext() {
const cell = useContext(CellContext);
if (!cell) throw new Error("`useCellContext` must be used within an `AppCell` component. Make sure your component is wrapped with `<table.AppCell cell={cell}>...</table.AppCell>`.");
return cell;
}
/**
* Access the header instance from within an `AppHeader` or `AppFooter` wrapper.
* Use this in custom `headerComponents` passed to `createTableHook`.
* TFeatures is already known from the createTableHook call.
*
* @example
* ```tsx
* function SortIndicator() {
* const header = useHeaderContext()
* const sorted = header.column.getIsSorted()
* return sorted === 'asc' ? '🔼' : sorted === 'desc' ? '🔽' : null
* }
*
* function ColumnFilter() {
* const header = useHeaderContext()
* if (!header.column.getCanFilter()) return null
* return (
* <input
* value={(header.column.getFilterValue() ?? '') as string}
* onChange={(e) => header.column.setFilterValue(e.target.value)}
* placeholder="Filter..."
* />
* )
* }
* ```
*/
function useHeaderContext() {
const header = useContext(HeaderContext);
if (!header) throw new Error("`useHeaderContext` must be used within an `AppHeader` or `AppFooter` component.");
return header;
}
/**
* Context-aware FlexRender component for cells.
* Uses the cell from context, so no need to pass cell prop.
*/
function CellFlexRender() {
const cell = useCellContext();
return createComponent$1(FlexRender, { cell });
}
/**
* Context-aware FlexRender component for headers.
* Uses the header from context, so no need to pass header prop.
*/
function HeaderFlexRender() {
const header = useHeaderContext();
return createComponent$1(FlexRender, { header });
}
/**
* Context-aware FlexRender component for footers.
* Uses the header from context, so no need to pass footer prop.
*/
function FooterFlexRender() {
const header = useHeaderContext();
return createComponent$1(FlexRender, { footer: header });
}
/**
* Enhanced useTable hook that returns a table with App wrapper components
* and pre-bound tableComponents attached directly to the table object.
*
* Default options from createTableHook are automatically merged with
* the options passed here. Options passed here take precedence.
*
* TFeatures is already known from the createTableHook call; TData is inferred from the data prop.
*/
function createAppTable(tableOptions) {
const mergedProps = mergeProps(defaultTableOptions, tableOptions);
const table = createTable(mergedProps);
function AppTable(props) {
return createComponent$1(TableContext.Provider, {
value: table,
get children() {
return props.children;
}
});
}
function AppCell(props) {
const extendedCell = Object.assign(props.cell, {
FlexRender: CellFlexRender,
...cellComponents
});
return createComponent$1(CellContext.Provider, {
get value() {
return props.cell;
},
get children() {
return props.children(extendedCell);
}
});
}
function AppHeader(props) {
const extendedHeader = Object.assign(props.header, {
FlexRender: HeaderFlexRender,
...headerComponents
});
return createComponent$1(HeaderContext.Provider, {
get value() {
return props.header;
},
get children() {
return props.children(extendedHeader);
}
});
}
function AppFooter(props) {
const extendedHeader = Object.assign(props.header, {
FlexRender: FooterFlexRender,
...headerComponents
});
return createComponent$1(HeaderContext.Provider, {
get value() {
return props.header;
},
get children() {
return props.children(extendedHeader);
}
});
}
return Object.assign(table, {
AppTable,
AppCell,
AppHeader,
AppFooter,
FlexRender,
...tableComponents
});
}
return {
appFeatures: defaultTableOptions.features,
createAppColumnHelper,
createAppTable,
useTableContext,
useCellContext,
useHeaderContext
};
}
//#endregion
export { createTableHook };