From 8c1f4183b757e0d82afa276413fff7a8095dd7d6 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sat, 7 Mar 2026 22:53:05 +0100 Subject: [PATCH 1/4] feat: add solid createTableHook --- packages/solid-table/src/FlexRender.tsx | 80 +- packages/solid-table/src/createTableHelper.ts | 81 -- packages/solid-table/src/createTableHook.tsx | 1083 +++++++++++++++++ packages/solid-table/src/index.tsx | 2 +- 4 files changed, 1157 insertions(+), 89 deletions(-) delete mode 100644 packages/solid-table/src/createTableHelper.ts create mode 100644 packages/solid-table/src/createTableHook.tsx diff --git a/packages/solid-table/src/FlexRender.tsx b/packages/solid-table/src/FlexRender.tsx index 21523bb90b..d4d082755b 100644 --- a/packages/solid-table/src/FlexRender.tsx +++ b/packages/solid-table/src/FlexRender.tsx @@ -1,5 +1,12 @@ -import { createComponent } from 'solid-js' +import { Match, Switch, createComponent } from 'solid-js' import type { JSX } from 'solid-js' +import type { + Cell, + CellData, + Header, + RowData, + TableFeatures, +} from '@tanstack/table-core' export function flexRender( Comp: ((_props: TProps) => JSX.Element) | JSX.Element | undefined, @@ -14,10 +21,69 @@ export function flexRender( return Comp } -export function FlexRender( - props: { - Comp: ((_props: TProps) => JSX.Element) | JSX.Element | undefined - } & TProps, -): JSX.Element { - return flexRender(props.Comp, props) +/** + * Simplified component wrapper of `flexRender`. Use this utility component to render headers, cells, or footers with custom markup. + * Only one prop (`cell`, `header`, or `footer`) may be passed. + * @example + * @example + * @example + */ +export type FlexRenderProps< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, +> = + | { cell: Cell; header?: never; footer?: never } + | { + header: Header + cell?: never + footer?: never + } + | { + footer: Header + cell?: never + header?: never + } + +/** + * Simplified component wrapper of `flexRender`. Use this utility component to render headers, cells, or footers with custom markup. + * Only one prop (`cell`, `header`, or `footer`) may be passed. + * @example + * ```tsx + * + * + * + * ``` + * + * This replaces calling `flexRender` directly like this: + * ```tsx + * flexRender(cell.column.columnDef.cell, cell.getContext()) + * flexRender(header.column.columnDef.header, header.getContext()) + * flexRender(footer.column.columnDef.footer, footer.getContext()) + * ``` + */ +export function FlexRender< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, +>(props: FlexRenderProps) { + return ( + + + {(cell) => + flexRender(cell().column.columnDef.cell, cell().getContext()) + } + + + {(header) => + flexRender(header().column.columnDef.header, header().getContext()) + } + + + {(footer) => + flexRender(footer().column.columnDef.footer, footer().getContext()) + } + + + ) } diff --git a/packages/solid-table/src/createTableHelper.ts b/packages/solid-table/src/createTableHelper.ts deleted file mode 100644 index 768899cf12..0000000000 --- a/packages/solid-table/src/createTableHelper.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { constructTableHelper } from '@tanstack/table-core' -import { createTable } from './createTable' -import type { SolidTable } from './createTable' -import type { - RowData, - TableFeatures, - TableHelperOptions, - TableHelper_Core, - TableOptions, - TableState, -} from '@tanstack/table-core' - -export type TableHelper< - TFeatures extends TableFeatures, - TData extends RowData = any, -> = Omit, 'tableCreator'> & { - createTable: ( - tableOptions: Omit< - TableOptions, - '_features' | '_rowModels' - >, - selector?: (state: TableState) => TSelected, - ) => SolidTable -} - -export function createTableHelper< - TFeatures extends TableFeatures, - TData extends RowData = any, ->( - tableHelperOptions: TableHelperOptions, -): TableHelper { - const tableHelper = constructTableHelper( - createTable as any, - tableHelperOptions, - ) - return { - ...tableHelper, - createTable: ( - tableOptions: Omit< - TableOptions, - '_features' | '_rowModels' - >, - selector?: (state: TableState) => TSelected, - ) => { - return createTable( - { ...tableHelper.options, ...tableOptions } as TableOptions< - TFeatures, - TData - >, - selector, - ) - }, - } as any -} - -// test - -// type Person = { -// firstName: string -// lastName: string -// age: number -// } - -// const tableHelper = createTableHelper({ -// _features: { rowSelectionFeature: {} }, -// TData: {} as Person, -// }) - -// const columns = [ -// tableHelper.columnHelper.accessor('firstName', { header: 'First Name' }), -// tableHelper.columnHelper.accessor('lastName', { header: 'Last Name' }), -// tableHelper.columnHelper.accessor('age', { header: 'Age' }), -// tableHelper.columnHelper.display({ header: 'Actions', id: 'actions' }), -// ] as Array> - -// const data: Array = [] - -// tableHelper.createTable({ -// columns, -// data, -// }) diff --git a/packages/solid-table/src/createTableHook.tsx b/packages/solid-table/src/createTableHook.tsx new file mode 100644 index 0000000000..f9c36902b3 --- /dev/null +++ b/packages/solid-table/src/createTableHook.tsx @@ -0,0 +1,1083 @@ +import { createColumnHelper as coreCreateColumnHelper } from '@tanstack/table-core' +import { createContext, createMemo, mergeProps, useContext } from 'solid-js' +import { createTable } from './createTable' +import { FlexRender } from './FlexRender' +import type { SolidTable } from './createTable' +import type { Accessor, Component, JSXElement } from 'solid-js' +import type { + AccessorFn, + AccessorFnColumnDef, + AccessorKeyColumnDef, + Cell, + CellContext, + CellData, + Column, + ColumnDef, + DeepKeys, + DeepValue, + DisplayColumnDef, + GroupColumnDef, + Header, + IdentifiedColumnDef, + NoInfer, + Row, + RowData, + Table, + TableFeatures, + TableOptions, + TableState, +} from '@tanstack/table-core' + +type ComponentType> = Component + +// ============================================================================= +// Enhanced Context Types with Pre-bound Components +// ============================================================================= + +/** + * Enhanced CellContext with pre-bound cell components. + * The `cell` property includes the registered cellComponents. + */ +export type AppCellContext< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + TCellComponents extends Record>, +> = { + cell: Cell & + TCellComponents & { FlexRender: () => JSXElement } + column: Column + getValue: CellContext['getValue'] + renderValue: CellContext['renderValue'] + row: Row + table: Table +} + +/** + * Enhanced HeaderContext with pre-bound header components. + * The `header` property includes the registered headerComponents. + */ +export type AppHeaderContext< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + THeaderComponents extends Record>, +> = { + column: Column + header: Header & + THeaderComponents & { FlexRender: () => JSXElement } + table: Table +} + +// ============================================================================= +// Enhanced Column Definition Types +// ============================================================================= + +/** + * Template type for column definitions that can be a string or a function. + */ +type AppColumnDefTemplate = + | string + | ((props: TProps) => any) + +/** + * Enhanced column definition base with pre-bound components in cell/header/footer contexts. + */ +type AppColumnDefBase< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + TCellComponents extends Record>, + THeaderComponents extends Record>, +> = Omit< + IdentifiedColumnDef, + 'cell' | 'header' | 'footer' +> & { + cell?: AppColumnDefTemplate< + AppCellContext + > + header?: AppColumnDefTemplate< + AppHeaderContext + > + footer?: AppColumnDefTemplate< + AppHeaderContext + > +} + +/** + * Enhanced display column definition with pre-bound components. + */ +type AppDisplayColumnDef< + TFeatures extends TableFeatures, + TData extends RowData, + TCellComponents extends Record>, + THeaderComponents extends Record>, +> = Omit< + DisplayColumnDef, + 'cell' | 'header' | 'footer' +> & { + cell?: AppColumnDefTemplate< + AppCellContext + > + header?: AppColumnDefTemplate< + AppHeaderContext + > + footer?: AppColumnDefTemplate< + AppHeaderContext + > +} + +/** + * Enhanced group column definition with pre-bound components. + */ +type AppGroupColumnDef< + TFeatures extends TableFeatures, + TData extends RowData, + TCellComponents extends Record>, + THeaderComponents extends Record>, +> = Omit< + GroupColumnDef, + 'cell' | 'header' | 'footer' | 'columns' +> & { + cell?: AppColumnDefTemplate< + AppCellContext + > + header?: AppColumnDefTemplate< + AppHeaderContext + > + footer?: AppColumnDefTemplate< + AppHeaderContext + > + columns?: Array> +} + +// ============================================================================= +// Enhanced Column Helper Type +// ============================================================================= + +/** + * Enhanced column helper with pre-bound components in cell/header/footer contexts. + * This enables TypeScript to know about the registered components when defining columns. + */ +export type AppColumnHelper< + TFeatures extends TableFeatures, + TData extends RowData, + TCellComponents extends Record>, + THeaderComponents extends Record>, +> = { + /** + * Creates a data column definition with an accessor key or function. + * The cell, header, and footer contexts include pre-bound components. + */ + accessor: < + TAccessor extends AccessorFn | DeepKeys, + TValue extends TAccessor extends AccessorFn + ? TReturn + : TAccessor extends DeepKeys + ? DeepValue + : never, + >( + accessor: TAccessor, + column: TAccessor extends AccessorFn + ? AppColumnDefBase< + TFeatures, + TData, + TValue, + TCellComponents, + THeaderComponents + > & { id: string } + : AppColumnDefBase< + TFeatures, + TData, + TValue, + TCellComponents, + THeaderComponents + >, + ) => TAccessor extends AccessorFn + ? AccessorFnColumnDef + : AccessorKeyColumnDef + + /** + * Wraps an array of column definitions to preserve each column's individual TValue type. + */ + columns: >>( + columns: [...TColumns], + ) => Array> & [...TColumns] + + /** + * Creates a display column definition for non-data columns. + * The cell, header, and footer contexts include pre-bound components. + */ + display: ( + column: AppDisplayColumnDef< + TFeatures, + TData, + TCellComponents, + THeaderComponents + >, + ) => DisplayColumnDef + + /** + * Creates a group column definition with nested child columns. + * The cell, header, and footer contexts include pre-bound components. + */ + group: ( + column: AppGroupColumnDef< + TFeatures, + TData, + TCellComponents, + THeaderComponents + >, + ) => GroupColumnDef +} + +// ============================================================================= +// CreateTableHook Options and Props +// ============================================================================= + +/** + * Options for creating a table hook with pre-bound components and default table options. + * Extends all TableOptions except 'columns' | 'data' | 'store' | 'state' | 'initialState'. + */ +export type CreateTableHookOptions< + TFeatures extends TableFeatures, + TTableComponents extends Record>, + TCellComponents extends Record>, + THeaderComponents extends Record>, +> = Omit< + TableOptions, + 'columns' | 'data' | 'store' | 'state' | 'initialState' +> & { + /** + * Table-level components that need access to the table instance. + * These are available directly on the table object returned by useAppTable. + * Use `useTableContext()` inside these components. + * @example { PaginationControls, GlobalFilter, RowCount } + */ + tableComponents?: TTableComponents + /** + * Cell-level components that need access to the cell instance. + * These are available on the cell object passed to AppCell's children. + * Use `useCellContext()` inside these components. + * @example { TextCell, NumberCell, DateCell, CurrencyCell } + */ + cellComponents?: TCellComponents + /** + * Header-level components that need access to the header instance. + * These are available on the header object passed to AppHeader/AppFooter's children. + * Use `useHeaderContext()` inside these components. + * @example { SortIndicator, ColumnFilter, ResizeHandle } + */ + headerComponents?: THeaderComponents +} + +/** + * Props for AppTable component - without selector + */ +export interface AppTablePropsWithoutSelector { + children: JSXElement + selector?: never +} + +/** + * Props for AppTable component - with selector + */ +export interface AppTablePropsWithSelector< + TFeatures extends TableFeatures, + TSelected, +> { + children: (state: TSelected) => JSXElement + selector: (state: TableState) => TSelected +} + +/** + * Props for AppCell component - without selector + */ +export interface AppCellPropsWithoutSelector< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + TCellComponents extends Record>, +> { + cell: Cell + children: ( + cell: Cell & + TCellComponents & { FlexRender: () => JSXElement }, + ) => JSXElement + selector?: never +} + +/** + * Props for AppCell component - with selector + */ +export interface AppCellPropsWithSelector< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + TCellComponents extends Record>, + TSelected, +> { + cell: Cell + children: ( + cell: Cell & + TCellComponents & { FlexRender: () => JSXElement }, + state: Accessor, + ) => JSXElement + selector: (state: TableState) => TSelected +} + +/** + * Props for AppHeader/AppFooter component - without selector + */ +export interface AppHeaderPropsWithoutSelector< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + THeaderComponents extends Record>, +> { + header: Header + children: ( + header: Header & + THeaderComponents & { FlexRender: () => JSXElement }, + ) => JSXElement + selector?: never +} + +/** + * Props for AppHeader/AppFooter component - with selector + */ +export interface AppHeaderPropsWithSelector< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + THeaderComponents extends Record>, + TSelected, +> { + header: Header + children: ( + header: Header & + THeaderComponents & { FlexRender: () => JSXElement }, + state: Accessor, + ) => JSXElement + selector: (state: TableState) => TSelected +} + +/** + * Component type for AppCell - wraps a cell and provides cell context with optional Subscribe + */ +export interface AppCellComponent< + TFeatures extends TableFeatures, + TData extends RowData, + TCellComponents extends Record>, +> { + ( + props: AppCellPropsWithoutSelector< + TFeatures, + TData, + TValue, + TCellComponents + >, + ): JSXElement + ( + props: AppCellPropsWithSelector< + TFeatures, + TData, + TValue, + TCellComponents, + TSelected + >, + ): JSXElement +} + +/** + * Component type for AppHeader/AppFooter - wraps a header and provides header context with optional Subscribe + */ +export interface AppHeaderComponent< + TFeatures extends TableFeatures, + TData extends RowData, + THeaderComponents extends Record>, +> { + ( + props: AppHeaderPropsWithoutSelector< + TFeatures, + TData, + TValue, + THeaderComponents + >, + ): JSXElement + ( + props: AppHeaderPropsWithSelector< + TFeatures, + TData, + TValue, + THeaderComponents, + TSelected + >, + ): JSXElement +} + +/** + * Component type for AppTable - root wrapper with optional Subscribe + */ +export interface AppTableComponent { + (props: AppTablePropsWithoutSelector): JSXElement + ( + props: AppTablePropsWithSelector, + ): JSXElement +} + +/** + * Extended table API returned by useAppTable with all App wrapper components + */ +export type AppSolidTable< + TFeatures extends TableFeatures, + TData extends RowData, + TSelected, + TTableComponents extends Record>, + TCellComponents extends Record>, + THeaderComponents extends Record>, +> = SolidTable & + NoInfer & { + /** + * Root wrapper component that provides table context with optional Subscribe. + * @example + * ```tsx + * // Without selector - children is JSXElement + * + * ...
+ *
+ * + * // With selector - children receives selected state + * s.pagination}> + * {(pagination) =>
Page {pagination.pageIndex}
} + *
+ * ``` + */ + AppTable: AppTableComponent + /** + * Wraps a cell and provides cell context with pre-bound cellComponents. + * Optionally accepts a selector for Subscribe functionality. + * @example + * ```tsx + * // Without selector + * + * {(c) => } + * + * + * // With selector - children receives cell and selected state + * s.columnFilters}> + * {(c, filters) => {filters.length}} + * + * ``` + */ + AppCell: AppCellComponent> + /** + * Wraps a header and provides header context with pre-bound headerComponents. + * Optionally accepts a selector for Subscribe functionality. + * @example + * ```tsx + * // Without selector + * + * {(h) => } + * + * + * // With selector + * s.sorting}> + * {(h, sorting) => {sorting.length} sorted} + * + * ``` + */ + AppHeader: AppHeaderComponent> + /** + * Wraps a footer and provides header context with pre-bound headerComponents. + * Optionally accepts a selector for Subscribe functionality. + * @example + * ```tsx + * + * {(f) => } + * + * ``` + */ + AppFooter: AppHeaderComponent> + } + +/** + * 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 `useAppTable` 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 { + * useAppTable, + * createAppColumnHelper, + * useTableContext, + * useCellContext, + * useHeaderContext, + * } = createTableHook({ + * _features: tableFeatures({ + * rowPaginationFeature, + * rowSortingFeature, + * columnFilteringFeature, + * }), + * _rowModels: { + * paginatedRowModel: createPaginatedRowModel(), + * sortedRowModel: createSortedRowModel(sortFns), + * filteredRowModel: createFilteredRowModel(filterFns), + * }, + * tableComponents: { PaginationControls, RowCount }, + * cellComponents: { TextCell, NumberCell }, + * headerComponents: { SortIndicator, ColumnFilter }, + * }) + * + * // Create column helper with TFeatures already bound + * const columnHelper = createAppColumnHelper() + * + * // components/table-components.tsx + * function PaginationControls() { + * const table = useTableContext() // TFeatures already known! + * return s.pagination}>... + * } + * + * // features/users.tsx + * function UsersTable({ data }: { data: Person[] }) { + * const table = useAppTable({ + * columns, + * data, // TData inferred from Person[] + * }) + * + * return ( + * + * + * + * {table.getHeaderGroups().map(headerGroup => ( + * + * {headerGroup.headers.map(h => ( + * + * {(header) => ( + * + * )} + * + * ))} + * + * ))} + * + * + * {table.getRowModel().rows.map(row => ( + * + * {row.getAllCells().map(c => ( + * + * {(cell) => } + * + * ))} + * + * ))} + * + *
+ * + * + *
+ * + *
+ * ) + * } + * ``` + */ +export function createTableHook< + TFeatures extends TableFeatures, + const TTableComponents extends Record>, + const TCellComponents extends Record>, + const THeaderComponents extends Record>, +>({ + tableComponents, + cellComponents, + headerComponents, + ...defaultTableOptions +}: CreateTableHookOptions< + TFeatures, + TTableComponents, + TCellComponents, + THeaderComponents +>) { + // Create contexts internally with TFeatures baked in + const TableContext = createContext>( + null as never, + ) + const CellContext = createContext>(null as never) + const HeaderContext = createContext>( + null as never, + ) + + /** + * 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() + * + * const columns = [ + * columnHelper.accessor('firstName', { + * header: 'First Name', + * cell: ({ cell }) => , // cell has pre-bound components! + * }), + * columnHelper.accessor('age', { + * header: 'Age', + * cell: ({ cell }) => , + * }), + * ] + * ``` + */ + function createAppColumnHelper(): AppColumnHelper< + TFeatures, + TData, + TCellComponents, + THeaderComponents + > { + // The runtime implementation is the same - components are attached at render time + // This cast provides the enhanced types for column definitions + return coreCreateColumnHelper() as AppColumnHelper< + TFeatures, + TData, + TCellComponents, + THeaderComponents + > + } + + /** + * 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 ( + * s.pagination}> + * {(pagination) => ( + *
+ * + * Page {pagination.pageIndex + 1} + * + *
+ * )} + *
+ * ) + * } + * ``` + */ + function useTableContext(): SolidTable< + TFeatures, + TData + > { + const table = useContext(TableContext) + + if (!table) { + throw new Error( + '`useTableContext` must be used within an `AppTable` component. ' + + 'Make sure your component is wrapped with `...`.', + ) + } + + 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() + * return {cell.getValue()} + * } + * + * function NumberCell({ format }: { format?: Intl.NumberFormatOptions }) { + * const cell = useCellContext() + * return {cell.getValue().toLocaleString(undefined, format)} + * } + * ``` + */ + function useCellContext(): Cell< + TFeatures, + any, + TValue + > { + const cell = useContext(CellContext) + + if (!cell) { + throw new Error( + '`useCellContext` must be used within an `AppCell` component. ' + + 'Make sure your component is wrapped with `...`.', + ) + } + + 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 ( + * header.column.setFilterValue(e.target.value)} + * placeholder="Filter..." + * /> + * ) + * } + * ``` + */ + function useHeaderContext(): Header< + TFeatures, + any, + TValue + > { + 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 + } + + /** + * Context-aware FlexRender component for headers. + * Uses the header from context, so no need to pass header prop. + */ + function HeaderFlexRender() { + const header = useHeaderContext() + return + } + + /** + * Context-aware FlexRender component for footers. + * Uses the header from context, so no need to pass footer prop. + */ + function FooterFlexRender() { + const header = useHeaderContext() + return + } + + /** + * 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: Omit< + TableOptions, + '_features' | '_rowModels' + >, + selector?: (state: TableState) => TSelected, + ): AppSolidTable< + TFeatures, + TData, + TSelected, + TTableComponents, + TCellComponents, + THeaderComponents + > { + // Merge default options with provided options (provided takes precedence) + const mergedProps = mergeProps(defaultTableOptions, tableOptions) + const table = createTable( + mergedProps as TableOptions, + selector, + ) + + // AppTable - Root wrapper that provides table context with optional Subscribe + const AppTable = createMemo(() => { + function AppTableImpl(props: AppTablePropsWithoutSelector): JSXElement + function AppTableImpl( + props: AppTablePropsWithSelector, + ): JSXElement + function AppTableImpl( + props: + | AppTablePropsWithoutSelector + | AppTablePropsWithSelector, + ): JSXElement { + const { children, selector: appTableSelector } = props as any + + return ( + + {appTableSelector ? ( + + {(state: Accessor) => children(state)} + + ) : ( + children + )} + + ) + } + return AppTableImpl as AppTableComponent + }, [table]) + + // AppCell - Wraps cell with context, pre-bound cellComponents, and optional Subscribe + const AppCell = createMemo(() => { + function AppCellImpl( + props: AppCellPropsWithoutSelector< + TFeatures, + TData, + TValue, + TCellComponents + >, + ): JSXElement + function AppCellImpl< + TValue extends CellData = CellData, + TAppCellSelected = unknown, + >( + props: AppCellPropsWithSelector< + TFeatures, + TData, + TValue, + TCellComponents, + TAppCellSelected + >, + ): JSXElement + function AppCellImpl< + TValue extends CellData = CellData, + TAppCellSelected = unknown, + >( + props: + | AppCellPropsWithoutSelector< + TFeatures, + TData, + TValue, + TCellComponents + > + | AppCellPropsWithSelector< + TFeatures, + TData, + TValue, + TCellComponents, + TAppCellSelected + >, + ): JSXElement { + const { cell, children, selector: appCellSelector } = props as any + const extendedCell = Object.assign(cell, { + FlexRender: CellFlexRender, + ...cellComponents, + }) + + return ( + + {appCellSelector ? ( + + {(state: Accessor) => + children(extendedCell, state) + } + + ) : ( + children(extendedCell) + )} + + ) + } + return AppCellImpl as AppCellComponent + }, [table]) + + // AppHeader - Wraps header with context, pre-bound headerComponents, and optional Subscribe + const AppHeader = createMemo(() => { + function AppHeaderImpl( + props: AppHeaderPropsWithoutSelector< + TFeatures, + TData, + TValue, + THeaderComponents + >, + ): JSXElement + function AppHeaderImpl< + TValue extends CellData = CellData, + TAppHeaderSelected = unknown, + >( + props: AppHeaderPropsWithSelector< + TFeatures, + TData, + TValue, + THeaderComponents, + TAppHeaderSelected + >, + ): JSXElement + function AppHeaderImpl< + TValue extends CellData = CellData, + TAppHeaderSelected = unknown, + >( + props: + | AppHeaderPropsWithoutSelector< + TFeatures, + TData, + TValue, + THeaderComponents + > + | AppHeaderPropsWithSelector< + TFeatures, + TData, + TValue, + THeaderComponents, + TAppHeaderSelected + >, + ): JSXElement { + const { header, children, selector: appHeaderSelector } = props as any + const extendedHeader = Object.assign(header, { + FlexRender: HeaderFlexRender, + ...headerComponents, + }) + + return ( + + {appHeaderSelector ? ( + + {(state: Accessor) => + children(extendedHeader, state) + } + + ) : ( + children(extendedHeader) + )} + + ) + } + return AppHeaderImpl as AppHeaderComponent< + TFeatures, + TData, + THeaderComponents + > + }, [table]) + + // AppFooter - Same as AppHeader (footers use Header type) + const AppFooter = createMemo(() => { + function AppFooterImpl( + props: AppHeaderPropsWithoutSelector< + TFeatures, + TData, + TValue, + THeaderComponents + >, + ): JSXElement + function AppFooterImpl< + TValue extends CellData = CellData, + TAppFooterSelected = unknown, + >( + props: AppHeaderPropsWithSelector< + TFeatures, + TData, + TValue, + THeaderComponents, + TAppFooterSelected + >, + ): JSXElement + function AppFooterImpl< + TValue extends CellData = CellData, + TAppFooterSelected = unknown, + >( + props: + | AppHeaderPropsWithoutSelector< + TFeatures, + TData, + TValue, + THeaderComponents + > + | AppHeaderPropsWithSelector< + TFeatures, + TData, + TValue, + THeaderComponents, + TAppFooterSelected + >, + ): JSXElement { + const { header, children, selector: appFooterSelector } = props as any + const extendedHeader = Object.assign(header, { + FlexRender: FooterFlexRender, + ...headerComponents, + }) + + return ( + + {appFooterSelector ? ( + + {(state: Accessor) => + children(extendedHeader, state) + } + + ) : ( + ( + children as ( + header: Header & + THeaderComponents & { FlexRender: () => JSXElement }, + ) => JSXElement + )(extendedHeader) + )} + + ) + } + return AppFooterImpl as AppHeaderComponent< + TFeatures, + TData, + THeaderComponents + > + }, [table]) + + // Combine everything into the extended table API + return Object.assign(table, { + AppTable, + AppCell, + AppHeader, + AppFooter, + ...tableComponents, + }) as AppSolidTable< + TFeatures, + TData, + TSelected, + TTableComponents, + TCellComponents, + THeaderComponents + > + } + + return { + appFeatures: defaultTableOptions._features as TFeatures, + createAppColumnHelper, + useAppTable: createAppTable, + useTableContext, + useCellContext, + useHeaderContext, + } +} diff --git a/packages/solid-table/src/index.tsx b/packages/solid-table/src/index.tsx index c3b394a4fb..93c73360df 100755 --- a/packages/solid-table/src/index.tsx +++ b/packages/solid-table/src/index.tsx @@ -2,4 +2,4 @@ export * from '@tanstack/table-core' export * from './createTable' export * from './FlexRender' -export * from './createTableHelper' +export * from './createTableHook' From 5e15ca31c53ef44bd41a113d1f18e942a6d80d3c Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 9 Mar 2026 21:43:38 +0100 Subject: [PATCH 2/4] feat(solid): createTableHook helper Add new examples: - basic app table - composble tables Update config.json to render new solid table examples --- docs/config.json | 5 +- examples/solid/basic-app-table/.gitignore | 2 + examples/solid/basic-app-table/README.md | 6 + examples/solid/basic-app-table/index.html | 16 + examples/solid/basic-app-table/package.json | 22 + examples/solid/basic-app-table/src/App.tsx | 174 +++++++ examples/solid/basic-app-table/src/index.css | 26 + examples/solid/basic-app-table/src/index.tsx | 5 + examples/solid/basic-app-table/tsconfig.json | 25 + examples/solid/basic-app-table/vite.config.ts | 10 + examples/solid/composable-tables/.gitignore | 2 + examples/solid/composable-tables/README.md | 6 + examples/solid/composable-tables/index.html | 16 + examples/solid/composable-tables/package.json | 22 + examples/solid/composable-tables/src/App.tsx | 438 ++++++++++++++++ .../src/components/cell-components.tsx | 107 ++++ .../src/components/header-components.tsx | 76 +++ .../src/components/table-components.tsx | 116 +++++ .../composable-tables/src/hooks/table.ts | 105 ++++ .../solid/composable-tables/src/index.css | 249 +++++++++ .../solid/composable-tables/src/index.tsx | 6 + .../solid/composable-tables/src/makeData.ts | 77 +++ .../solid/composable-tables/tsconfig.json | 25 + .../solid/composable-tables/vite.config.ts | 10 + packages/solid-table/src/createTableHook.tsx | 479 +++++++++--------- pnpm-lock.yaml | 38 ++ 26 files changed, 1821 insertions(+), 242 deletions(-) create mode 100644 examples/solid/basic-app-table/.gitignore create mode 100644 examples/solid/basic-app-table/README.md create mode 100644 examples/solid/basic-app-table/index.html create mode 100644 examples/solid/basic-app-table/package.json create mode 100644 examples/solid/basic-app-table/src/App.tsx create mode 100644 examples/solid/basic-app-table/src/index.css create mode 100644 examples/solid/basic-app-table/src/index.tsx create mode 100644 examples/solid/basic-app-table/tsconfig.json create mode 100644 examples/solid/basic-app-table/vite.config.ts create mode 100644 examples/solid/composable-tables/.gitignore create mode 100644 examples/solid/composable-tables/README.md create mode 100644 examples/solid/composable-tables/index.html create mode 100644 examples/solid/composable-tables/package.json create mode 100644 examples/solid/composable-tables/src/App.tsx create mode 100644 examples/solid/composable-tables/src/components/cell-components.tsx create mode 100644 examples/solid/composable-tables/src/components/header-components.tsx create mode 100644 examples/solid/composable-tables/src/components/table-components.tsx create mode 100644 examples/solid/composable-tables/src/hooks/table.ts create mode 100644 examples/solid/composable-tables/src/index.css create mode 100644 examples/solid/composable-tables/src/index.tsx create mode 100644 examples/solid/composable-tables/src/makeData.ts create mode 100644 examples/solid/composable-tables/tsconfig.json create mode 100644 examples/solid/composable-tables/vite.config.ts diff --git a/docs/config.json b/docs/config.json index 8f7f3818be..637337e213 100644 --- a/docs/config.json +++ b/docs/config.json @@ -823,6 +823,7 @@ "label": "solid", "children": [ { "to": "framework/solid/examples/basic", "label": "Basic" }, + { "to": "framework/solid/examples/basic-app-table", "label": "Basic (useAppTable)" }, { "to": "framework/solid/examples/basic-table-helper", "label": "Basic with Helpers" }, { "to": "framework/solid/examples/column-groups", "label": "Header Groups" } ] @@ -999,8 +1000,8 @@ { "label": "solid", "children": [ - { "to": "framework/solid/examples/bootstrap", "label": "Solid Bootstrap" } - ] + { "to": "framework/solid/examples/bootstrap", "label": "Solid Bootstrap" }, + { "to": "framework/solid/examples/composable-tables", "label": "Composable Tables" }] }, { "label": "vue", diff --git a/examples/solid/basic-app-table/.gitignore b/examples/solid/basic-app-table/.gitignore new file mode 100644 index 0000000000..76add878f8 --- /dev/null +++ b/examples/solid/basic-app-table/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/examples/solid/basic-app-table/README.md b/examples/solid/basic-app-table/README.md new file mode 100644 index 0000000000..b168d3c4b1 --- /dev/null +++ b/examples/solid/basic-app-table/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm run start` or `yarn start` diff --git a/examples/solid/basic-app-table/index.html b/examples/solid/basic-app-table/index.html new file mode 100644 index 0000000000..7d9b4f7c51 --- /dev/null +++ b/examples/solid/basic-app-table/index.html @@ -0,0 +1,16 @@ + + + + + + + + Solid App + + + +
+ + + + diff --git a/examples/solid/basic-app-table/package.json b/examples/solid/basic-app-table/package.json new file mode 100644 index 0000000000..513083a3f1 --- /dev/null +++ b/examples/solid/basic-app-table/package.json @@ -0,0 +1,22 @@ +{ + "name": "tanstack-table-example-solid-basic-app-table", + "version": "0.0.0", + "description": "", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "lint": "eslint ./src" + }, + "license": "MIT", + "devDependencies": { + "typescript": "5.9.3", + "vite": "^7.3.1", + "vite-plugin-solid": "^2.11.10" + }, + "dependencies": { + "@tanstack/solid-table": "^9.0.0-alpha.10", + "solid-js": "^1.9.11" + } +} diff --git a/examples/solid/basic-app-table/src/App.tsx b/examples/solid/basic-app-table/src/App.tsx new file mode 100644 index 0000000000..8b1507e30a --- /dev/null +++ b/examples/solid/basic-app-table/src/App.tsx @@ -0,0 +1,174 @@ +import { createTableHook } from '@tanstack/solid-table' +import { For, createSignal } from 'solid-js' + +// This example uses the new `createTableHook` method to create a re-usable table hook factory instead of independently using the standalone `useTable` hook and `createColumnHelper` method. You can choose to use either way. + +// 1. Define what the shape of your data will be for each row +type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +// 2. Create some dummy data with a stable reference (this could be an API response stored in useState or similar) +const defaultData: Array = [ + { + firstName: 'tanner', + lastName: 'linsley', + age: 24, + visits: 100, + status: 'In Relationship', + progress: 50, + }, + { + firstName: 'tandy', + lastName: 'miller', + age: 40, + visits: 40, + status: 'Single', + progress: 80, + }, + { + firstName: 'joe', + lastName: 'dirte', + age: 45, + visits: 20, + status: 'Complicated', + progress: 10, + }, + { + firstName: 'kevin', + lastName: 'vandy', + age: 28, + visits: 100, + status: 'Single', + progress: 70, + }, +] + +// 3. New in V9! Tell the table which features and row models we want to use. In this case, this will be a basic table with no additional features +const { createAppTable, createAppColumnHelper } = createTableHook({ + _features: {}, + _rowModels: {}, // client-side row models. `Core` row model is now included by default, but you can still override it here + debugTable: true, +}) + +// 4. Create a helper object to help define our columns +const columnHelper = createAppColumnHelper() + +// 5. Define the columns for your table with a stable reference (in this case, defined statically outside of a react component) +const columns = columnHelper.columns([ + // accessorKey method (most common for simple use-cases) + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (info) => info.column.id, + }), + // accessorFn used (alternative) along with a custom id + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => {info.getValue()}, + header: () => Last Name, + footer: (info) => info.column.id, + }), + // accessorFn used to transform the data + columnHelper.accessor((row) => Number(row.age), { + id: 'age', + header: () => 'Age', + cell: (info) => info.renderValue(), + footer: (info) => info.column.id, + }), + columnHelper.accessor('visits', { + header: () => Visits, + footer: (info) => info.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (info) => info.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (info) => info.column.id, + }), +]) + +export function App() { + // 6. Store data with a stable reference + const [data, setData] = createSignal([...defaultData]) + + // Helper to rerender with sorted data (by age ascending) + function rerender() { + setData((prev) => + prev.slice().sort((a: Person, b: Person) => a.age - b.age), + ) + } + + // 7. Create the table instance with the required columns and data. + // Features and row models are already defined in the createTableHook call above + const table = createAppTable({ + columns, + get data() { + return data() + }, + // add additional table options here or in the createTableHook call above + }) + + // 8. Render your table markup from the table instance APIs + return ( +
+ + + + {(headerGroup) => ( + + + {(header) => ( + + )} + + + )} + + + + + {(row) => ( + + + {(cell) => ( + + )} + + + )} + + + + + {(footerGroup) => ( + + + {(header) => ( + + )} + + + )} + + +
+ +
+ +
+ +
+
+ +
+ ) +} diff --git a/examples/solid/basic-app-table/src/index.css b/examples/solid/basic-app-table/src/index.css new file mode 100644 index 0000000000..43c09e0f6b --- /dev/null +++ b/examples/solid/basic-app-table/src/index.css @@ -0,0 +1,26 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} diff --git a/examples/solid/basic-app-table/src/index.tsx b/examples/solid/basic-app-table/src/index.tsx new file mode 100644 index 0000000000..30c97d2fdb --- /dev/null +++ b/examples/solid/basic-app-table/src/index.tsx @@ -0,0 +1,5 @@ +import { render } from 'solid-js/web' +import './index.css' +import { App } from './App' + +render(() => , document.getElementById('root') as HTMLElement) diff --git a/examples/solid/basic-app-table/tsconfig.json b/examples/solid/basic-app-table/tsconfig.json new file mode 100644 index 0000000000..7ab027b8d2 --- /dev/null +++ b/examples/solid/basic-app-table/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/solid/basic-app-table/vite.config.ts b/examples/solid/basic-app-table/vite.config.ts new file mode 100644 index 0000000000..d27427972d --- /dev/null +++ b/examples/solid/basic-app-table/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import solidPlugin from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [solidPlugin()], + build: { + target: 'esnext', + polyfillDynamicImport: false, + }, +}) diff --git a/examples/solid/composable-tables/.gitignore b/examples/solid/composable-tables/.gitignore new file mode 100644 index 0000000000..f06235c460 --- /dev/null +++ b/examples/solid/composable-tables/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/examples/solid/composable-tables/README.md b/examples/solid/composable-tables/README.md new file mode 100644 index 0000000000..b168d3c4b1 --- /dev/null +++ b/examples/solid/composable-tables/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm run start` or `yarn start` diff --git a/examples/solid/composable-tables/index.html b/examples/solid/composable-tables/index.html new file mode 100644 index 0000000000..7d9b4f7c51 --- /dev/null +++ b/examples/solid/composable-tables/index.html @@ -0,0 +1,16 @@ + + + + + + + + Solid App + + + +
+ + + + diff --git a/examples/solid/composable-tables/package.json b/examples/solid/composable-tables/package.json new file mode 100644 index 0000000000..a475bb82db --- /dev/null +++ b/examples/solid/composable-tables/package.json @@ -0,0 +1,22 @@ +{ + "name": "tanstack-table-example-solid-composable-tables", + "version": "0.0.0", + "description": "", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "lint": "eslint ./src" + }, + "license": "MIT", + "devDependencies": { + "typescript": "5.9.3", + "vite": "^7.3.1", + "vite-plugin-solid": "^2.11.10" + }, + "dependencies": { + "@tanstack/solid-table": "^9.0.0-alpha.10", + "solid-js": "^1.9.11" + } +} diff --git a/examples/solid/composable-tables/src/App.tsx b/examples/solid/composable-tables/src/App.tsx new file mode 100644 index 0000000000..c7c64bd112 --- /dev/null +++ b/examples/solid/composable-tables/src/App.tsx @@ -0,0 +1,438 @@ +import { For, createSignal } from 'solid-js' +import { createAppColumnHelper, createAppTable } from './hooks/table' +import { makeData, makeProductData } from './makeData' +import type { Person, Product } from './makeData' +// Import cell components directly - they use useCellContext internally + +// Create column helpers with TFeatures already bound - only need TData! +const personColumnHelper = createAppColumnHelper() +const productColumnHelper = createAppColumnHelper() + +// Users Table Component - Original implementation +function UsersTable() { + // Data state + const [data, setData] = createSignal(makeData(1000)) + + // Refresh data callback + const refreshData = () => { + setData(() => makeData(1000)) + } + + // Define columns using the column helper + const columns = + // NOTE: You must use `createAppColumnHelper` instead of `createColumnHelper` when using pre-bound components like + personColumnHelper.columns([ + personColumnHelper.accessor('firstName', { + header: 'First Name', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.accessor('lastName', { + header: 'Last Name', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.accessor('age', { + header: 'Age', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.accessor('visits', { + header: 'Visits', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.accessor('progress', { + header: 'Progress', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.display({ + id: 'actions', + header: 'Actions', + cell: ({ cell }) => , + }), + ]) + + // Create the table - _features and _rowModels are already configured! + const table = createAppTable({ + columns, + get data() { + return data() + }, + debugTable: true, + // more table options + }) + + return ( + // Main selector on AppTable - selects all needed state in one place + ({ + // subscribe to specific states for re-rendering if you are optimizing for maximum performance + pagination: state.pagination, + sorting: state.sorting, + columnFilters: state.columnFilters, + })} + > + {(state) => { + const sorting = () => state().sorting + const columnFilters = () => state().columnFilters + return ( +
+ {/* Table toolbar using pre-bound component */} + + + {/* Table element */} + + + + {(headerGroup) => ( + + + {(h) => ( + + {(header) => ( + + )} + + )} + + + )} + + + + + {(row) => ( + + + {(c) => ( + + {(cell) => ( + + )} + + )} + + + )} + + + + + {(footerGroup) => ( + + + {(f) => ( + + {(footer) => { + const columnId = footer.column.id + const hasFilter = () => + columnFilters().some((cf) => cf.id === columnId) + + return ( + + ) + }} + + )} + + + )} + + +
+ {header.isPlaceholder ? null : ( + <> + + + + {/* Show sort order number when multiple columns sorted */} + {sorting().length > 1 && + sorting().findIndex( + (s) => s.id === header.column.id, + ) > -1 && ( + + {sorting().findIndex( + (s) => s.id === header.column.id, + ) + 1} + + )} + + )} +
+ {/* Cell components are pre-bound via AppCell */} + +
+ {footer.isPlaceholder ? null : ( + <> + {/* Use FooterSum for numeric columns, FooterColumnId for others */} + {columnId === 'age' || + columnId === 'visits' || + columnId === 'progress' ? ( + <> + + {hasFilter() && ( + + {' '} + (filtered) + + )} + + ) : columnId === 'actions' ? null : ( + <> + + {hasFilter() && ( + + {' '} + ✓ + + )} + + )} + + )} +
+ + {/* Pagination using pre-bound component */} + + + {/* Row count using pre-bound component */} + +
+ ) + }} +
+ ) +} + +// Products Table Component - New implementation using same hook and components +function ProductsTable() { + // Data state + const [data, setData] = createSignal(makeProductData(500)) + + // Refresh data callback + const refreshData = () => { + setData(makeProductData(500)) + } + + // Define columns using the column helper - different structure than Users table + const columns = productColumnHelper.columns([ + productColumnHelper.accessor('name', { + header: 'Product Name', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + productColumnHelper.accessor('category', { + header: 'Category', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + productColumnHelper.accessor('price', { + header: 'Price', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + productColumnHelper.accessor('stock', { + header: 'In Stock', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + productColumnHelper.accessor('rating', { + header: 'Rating', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + ]) + + // Create the table using the same createAppTable hook + const table = createAppTable({ + columns, + get data() { + return data() + }, + getRowId: (row) => row.id, + }) + + return ( + ({ + pagination: state.pagination, + sorting: state.sorting, + columnFilters: state.columnFilters, + })} + > + {(state) => { + const sorting = () => state().sorting + const columnFilters = () => state().columnFilters + return ( +
+ {/* Table toolbar using the same pre-bound component */} + + + {/* Table element */} + + + + {(headerGroup) => ( + + + {(h) => ( + + {(header) => ( + + )} + + )} + + + )} + + + + + {(row) => ( + + + {(c) => ( + + {(cell) => ( + + )} + + )} + + + )} + + + + + {(footerGroup) => ( + + + {(f) => ( + + {(footer) => { + const columnId = footer.column.id + const hasFilter = () => + columnFilters().some((cf) => cf.id === columnId) + + return ( + + ) + }} + + )} + + + )} + + +
+ {header.isPlaceholder ? null : ( + <> + + + + {sorting().length > 1 && + sorting().findIndex( + (s) => s.id === header.column.id, + ) > -1 && ( + + {sorting().findIndex( + (s) => s.id === header.column.id, + ) + 1} + + )} + + )} +
+ {/* Cell components are pre-bound via AppCell */} + +
+ {footer.isPlaceholder ? null : ( + <> + {/* Use FooterSum for numeric columns, FooterColumnId for others */} + {columnId === 'price' || + columnId === 'stock' || + columnId === 'rating' ? ( + <> + + {hasFilter() && ( + + {' '} + (filtered) + + )} + + ) : ( + <> + + {hasFilter() && ( + + {' '} + ✓ + + )} + + )} + + )} +
+ + {/* Pagination using the same pre-bound component */} + + + {/* Row count using the same pre-bound component */} + +
+ ) + }} +
+ ) +} + +function App() { + return ( +
+

Composable Tables Example

+

+ Both tables below use the same createAppTable hook and + shareable components, but with different data types and column + configurations. +

+ + {/* Original Users Table */} + + +
+ + {/* New Products Table */} + +
+ ) +} + +export default App diff --git a/examples/solid/composable-tables/src/components/cell-components.tsx b/examples/solid/composable-tables/src/components/cell-components.tsx new file mode 100644 index 0000000000..af78520679 --- /dev/null +++ b/examples/solid/composable-tables/src/components/cell-components.tsx @@ -0,0 +1,107 @@ +/** + * Cell-level components that use useCellContext + * + * These components can be used via the pre-bound cellComponents + * in AppCell children, e.g., + */ +import { useCellContext } from '../hooks/table' + +/** + * Generic text cell renderer + */ +export function TextCell() { + const cell = useCellContext() + return {cell.getValue()} +} + +/** + * Number cell with locale formatting + */ +export function NumberCell() { + const cell = useCellContext() + return {cell.getValue().toLocaleString()} +} + +/** + * Status badge cell for status column + */ +export function StatusCell() { + const cell = useCellContext<'relationship' | 'complicated' | 'single'>() + const status = cell.getValue() + return {status} +} + +/** + * Progress bar cell + */ +export function ProgressCell() { + const cell = useCellContext() + const progress = cell.getValue() + return ( +
+
+
+ ) +} + +/** + * Row actions cell - actions for the current row + */ +export function RowActionsCell() { + const cell = useCellContext() + const row = cell.row + + return ( +
+ + + +
+ ) +} + +/** + * Price cell with currency formatting + */ +export function PriceCell() { + const cell = useCellContext() + return ( + + $ + {cell.getValue().toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ) +} + +/** + * Category badge cell + */ +export function CategoryCell() { + const cell = useCellContext<'electronics' | 'clothing' | 'food' | 'books'>() + const category = cell.getValue() + return {category} +} diff --git a/examples/solid/composable-tables/src/components/header-components.tsx b/examples/solid/composable-tables/src/components/header-components.tsx new file mode 100644 index 0000000000..63394cfb77 --- /dev/null +++ b/examples/solid/composable-tables/src/components/header-components.tsx @@ -0,0 +1,76 @@ +/** + * Header-level components that use useHeaderContext + * + * These components can be used via the pre-bound headerComponents + * in AppHeader children, e.g., + */ +import { Show } from 'solid-js' +import { useHeaderContext } from '../hooks/table' + +/** + * Sort indicator showing current sort direction + */ +export function SortIndicator() { + const header = useHeaderContext() + const sorted = () => header.column.getIsSorted() + + return ( + + {(sorted) => ( + {sorted() === 'asc' ? '🔼' : '🔽'} + )} + + ) +} + +/** + * Column filter input + */ +export function ColumnFilter() { + const header = useHeaderContext() + const canFilter = () => header.column.getCanFilter() + + const columnFilterValue = () => + (header.column.getFilterValue() ?? '') as string + + return ( + +
e.stopPropagation()}> + header.column.setFilterValue(e.target.value)} + placeholder={`Filter ${header.column.id}...`} + /> +
+
+ ) +} + +/** + * Footer showing the column ID + */ +export function FooterColumnId() { + const header = useHeaderContext() + return {header.column.id} +} + +/** + * Footer showing a summary/aggregation for numeric columns + */ +export function FooterSum() { + const header = useHeaderContext() + const table = header.getContext().table + const rows = table.getFilteredRowModel().rows + + // Calculate sum for numeric columns + const sum = () => + rows.reduce((acc, row) => { + const value = row.getValue(header.column.id) + return acc + (typeof value === 'number' ? value : 0) + }, 0) + + return ( + {sum() > 0 ? sum().toLocaleString() : '—'} + ) +} diff --git a/examples/solid/composable-tables/src/components/table-components.tsx b/examples/solid/composable-tables/src/components/table-components.tsx new file mode 100644 index 0000000000..d1fd76bd3b --- /dev/null +++ b/examples/solid/composable-tables/src/components/table-components.tsx @@ -0,0 +1,116 @@ +/** + * Table-level components that use useTableContext + * + * These components can be used via the pre-bound tableComponents + * directly on the table object, e.g., + */ +import { For, createMemo } from 'solid-js' +import { useTableContext } from '../hooks/table' + +/** + * Pagination controls for the table + */ +export function PaginationControls() { + const table = useTableContext() + + const pagination = createMemo(() => table.store.state.pagination) + + return ( + + ) +} + +/** + * Row count display + */ +export function RowCount() { + const table = useTableContext() + + return ( +
+ Showing {table.getRowModel().rows.length.toLocaleString()} of{' '} + {table.getRowCount().toLocaleString()} rows +
+ ) +} + +/** + * Table toolbar with title and actions + */ +export function TableToolbar({ + title, + onRefresh, +}: { + title: string + onRefresh?: () => void +}) { + const table = useTableContext() + + return ( +
+

{title}

+
+ + + {onRefresh && } +
+
+ ) +} diff --git a/examples/solid/composable-tables/src/hooks/table.ts b/examples/solid/composable-tables/src/hooks/table.ts new file mode 100644 index 0000000000..308b20db27 --- /dev/null +++ b/examples/solid/composable-tables/src/hooks/table.ts @@ -0,0 +1,105 @@ +/** + * Custom table hook setup using createTableHook + * + * This file creates a custom useAppTable hook with pre-bound components. + * Features, row models, and default options are defined once here and shared across all tables. + * Context hooks and a pre-bound createAppColumnHelper are also exported. + */ +import { + columnFilteringFeature, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + createTableHook, + filterFns, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/solid-table' + +// Import table-level components +import { + PaginationControls, + RowCount, + TableToolbar, +} from '../components/table-components' + +// Import cell-level components +import { + CategoryCell, + NumberCell, + PriceCell, + ProgressCell, + RowActionsCell, + StatusCell, + TextCell, +} from '../components/cell-components' + +// Import header/footer-level components (both use useHeaderContext) +import { + ColumnFilter, + FooterColumnId, + FooterSum, + SortIndicator, +} from '../components/header-components' + +/** + * Create the custom table hook with all pre-bound components. + * This exports: + * - createAppColumnHelper: Create column definitions with TFeatures already bound + * - useAppTable: Hook for creating tables with TFeatures baked in + * - useTableContext: Access table instance in tableComponents + * - useCellContext: Access cell instance in cellComponents + * - useHeaderContext: Access header instance in headerComponents + */ +export const { + createAppColumnHelper, + createAppTable, + useTableContext, + useCellContext, + useHeaderContext, +} = createTableHook({ + // Features are set once here and shared across all tables + _features: tableFeatures({ + columnFilteringFeature, + rowPaginationFeature, + rowSortingFeature, + }), + + // Row models are set once here + _rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + filteredRowModel: createFilteredRowModel(filterFns), + paginatedRowModel: createPaginatedRowModel(), + }, + + // set any default table options here too + getRowId: (row) => row.id, + + // Register table-level components (accessible via table.ComponentName) + tableComponents: { + PaginationControls, + RowCount, + TableToolbar, + }, + + // Register cell-level components (accessible via cell.ComponentName in AppCell) + cellComponents: { + TextCell, + NumberCell, + StatusCell, + ProgressCell, + RowActionsCell, + PriceCell, + CategoryCell, + }, + + // Register header/footer-level components (accessible via header.ComponentName in AppHeader/AppFooter) + headerComponents: { + SortIndicator, + ColumnFilter, + FooterColumnId, + FooterSum, + }, +}) diff --git a/examples/solid/composable-tables/src/index.css b/examples/solid/composable-tables/src/index.css new file mode 100644 index 0000000000..d7a5ed6c49 --- /dev/null +++ b/examples/solid/composable-tables/src/index.css @@ -0,0 +1,249 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + width: 100%; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th, +td { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 8px 12px; + text-align: left; +} + +th { + background-color: #f5f5f5; + font-weight: 600; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +.table-container { + padding: 16px; + max-width: 1200px; + margin: 0 auto; +} + +.pagination { + display: flex; + align-items: center; + gap: 8px; + margin-top: 16px; + flex-wrap: wrap; +} + +.pagination button { + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + background: white; +} + +.pagination button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.pagination input { + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px; + width: 64px; +} + +.pagination select { + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px; +} + +.sort-indicator { + margin-left: 4px; +} + +.column-filter { + margin-top: 4px; +} + +.column-filter input { + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px; + width: 100%; + font-size: 12px; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.sortable-header:hover { + background-color: #e8e8e8; +} + +.row-actions { + display: flex; + gap: 4px; +} + +.row-actions button { + border: 1px solid #ccc; + border-radius: 4px; + padding: 2px 8px; + cursor: pointer; + background: white; + font-size: 12px; +} + +.row-actions button:hover { + background-color: #f0f0f0; +} + +.status-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.status-badge.relationship { + background-color: #d4edda; + color: #155724; +} + +.status-badge.complicated { + background-color: #fff3cd; + color: #856404; +} + +.status-badge.single { + background-color: #cce5ff; + color: #004085; +} + +.progress-bar { + width: 100%; + height: 8px; + background-color: #e9ecef; + border-radius: 4px; + overflow: hidden; +} + +.progress-bar-fill { + height: 100%; + background-color: #007bff; + transition: width 0.2s; +} + +.table-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + flex-wrap: wrap; + gap: 8px; +} + +.table-toolbar h2 { + margin: 0; +} + +.table-toolbar button { + border: 1px solid #ccc; + border-radius: 4px; + padding: 8px 16px; + cursor: pointer; + background: white; +} + +.table-toolbar button:hover { + background-color: #f0f0f0; +} + +.row-count { + color: #666; + font-size: 14px; + margin-top: 8px; +} + +.app { + padding: 16px; +} + +.app h1 { + text-align: center; + margin-bottom: 8px; +} + +.description { + text-align: center; + color: #666; + margin-bottom: 32px; +} + +.description code { + background-color: #f5f5f5; + padding: 2px 6px; + border-radius: 4px; + font-size: 13px; +} + +.table-divider { + height: 48px; + border-bottom: 1px solid #e0e0e0; + margin: 32px auto; + max-width: 1200px; +} + +.category-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + text-transform: capitalize; +} + +.category-badge.electronics { + background-color: #e3f2fd; + color: #1565c0; +} + +.category-badge.clothing { + background-color: #fce4ec; + color: #c2185b; +} + +.category-badge.food { + background-color: #e8f5e9; + color: #2e7d32; +} + +.category-badge.books { + background-color: #fff8e1; + color: #f57c00; +} + +.price { + font-weight: 600; + color: #2e7d32; +} diff --git a/examples/solid/composable-tables/src/index.tsx b/examples/solid/composable-tables/src/index.tsx new file mode 100644 index 0000000000..ea9c952177 --- /dev/null +++ b/examples/solid/composable-tables/src/index.tsx @@ -0,0 +1,6 @@ +/* @refresh reload */ +import { render } from 'solid-js/web' +import './index.css' +import App from './App' + +render(() => , document.getElementById('root') as HTMLElement) diff --git a/examples/solid/composable-tables/src/makeData.ts b/examples/solid/composable-tables/src/makeData.ts new file mode 100644 index 0000000000..17dec1c6de --- /dev/null +++ b/examples/solid/composable-tables/src/makeData.ts @@ -0,0 +1,77 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +export type Product = { + id: string + name: string + category: 'electronics' | 'clothing' | 'food' | 'books' + price: number + stock: number + rating: number +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +const newProduct = (): Product => { + return { + id: faker.string.uuid(), + name: faker.commerce.productName(), + category: faker.helpers.shuffle([ + 'electronics', + 'clothing', + 'food', + 'books', + ])[0], + price: parseFloat(faker.commerce.price({ min: 5, max: 500 })), + stock: faker.number.int({ min: 0, max: 200 }), + rating: faker.number.int({ min: 0, max: 100 }), + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} + +export function makeProductData(count: number): Array { + return range(count).map(() => newProduct()) +} diff --git a/examples/solid/composable-tables/tsconfig.json b/examples/solid/composable-tables/tsconfig.json new file mode 100644 index 0000000000..7ab027b8d2 --- /dev/null +++ b/examples/solid/composable-tables/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/solid/composable-tables/vite.config.ts b/examples/solid/composable-tables/vite.config.ts new file mode 100644 index 0000000000..d27427972d --- /dev/null +++ b/examples/solid/composable-tables/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import solidPlugin from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [solidPlugin()], + build: { + target: 'esnext', + polyfillDynamicImport: false, + }, +}) diff --git a/packages/solid-table/src/createTableHook.tsx b/packages/solid-table/src/createTableHook.tsx index f9c36902b3..a5dae0699b 100644 --- a/packages/solid-table/src/createTableHook.tsx +++ b/packages/solid-table/src/createTableHook.tsx @@ -1,5 +1,5 @@ import { createColumnHelper as coreCreateColumnHelper } from '@tanstack/table-core' -import { createContext, createMemo, mergeProps, useContext } from 'solid-js' +import { Show, createContext, mergeProps, useContext } from 'solid-js' import { createTable } from './createTable' import { FlexRender } from './FlexRender' import type { SolidTable } from './createTable' @@ -250,7 +250,7 @@ export type CreateTableHookOptions< > & { /** * Table-level components that need access to the table instance. - * These are available directly on the table object returned by useAppTable. + * These are available directly on the table object returned by createAppTable. * Use `useTableContext()` inside these components. * @example { PaginationControls, GlobalFilter, RowCount } */ @@ -286,7 +286,7 @@ export interface AppTablePropsWithSelector< TFeatures extends TableFeatures, TSelected, > { - children: (state: TSelected) => JSXElement + children: (state: Accessor) => JSXElement selector: (state: TableState) => TSelected } @@ -427,7 +427,7 @@ export interface AppTableComponent { } /** - * Extended table API returned by useAppTable with all App wrapper components + * Extended table API returned by createAppTable with all App wrapper components */ export type AppSolidTable< TFeatures extends TableFeatures, @@ -499,6 +499,17 @@ export type AppSolidTable< * ``` */ AppFooter: AppHeaderComponent> + /** + * Convenience FlexRender component attached to the table instance. + * Renders cell, header, or footer content from column definitions. + * @example + * ```tsx + * + * + * + * ``` + */ + FlexRender: typeof FlexRender } /** @@ -508,14 +519,14 @@ export type AppSolidTable< * - 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 `useAppTable` hook that returns an extended table with App wrapper 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 { - * useAppTable, + * createAppTable, * createAppColumnHelper, * useTableContext, * useCellContext, @@ -547,7 +558,7 @@ export type AppSolidTable< * * // features/users.tsx * function UsersTable({ data }: { data: Person[] }) { - * const table = useAppTable({ + * const table = createAppTable({ * columns, * data, // TData inferred from Person[] * }) @@ -556,31 +567,39 @@ export type AppSolidTable< * * * - * {table.getHeaderGroups().map(headerGroup => ( - * - * {headerGroup.headers.map(h => ( - * - * {(header) => ( - * + * + * {(headerGroup) => ( + * + * + * {(h) => ( + * + * {(header) => ( + * + * )} + * * )} - * - * ))} - * - * ))} + * + * + * )} + * * * - * {table.getRowModel().rows.map(row => ( - * - * {row.getAllCells().map(c => ( - * - * {(cell) => } - * - * ))} - * - * ))} + * + * {(row) => ( + * + * + * {(c) => ( + * + * {(cell) => } + * + * )} + * + * + * )} + * * *
- * - * - *
+ * + * + *
* @@ -822,238 +841,217 @@ export function createTableHook< selector, ) - // AppTable - Root wrapper that provides table context with optional Subscribe - const AppTable = createMemo(() => { - function AppTableImpl(props: AppTablePropsWithoutSelector): JSXElement - function AppTableImpl( - props: AppTablePropsWithSelector, - ): JSXElement - function AppTableImpl( - props: - | AppTablePropsWithoutSelector - | AppTablePropsWithSelector, - ): JSXElement { - const { children, selector: appTableSelector } = props as any - - return ( - - {appTableSelector ? ( - - {(state: Accessor) => children(state)} + // AppTable - Root wrapper that provides table context with optional state selector + function AppTable(props: AppTablePropsWithoutSelector): JSXElement + function AppTable( + props: AppTablePropsWithSelector, + ): JSXElement + function AppTable( + props: + | AppTablePropsWithoutSelector + | AppTablePropsWithSelector, + ): JSXElement { + return ( + + + {(selector) => ( + + {(state: Accessor) => + (props.children as (state: Accessor) => JSXElement)(state) + } - ) : ( - children )} - - ) - } - return AppTableImpl as AppTableComponent - }, [table]) + + + ) + } - // AppCell - Wraps cell with context, pre-bound cellComponents, and optional Subscribe - const AppCell = createMemo(() => { - function AppCellImpl( - props: AppCellPropsWithoutSelector< - TFeatures, - TData, - TValue, - TCellComponents - >, - ): JSXElement - function AppCellImpl< - TValue extends CellData = CellData, - TAppCellSelected = unknown, - >( - props: AppCellPropsWithSelector< - TFeatures, - TData, - TValue, - TCellComponents, - TAppCellSelected - >, - ): JSXElement - function AppCellImpl< - TValue extends CellData = CellData, - TAppCellSelected = unknown, - >( - props: - | AppCellPropsWithoutSelector< - TFeatures, - TData, - TValue, - TCellComponents - > - | AppCellPropsWithSelector< - TFeatures, - TData, - TValue, - TCellComponents, - TAppCellSelected - >, - ): JSXElement { - const { cell, children, selector: appCellSelector } = props as any - const extendedCell = Object.assign(cell, { - FlexRender: CellFlexRender, - ...cellComponents, - }) + // AppCell - Wraps cell with context, pre-bound cellComponents, and optional state selector + function AppCell( + props: AppCellPropsWithoutSelector< + TFeatures, + TData, + TValue, + TCellComponents + >, + ): JSXElement + function AppCell< + TValue extends CellData = CellData, + TAppCellSelected = unknown, + >( + props: AppCellPropsWithSelector< + TFeatures, + TData, + TValue, + TCellComponents, + TAppCellSelected + >, + ): JSXElement + function AppCell< + TValue extends CellData = CellData, + TAppCellSelected = unknown, + >( + props: + | AppCellPropsWithoutSelector + | AppCellPropsWithSelector< + TFeatures, + TData, + TValue, + TCellComponents, + TAppCellSelected + >, + ): JSXElement { + const extendedCell = Object.assign(props.cell, { + FlexRender: CellFlexRender, + ...cellComponents, + }) as Cell & + TCellComponents & { FlexRender: () => JSXElement } - return ( - - {appCellSelector ? ( - + return ( + + JSXElement)(extendedCell as any)} + > + {(selector) => ( + {(state: Accessor) => - children(extendedCell, state) + props.children(extendedCell as any, state) } - ) : ( - children(extendedCell) )} - - ) - } - return AppCellImpl as AppCellComponent - }, [table]) + + + ) + } - // AppHeader - Wraps header with context, pre-bound headerComponents, and optional Subscribe - const AppHeader = createMemo(() => { - function AppHeaderImpl( - props: AppHeaderPropsWithoutSelector< - TFeatures, - TData, - TValue, - THeaderComponents - >, - ): JSXElement - function AppHeaderImpl< - TValue extends CellData = CellData, - TAppHeaderSelected = unknown, - >( - props: AppHeaderPropsWithSelector< - TFeatures, - TData, - TValue, - THeaderComponents, - TAppHeaderSelected - >, - ): JSXElement - function AppHeaderImpl< - TValue extends CellData = CellData, - TAppHeaderSelected = unknown, - >( - props: - | AppHeaderPropsWithoutSelector< - TFeatures, - TData, - TValue, - THeaderComponents - > - | AppHeaderPropsWithSelector< - TFeatures, - TData, - TValue, - THeaderComponents, - TAppHeaderSelected - >, - ): JSXElement { - const { header, children, selector: appHeaderSelector } = props as any - const extendedHeader = Object.assign(header, { - FlexRender: HeaderFlexRender, - ...headerComponents, - }) + // AppHeader - Wraps header with context, pre-bound headerComponents, and optional state selector + function AppHeader( + props: AppHeaderPropsWithoutSelector< + TFeatures, + TData, + TValue, + THeaderComponents + >, + ): JSXElement + function AppHeader< + TValue extends CellData = CellData, + TAppHeaderSelected = unknown, + >( + props: AppHeaderPropsWithSelector< + TFeatures, + TData, + TValue, + THeaderComponents, + TAppHeaderSelected + >, + ): JSXElement + function AppHeader< + TValue extends CellData = CellData, + TAppHeaderSelected = unknown, + >( + props: + | AppHeaderPropsWithoutSelector< + TFeatures, + TData, + TValue, + THeaderComponents + > + | AppHeaderPropsWithSelector< + TFeatures, + TData, + TValue, + THeaderComponents, + TAppHeaderSelected + >, + ): JSXElement { + const extendedHeader = Object.assign(props.header, { + FlexRender: HeaderFlexRender, + ...headerComponents, + }) as Header & + THeaderComponents & { FlexRender: () => JSXElement } - return ( - - {appHeaderSelector ? ( - + return ( + + JSXElement)(extendedHeader as any)} + > + {(selector) => ( + {(state: Accessor) => - children(extendedHeader, state) + props.children(extendedHeader as any, state) } - ) : ( - children(extendedHeader) )} - - ) - } - return AppHeaderImpl as AppHeaderComponent< + + + ) + } + + // AppFooter - Same as AppHeader but uses FooterFlexRender (footers use Header type) + function AppFooter( + props: AppHeaderPropsWithoutSelector< TFeatures, TData, + TValue, THeaderComponents - > - }, [table]) - - // AppFooter - Same as AppHeader (footers use Header type) - const AppFooter = createMemo(() => { - function AppFooterImpl( - props: AppHeaderPropsWithoutSelector< - TFeatures, - TData, - TValue, - THeaderComponents - >, - ): JSXElement - function AppFooterImpl< - TValue extends CellData = CellData, - TAppFooterSelected = unknown, - >( - props: AppHeaderPropsWithSelector< - TFeatures, - TData, - TValue, - THeaderComponents, - TAppFooterSelected - >, - ): JSXElement - function AppFooterImpl< - TValue extends CellData = CellData, - TAppFooterSelected = unknown, - >( - props: - | AppHeaderPropsWithoutSelector< - TFeatures, - TData, - TValue, - THeaderComponents - > - | AppHeaderPropsWithSelector< - TFeatures, - TData, - TValue, - THeaderComponents, - TAppFooterSelected - >, - ): JSXElement { - const { header, children, selector: appFooterSelector } = props as any - const extendedHeader = Object.assign(header, { - FlexRender: FooterFlexRender, - ...headerComponents, - }) + >, + ): JSXElement + function AppFooter< + TValue extends CellData = CellData, + TAppFooterSelected = unknown, + >( + props: AppHeaderPropsWithSelector< + TFeatures, + TData, + TValue, + THeaderComponents, + TAppFooterSelected + >, + ): JSXElement + function AppFooter< + TValue extends CellData = CellData, + TAppFooterSelected = unknown, + >( + props: + | AppHeaderPropsWithoutSelector< + TFeatures, + TData, + TValue, + THeaderComponents + > + | AppHeaderPropsWithSelector< + TFeatures, + TData, + TValue, + THeaderComponents, + TAppFooterSelected + >, + ): JSXElement { + const extendedHeader = Object.assign(props.header, { + FlexRender: FooterFlexRender, + ...headerComponents, + }) as Header & + THeaderComponents & { FlexRender: () => JSXElement } - return ( - - {appFooterSelector ? ( - + return ( + + JSXElement)(extendedHeader as any)} + > + {(selector) => ( + {(state: Accessor) => - children(extendedHeader, state) + props.children(extendedHeader as any, state) } - ) : ( - ( - children as ( - header: Header & - THeaderComponents & { FlexRender: () => JSXElement }, - ) => JSXElement - )(extendedHeader) )} - - ) - } - return AppFooterImpl as AppHeaderComponent< - TFeatures, - TData, - THeaderComponents - > - }, [table]) + + + ) + } // Combine everything into the extended table API return Object.assign(table, { @@ -1061,6 +1059,7 @@ export function createTableHook< AppCell, AppHeader, AppFooter, + FlexRender, ...tableComponents, }) as AppSolidTable< TFeatures, @@ -1075,7 +1074,7 @@ export function createTableHook< return { appFeatures: defaultTableOptions._features as TFeatures, createAppColumnHelper, - useAppTable: createAppTable, + createAppTable: createAppTable, useTableContext, useCellContext, useHeaderContext, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21fc78aedb..a75f1f4f68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2647,6 +2647,25 @@ importers: specifier: ^2.11.10 version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2)) + examples/solid/basic-app-table: + dependencies: + '@tanstack/solid-table': + specifier: ^9.0.0-alpha.10 + version: link:../../../packages/solid-table + solid-js: + specifier: ^1.9.11 + version: 1.9.11 + devDependencies: + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2) + vite-plugin-solid: + specifier: ^2.11.10 + version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2)) + examples/solid/basic-table-helper: dependencies: '@tanstack/solid-table': @@ -2726,6 +2745,25 @@ importers: specifier: ^2.11.10 version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2)) + examples/solid/composable-tables: + dependencies: + '@tanstack/solid-table': + specifier: ^9.0.0-alpha.10 + version: link:../../../packages/solid-table + solid-js: + specifier: ^1.9.11 + version: 1.9.11 + devDependencies: + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2) + vite-plugin-solid: + specifier: ^2.11.10 + version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2)) + examples/solid/filters: dependencies: '@solid-primitives/scheduled': From 9f5f16db1e3713499b22e06b11636a8ffbb84c85 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:44:41 +0000 Subject: [PATCH 3/4] ci: apply automated fixes --- examples/solid/composable-tables/src/App.tsx | 493 ++++++++++--------- packages/solid-table/src/createTableHook.tsx | 18 +- 2 files changed, 262 insertions(+), 249 deletions(-) diff --git a/examples/solid/composable-tables/src/App.tsx b/examples/solid/composable-tables/src/App.tsx index c7c64bd112..8b220922fc 100644 --- a/examples/solid/composable-tables/src/App.tsx +++ b/examples/solid/composable-tables/src/App.tsx @@ -83,136 +83,136 @@ function UsersTable() { const sorting = () => state().sorting const columnFilters = () => state().columnFilters return ( -
- {/* Table toolbar using pre-bound component */} - +
+ {/* Table toolbar using pre-bound component */} + - {/* Table element */} - - - - {(headerGroup) => ( - - - {(h) => ( - - {(header) => ( - - )} - - )} - - - )} - - - - - {(row) => ( - - - {(c) => ( - - {(cell) => ( - - )} - - )} - - - )} - - - - - {(footerGroup) => ( - - - {(f) => ( - - {(footer) => { - const columnId = footer.column.id - const hasFilter = () => - columnFilters().some((cf) => cf.id === columnId) - - return ( - + )} + + + + + {(footerGroup) => ( + + + {(f) => ( + + {(footer) => { + const columnId = footer.column.id + const hasFilter = () => + columnFilters().some((cf) => cf.id === columnId) + + return ( + + ) + }} + + )} + + + )} + + +
- {header.isPlaceholder ? null : ( - <> - - - - {/* Show sort order number when multiple columns sorted */} - {sorting().length > 1 && - sorting().findIndex( - (s) => s.id === header.column.id, - ) > -1 && ( - - {sorting().findIndex( - (s) => s.id === header.column.id, - ) + 1} - - )} - - )} -
- {/* Cell components are pre-bound via AppCell */} - -
- {footer.isPlaceholder ? null : ( + {/* Table element */} + + + + {(headerGroup) => ( + + + {(h) => ( + + {(header) => ( + + )} + + )} + + + )} + + + + + {(row) => ( + + + {(c) => ( + + {(cell) => ( + - ) - }} - - )} - - - )} - - -
+ {header.isPlaceholder ? null : ( <> - {/* Use FooterSum for numeric columns, FooterColumnId for others */} - {columnId === 'age' || - columnId === 'visits' || - columnId === 'progress' ? ( - <> - - {hasFilter() && ( - - {' '} - (filtered) - - )} - - ) : columnId === 'actions' ? null : ( - <> - - {hasFilter() && ( - - {' '} - ✓ - - )} - - )} + + + + {/* Show sort order number when multiple columns sorted */} + {sorting().length > 1 && + sorting().findIndex( + (s) => s.id === header.column.id, + ) > -1 && ( + + {sorting().findIndex( + (s) => s.id === header.column.id, + ) + 1} + + )} )} +
+ {/* Cell components are pre-bound via AppCell */} +
+ )} + + )} + +
+ {footer.isPlaceholder ? null : ( + <> + {/* Use FooterSum for numeric columns, FooterColumnId for others */} + {columnId === 'age' || + columnId === 'visits' || + columnId === 'progress' ? ( + <> + + {hasFilter() && ( + + {' '} + (filtered) + + )} + + ) : columnId === 'actions' ? null : ( + <> + + {hasFilter() && ( + + {' '} + ✓ + + )} + + )} + + )} +
- {/* Pagination using pre-bound component */} - + {/* Pagination using pre-bound component */} + - {/* Row count using pre-bound component */} - -
+ {/* Row count using pre-bound component */} + +
) }}
@@ -279,135 +279,138 @@ function ProductsTable() { const sorting = () => state().sorting const columnFilters = () => state().columnFilters return ( -
- {/* Table toolbar using the same pre-bound component */} - +
+ {/* Table toolbar using the same pre-bound component */} + - {/* Table element */} - - - - {(headerGroup) => ( - - - {(h) => ( - - {(header) => ( - - )} - - )} - - - )} - - - - - {(row) => ( - - - {(c) => ( - - {(cell) => ( - - )} - - )} - - - )} - - - - - {(footerGroup) => ( - - - {(f) => ( - - {(footer) => { - const columnId = footer.column.id - const hasFilter = () => - columnFilters().some((cf) => cf.id === columnId) - - return ( - + )} + + + + + {(footerGroup) => ( + + + {(f) => ( + + {(footer) => { + const columnId = footer.column.id + const hasFilter = () => + columnFilters().some((cf) => cf.id === columnId) + + return ( + + ) + }} + + )} + + + )} + + +
- {header.isPlaceholder ? null : ( - <> - - - - {sorting().length > 1 && - sorting().findIndex( - (s) => s.id === header.column.id, - ) > -1 && ( - - {sorting().findIndex( - (s) => s.id === header.column.id, - ) + 1} - - )} - - )} -
- {/* Cell components are pre-bound via AppCell */} - -
- {footer.isPlaceholder ? null : ( + {/* Table element */} + + + + {(headerGroup) => ( + + + {(h) => ( + + {(header) => ( + + )} + + )} + + + )} + + + + + {(row) => ( + + + {(c) => ( + + {(cell) => ( + - ) - }} - - )} - - - )} - - -
+ {header.isPlaceholder ? null : ( <> - {/* Use FooterSum for numeric columns, FooterColumnId for others */} - {columnId === 'price' || - columnId === 'stock' || - columnId === 'rating' ? ( - <> - - {hasFilter() && ( - - {' '} - (filtered) - - )} - - ) : ( - <> - - {hasFilter() && ( - - {' '} - ✓ - - )} - - )} + + + + {sorting().length > 1 && + sorting().findIndex( + (s) => s.id === header.column.id, + ) > -1 && ( + + {sorting().findIndex( + (s) => s.id === header.column.id, + ) + 1} + + )} )} +
+ {/* Cell components are pre-bound via AppCell */} +
+ )} + + )} + +
+ {footer.isPlaceholder ? null : ( + <> + {/* Use FooterSum for numeric columns, FooterColumnId for others */} + {columnId === 'price' || + columnId === 'stock' || + columnId === 'rating' ? ( + <> + + {hasFilter() && ( + + {' '} + (filtered) + + )} + + ) : ( + <> + + {hasFilter() && ( + + {' '} + ✓ + + )} + + )} + + )} +
- {/* Pagination using the same pre-bound component */} - + {/* Pagination using the same pre-bound component */} + - {/* Row count using the same pre-bound component */} - -
+ {/* Row count using the same pre-bound component */} + +
) }} diff --git a/packages/solid-table/src/createTableHook.tsx b/packages/solid-table/src/createTableHook.tsx index a5dae0699b..6bfe3e9597 100644 --- a/packages/solid-table/src/createTableHook.tsx +++ b/packages/solid-table/src/createTableHook.tsx @@ -857,7 +857,11 @@ export function createTableHook< {(selector) => ( {(state: Accessor) => - (props.children as (state: Accessor) => JSXElement)(state) + ( + props.children as ( + state: Accessor, + ) => JSXElement + )(state) } )} @@ -911,7 +915,9 @@ export function createTableHook< JSXElement)(extendedCell as any)} + fallback={(props.children as (cell: any) => JSXElement)( + extendedCell as any, + )} > {(selector) => ( @@ -975,7 +981,9 @@ export function createTableHook< JSXElement)(extendedHeader as any)} + fallback={(props.children as (header: any) => JSXElement)( + extendedHeader as any, + )} > {(selector) => ( @@ -1039,7 +1047,9 @@ export function createTableHook< JSXElement)(extendedHeader as any)} + fallback={(props.children as (header: any) => JSXElement)( + extendedHeader as any, + )} > {(selector) => ( From 3ae6f62fb42764634d9da32c1f0bdf26877312f6 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 9 Mar 2026 22:06:03 +0100 Subject: [PATCH 4/4] chore(solid): add new table examples Add new examples: - basic-external-state - basic-external-stpore - basic-use-table - with-tanstack-query Update config.json to render new solid table examples --- docs/config.json | 10 +- .../.gitignore | 0 .../README.md | 0 .../index.html | 0 .../package.json | 3 +- .../solid/basic-external-state/src/App.tsx | 207 +++++++++++++++++ .../src/index.css | 0 .../src/index.tsx | 0 .../basic-external-state/src/makeData.ts | 48 ++++ .../tsconfig.json | 0 .../vite.config.ts | 0 .../.gitignore | 0 .../{basic => basic-external-store}/README.md | 0 .../index.html | 0 .../solid/basic-external-store/package.json | 23 ++ .../solid/basic-external-store/src/App.tsx | 214 ++++++++++++++++++ .../src/index.css | 0 .../src/index.tsx | 0 .../basic-external-store/src/makeData.ts | 48 ++++ .../tsconfig.json | 0 .../vite.config.ts | 0 examples/solid/basic-table-helper/src/App.tsx | 177 --------------- examples/solid/basic-use-table/.gitignore | 2 + examples/solid/basic-use-table/README.md | 6 + examples/solid/basic-use-table/index.html | 16 ++ .../{basic => basic-use-table}/package.json | 2 +- .../{basic => basic-use-table}/src/App.tsx | 46 ++-- examples/solid/basic-use-table/src/index.css | 26 +++ examples/solid/basic-use-table/src/index.tsx | 6 + examples/solid/basic-use-table/tsconfig.json | 25 ++ examples/solid/basic-use-table/vite.config.ts | 10 + examples/solid/with-tanstack-query/.gitignore | 2 + examples/solid/with-tanstack-query/README.md | 6 + examples/solid/with-tanstack-query/index.html | 16 ++ .../solid/with-tanstack-query/package.json | 25 ++ .../solid/with-tanstack-query/src/App.tsx | 193 ++++++++++++++++ .../with-tanstack-query/src/fetchData.ts | 66 ++++++ .../solid/with-tanstack-query/src/index.css | 26 +++ .../solid/with-tanstack-query/src/index.tsx | 15 ++ .../solid/with-tanstack-query/tsconfig.json | 25 ++ .../solid/with-tanstack-query/vite.config.ts | 9 + pnpm-lock.yaml | 84 ++++++- 42 files changed, 1117 insertions(+), 219 deletions(-) rename examples/solid/{basic-table-helper => basic-external-state}/.gitignore (100%) rename examples/solid/{basic-table-helper => basic-external-state}/README.md (100%) rename examples/solid/{basic-table-helper => basic-external-state}/index.html (100%) rename examples/solid/{basic-table-helper => basic-external-state}/package.json (81%) create mode 100644 examples/solid/basic-external-state/src/App.tsx rename examples/solid/{basic-table-helper => basic-external-state}/src/index.css (100%) rename examples/solid/{basic-table-helper => basic-external-state}/src/index.tsx (100%) create mode 100644 examples/solid/basic-external-state/src/makeData.ts rename examples/solid/{basic-table-helper => basic-external-state}/tsconfig.json (100%) rename examples/solid/{basic-table-helper => basic-external-state}/vite.config.ts (100%) rename examples/solid/{basic => basic-external-store}/.gitignore (100%) rename examples/solid/{basic => basic-external-store}/README.md (100%) rename examples/solid/{basic => basic-external-store}/index.html (100%) create mode 100644 examples/solid/basic-external-store/package.json create mode 100644 examples/solid/basic-external-store/src/App.tsx rename examples/solid/{basic => basic-external-store}/src/index.css (100%) rename examples/solid/{basic => basic-external-store}/src/index.tsx (100%) create mode 100644 examples/solid/basic-external-store/src/makeData.ts rename examples/solid/{basic => basic-external-store}/tsconfig.json (100%) rename examples/solid/{basic => basic-external-store}/vite.config.ts (100%) delete mode 100644 examples/solid/basic-table-helper/src/App.tsx create mode 100644 examples/solid/basic-use-table/.gitignore create mode 100644 examples/solid/basic-use-table/README.md create mode 100644 examples/solid/basic-use-table/index.html rename examples/solid/{basic => basic-use-table}/package.json (87%) rename examples/solid/{basic => basic-use-table}/src/App.tsx (65%) create mode 100644 examples/solid/basic-use-table/src/index.css create mode 100644 examples/solid/basic-use-table/src/index.tsx create mode 100644 examples/solid/basic-use-table/tsconfig.json create mode 100644 examples/solid/basic-use-table/vite.config.ts create mode 100644 examples/solid/with-tanstack-query/.gitignore create mode 100644 examples/solid/with-tanstack-query/README.md create mode 100644 examples/solid/with-tanstack-query/index.html create mode 100644 examples/solid/with-tanstack-query/package.json create mode 100644 examples/solid/with-tanstack-query/src/App.tsx create mode 100644 examples/solid/with-tanstack-query/src/fetchData.ts create mode 100644 examples/solid/with-tanstack-query/src/index.css create mode 100644 examples/solid/with-tanstack-query/src/index.tsx create mode 100644 examples/solid/with-tanstack-query/tsconfig.json create mode 100644 examples/solid/with-tanstack-query/vite.config.ts diff --git a/docs/config.json b/docs/config.json index 637337e213..3f534f5089 100644 --- a/docs/config.json +++ b/docs/config.json @@ -822,9 +822,10 @@ { "label": "solid", "children": [ - { "to": "framework/solid/examples/basic", "label": "Basic" }, - { "to": "framework/solid/examples/basic-app-table", "label": "Basic (useAppTable)" }, - { "to": "framework/solid/examples/basic-table-helper", "label": "Basic with Helpers" }, + { "to": "framework/solid/examples/basic-use-table", "label": "Basic (createTable)" }, + { "to": "framework/solid/examples/basic-app-table", "label": "Basic (createAppTable)" }, + { "to": "framework/solid/examples/basic-external-state", "label": "Basic (External State)" }, + { "to": "framework/solid/examples/basic-external-store", "label": "Basic (External Store)" }, { "to": "framework/solid/examples/column-groups", "label": "Header Groups" } ] }, @@ -1001,7 +1002,8 @@ "label": "solid", "children": [ { "to": "framework/solid/examples/bootstrap", "label": "Solid Bootstrap" }, - { "to": "framework/solid/examples/composable-tables", "label": "Composable Tables" }] + { "to": "framework/solid/examples/composable-tables", "label": "Composable Tables" }, + { "to": "framework/solid/examples/with-tanstack-query", "label": "With TanStack Query" }] }, { "label": "vue", diff --git a/examples/solid/basic-table-helper/.gitignore b/examples/solid/basic-external-state/.gitignore similarity index 100% rename from examples/solid/basic-table-helper/.gitignore rename to examples/solid/basic-external-state/.gitignore diff --git a/examples/solid/basic-table-helper/README.md b/examples/solid/basic-external-state/README.md similarity index 100% rename from examples/solid/basic-table-helper/README.md rename to examples/solid/basic-external-state/README.md diff --git a/examples/solid/basic-table-helper/index.html b/examples/solid/basic-external-state/index.html similarity index 100% rename from examples/solid/basic-table-helper/index.html rename to examples/solid/basic-external-state/index.html diff --git a/examples/solid/basic-table-helper/package.json b/examples/solid/basic-external-state/package.json similarity index 81% rename from examples/solid/basic-table-helper/package.json rename to examples/solid/basic-external-state/package.json index 75e21a3eda..b03083408c 100644 --- a/examples/solid/basic-table-helper/package.json +++ b/examples/solid/basic-external-state/package.json @@ -1,5 +1,5 @@ { - "name": "tanstack-table-example-solid-basic-table-helper", + "name": "tanstack-table-example-solid-basic-external-state", "version": "0.0.0", "description": "", "scripts": { @@ -11,6 +11,7 @@ }, "license": "MIT", "devDependencies": { + "@faker-js/faker": "^10.2.0", "typescript": "5.9.3", "vite": "^7.3.1", "vite-plugin-solid": "^2.11.10" diff --git a/examples/solid/basic-external-state/src/App.tsx b/examples/solid/basic-external-state/src/App.tsx new file mode 100644 index 0000000000..5e43930fe9 --- /dev/null +++ b/examples/solid/basic-external-state/src/App.tsx @@ -0,0 +1,207 @@ +import { + createColumnHelper, + createPaginatedRowModel, + createSortedRowModel, + flexRender, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, + createTable, +} from '@tanstack/solid-table' +import { For, createSignal } from 'solid-js' +import { makeData } from './makeData' +import type { PaginationState, SortingState } from '@tanstack/solid-table' +import type { Person } from './makeData' + +const _features = tableFeatures({ + rowPaginationFeature, + rowSortingFeature, +}) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('lastName', { + header: 'Last Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { + header: 'Age', + }), + columnHelper.accessor('visits', { + header: 'Visits', + }), + columnHelper.accessor('status', { + header: 'Status', + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + }), +]) + +function App() { + const [data] = createSignal(makeData(1000)) + const [sorting, setSorting] = createSignal([]) + const [pagination, setPagination] = createSignal({ + pageIndex: 0, + pageSize: 10, + }) + + const table = createTable({ + _features, + _rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + get data() { + return data() + }, + state: { + get sorting() { + return sorting() + }, + get pagination() { + return pagination() + }, + }, + onSortingChange: setSorting, + onPaginationChange: setPagination, + }) + + return ( +
+ + + + {(headerGroup) => ( + + + {(header) => ( + + )} + + + )} + + + + + {(row) => ( + + + {(cell) => ( + + )} + + + )} + + +
+ {header.isPlaceholder ? null : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} +
+ )} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} +
+
+
+ + + + + +
Page
+ + {pagination().pageIndex + 1} of {table.getPageCount()} + +
+ + | Go to page: + { + const page = e.currentTarget.value + ? Number(e.currentTarget.value) - 1 + : 0 + table.setPageIndex(page) + }} + class="border p-1 rounded w-16" + /> + + +
+
+
+        {JSON.stringify(
+          { sorting: sorting(), pagination: pagination() },
+          null,
+          2,
+        )}
+      
+
+ ) +} + +export default App diff --git a/examples/solid/basic-table-helper/src/index.css b/examples/solid/basic-external-state/src/index.css similarity index 100% rename from examples/solid/basic-table-helper/src/index.css rename to examples/solid/basic-external-state/src/index.css diff --git a/examples/solid/basic-table-helper/src/index.tsx b/examples/solid/basic-external-state/src/index.tsx similarity index 100% rename from examples/solid/basic-table-helper/src/index.tsx rename to examples/solid/basic-external-state/src/index.tsx diff --git a/examples/solid/basic-external-state/src/makeData.ts b/examples/solid/basic-external-state/src/makeData.ts new file mode 100644 index 0000000000..6311127267 --- /dev/null +++ b/examples/solid/basic-external-state/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/solid/basic-table-helper/tsconfig.json b/examples/solid/basic-external-state/tsconfig.json similarity index 100% rename from examples/solid/basic-table-helper/tsconfig.json rename to examples/solid/basic-external-state/tsconfig.json diff --git a/examples/solid/basic-table-helper/vite.config.ts b/examples/solid/basic-external-state/vite.config.ts similarity index 100% rename from examples/solid/basic-table-helper/vite.config.ts rename to examples/solid/basic-external-state/vite.config.ts diff --git a/examples/solid/basic/.gitignore b/examples/solid/basic-external-store/.gitignore similarity index 100% rename from examples/solid/basic/.gitignore rename to examples/solid/basic-external-store/.gitignore diff --git a/examples/solid/basic/README.md b/examples/solid/basic-external-store/README.md similarity index 100% rename from examples/solid/basic/README.md rename to examples/solid/basic-external-store/README.md diff --git a/examples/solid/basic/index.html b/examples/solid/basic-external-store/index.html similarity index 100% rename from examples/solid/basic/index.html rename to examples/solid/basic-external-store/index.html diff --git a/examples/solid/basic-external-store/package.json b/examples/solid/basic-external-store/package.json new file mode 100644 index 0000000000..8560c39cd4 --- /dev/null +++ b/examples/solid/basic-external-store/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-table-example-solid-basic-external-store", + "version": "0.0.0", + "description": "", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "lint": "eslint ./src" + }, + "license": "MIT", + "devDependencies": { + "@faker-js/faker": "^10.2.0", + "typescript": "5.9.3", + "vite": "^7.3.1", + "vite-plugin-solid": "^2.11.10" + }, + "dependencies": { + "@tanstack/solid-table": "^9.0.0-alpha.10", + "solid-js": "^1.9.11" + } +} diff --git a/examples/solid/basic-external-store/src/App.tsx b/examples/solid/basic-external-store/src/App.tsx new file mode 100644 index 0000000000..de969cbfc5 --- /dev/null +++ b/examples/solid/basic-external-store/src/App.tsx @@ -0,0 +1,214 @@ +import { + createColumnHelper, + createPaginatedRowModel, + createSortedRowModel, + createTable, + flexRender, + getInitialTableState, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/solid-table' +import { For, createSignal } from 'solid-js' +import { makeData } from './makeData' +import type { Person } from './makeData' +import type { PaginationState, SortingState } from '@tanstack/solid-table' + +const _features = tableFeatures({ + rowPaginationFeature, + rowSortingFeature, +}) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('lastName', { + header: 'Last Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { + header: 'Age', + }), + columnHelper.accessor('visits', { + header: 'Visits', + }), + columnHelper.accessor('status', { + header: 'Status', + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + }), +]) + +function App() { + const [data] = createSignal(makeData(1000)) + const [tableState, setTableState] = createSignal( + getInitialTableState(_features, { + sorting: [] as SortingState, + pagination: { pageIndex: 0, pageSize: 10 } as PaginationState, + }), + ) + + const table = createTable({ + _features, + _rowModels: { + sortedRowModel: createSortedRowModel(sortFns), + paginatedRowModel: createPaginatedRowModel(), + }, + columns, + get data() { + return data() + }, + state: { + get sorting() { + return tableState().sorting + }, + get pagination() { + return tableState().pagination + }, + }, + onSortingChange: (updater) => + setTableState((prev) => ({ + ...prev, + sorting: + typeof updater === 'function' ? updater(prev.sorting) : updater, + })), + onPaginationChange: (updater) => + setTableState((prev) => ({ + ...prev, + pagination: + typeof updater === 'function' ? updater(prev.pagination) : updater, + })), + debugTable: true, + }) + + return ( +
+ + + + {(headerGroup) => ( + + + {(header) => ( + + )} + + + )} + + + + + {(row) => ( + + + {(cell) => ( + + )} + + + )} + + +
+ {header.isPlaceholder ? null : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} +
+ )} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} +
+
+
+ + + + + +
Page
+ + {tableState().pagination.pageIndex + 1} of {table.getPageCount()} + +
+ + | Go to page: + { + const page = e.currentTarget.value + ? Number(e.currentTarget.value) - 1 + : 0 + table.setPageIndex(page) + }} + class="border p-1 rounded w-16" + /> + + +
+
+
{JSON.stringify(tableState(), null, 2)}
+
+ ) +} + +export default App diff --git a/examples/solid/basic/src/index.css b/examples/solid/basic-external-store/src/index.css similarity index 100% rename from examples/solid/basic/src/index.css rename to examples/solid/basic-external-store/src/index.css diff --git a/examples/solid/basic/src/index.tsx b/examples/solid/basic-external-store/src/index.tsx similarity index 100% rename from examples/solid/basic/src/index.tsx rename to examples/solid/basic-external-store/src/index.tsx diff --git a/examples/solid/basic-external-store/src/makeData.ts b/examples/solid/basic-external-store/src/makeData.ts new file mode 100644 index 0000000000..6311127267 --- /dev/null +++ b/examples/solid/basic-external-store/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/solid/basic/tsconfig.json b/examples/solid/basic-external-store/tsconfig.json similarity index 100% rename from examples/solid/basic/tsconfig.json rename to examples/solid/basic-external-store/tsconfig.json diff --git a/examples/solid/basic/vite.config.ts b/examples/solid/basic-external-store/vite.config.ts similarity index 100% rename from examples/solid/basic/vite.config.ts rename to examples/solid/basic-external-store/vite.config.ts diff --git a/examples/solid/basic-table-helper/src/App.tsx b/examples/solid/basic-table-helper/src/App.tsx deleted file mode 100644 index 59cdfc9a45..0000000000 --- a/examples/solid/basic-table-helper/src/App.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { createTableHelper, flexRender } from '@tanstack/solid-table' -import { For, createSignal } from 'solid-js' - -// This example uses the new `createTableHelper` method to create a re-usable table helper object instead of independently using the standalone `createTable` hook and `createColumnHelper` method. You can choose to use either way. - -// 1. Define what the shape of your data will be for each row -type Person = { - firstName: string - lastName: string - age: number - visits: number - status: string - progress: number -} - -// 2. Create some dummy data with a stable reference (this could be an API response stored in useState or similar) -const defaultData: Array = [ - { - firstName: 'tanner', - lastName: 'linsley', - age: 24, - visits: 100, - status: 'In Relationship', - progress: 50, - }, - { - firstName: 'tandy', - lastName: 'miller', - age: 40, - visits: 40, - status: 'Single', - progress: 80, - }, - { - firstName: 'joe', - lastName: 'dirte', - age: 45, - visits: 20, - status: 'Complicated', - progress: 10, - }, -] - -// 3. New in V9! Tell the table which features and row models we want to use. In this case, this will be a basic table with no additional features -const tableHelper = createTableHelper({ - _features: {}, - _rowModels: {}, // client-side row models. `Core` row model is now included by default, but you can still override it here - - TData: {} as Person, - debugTable: true, -}) - -// 4. Create a helper object to help define our columns -// const { columnHelper } = tableHelper // if TData was set in the table helper options - otherwise use the createColumnHelper method below -const columnHelper = tableHelper.createColumnHelper() - -// 5. Define the columns for your table with a stable reference (in this case, defined statically outside of a react component) -const columns = columnHelper.columns([ - // accessorKey method (most common for simple use-cases) - columnHelper.accessor('firstName', { - cell: (info) => info.getValue(), - footer: (info) => info.column.id, - }), - // accessorFn used (alternative) along with a custom id - columnHelper.accessor((row) => row.lastName, { - id: 'lastName', - cell: (info) => {info.getValue()}, - header: () => Last Name, - footer: (info) => info.column.id, - }), - // accessorFn used to transform the data - columnHelper.accessor((row) => Number(row.age), { - id: 'age', - header: () => 'Age', - cell: (info) => info.renderValue(), - footer: (info) => info.column.id, - }), - columnHelper.accessor('visits', { - header: () => Visits, - footer: (info) => info.column.id, - }), - columnHelper.accessor('status', { - header: 'Status', - footer: (info) => info.column.id, - }), - columnHelper.accessor('progress', { - header: 'Profile Progress', - footer: (info) => info.column.id, - }), -]) - -function App() { - // 6. Store data with a stable reference - const [data, setData] = createSignal(defaultData) - const rerender = () => setData(defaultData) - - // 7. Create the table instance with the required columns and data. - // Features and row models are already defined in the table helper object above - const table = tableHelper.createTable({ - columns, - get data() { - return data() - }, - // add additional table options here or in the table helper above - }) - - return ( -
- - - - {(headerGroup) => ( - - - {(header) => ( - - )} - - - )} - - - - - {(row) => ( - - - {(cell) => ( - - )} - - - )} - - - - - {(footerGroup) => ( - - - {(header) => ( - - )} - - - )} - - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} -
- {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.footer, - header.getContext(), - )} -
-
- -
- ) -} - -export default App diff --git a/examples/solid/basic-use-table/.gitignore b/examples/solid/basic-use-table/.gitignore new file mode 100644 index 0000000000..76add878f8 --- /dev/null +++ b/examples/solid/basic-use-table/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/examples/solid/basic-use-table/README.md b/examples/solid/basic-use-table/README.md new file mode 100644 index 0000000000..b168d3c4b1 --- /dev/null +++ b/examples/solid/basic-use-table/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm run start` or `yarn start` diff --git a/examples/solid/basic-use-table/index.html b/examples/solid/basic-use-table/index.html new file mode 100644 index 0000000000..7d9b4f7c51 --- /dev/null +++ b/examples/solid/basic-use-table/index.html @@ -0,0 +1,16 @@ + + + + + + + + Solid App + + + +
+ + + + diff --git a/examples/solid/basic/package.json b/examples/solid/basic-use-table/package.json similarity index 87% rename from examples/solid/basic/package.json rename to examples/solid/basic-use-table/package.json index b630fd4f2e..b996079ae9 100644 --- a/examples/solid/basic/package.json +++ b/examples/solid/basic-use-table/package.json @@ -1,5 +1,5 @@ { - "name": "tanstack-table-example-solid-basic", + "name": "tanstack-table-example-solid-basic-use-table", "version": "0.0.0", "description": "", "scripts": { diff --git a/examples/solid/basic/src/App.tsx b/examples/solid/basic-use-table/src/App.tsx similarity index 65% rename from examples/solid/basic/src/App.tsx rename to examples/solid/basic-use-table/src/App.tsx index bb0f8df7c8..ad9441e4c3 100644 --- a/examples/solid/basic/src/App.tsx +++ b/examples/solid/basic-use-table/src/App.tsx @@ -2,9 +2,6 @@ import { createTable, flexRender, tableFeatures } from '@tanstack/solid-table' import { For, createSignal } from 'solid-js' import type { ColumnDef } from '@tanstack/solid-table' -// This example uses the classic standalone `useTable` hook to create a table without the new `createTableHelper` util. - -// 1. Define what the shape of your data will be for each row type Person = { firstName: string lastName: string @@ -14,7 +11,6 @@ type Person = { progress: number } -// 2. Create some dummy data with a stable reference (this could be an API response stored in useState or similar) const defaultData: Array = [ { firstName: 'tanner', @@ -40,58 +36,58 @@ const defaultData: Array = [ status: 'Complicated', progress: 10, }, + { + firstName: 'kevin', + lastName: 'vandy', + age: 12, + visits: 100, + status: 'Single', + progress: 70, + }, ] -// 3. New in V9! Tell the table which features and row models we want to use. In this case, this will be a basic table with no additional features -const _features = tableFeatures({}) // util method to create sharable TFeatures object/type +const _features = tableFeatures({}) -// 4. Define the columns for your table. This uses the new `ColumnDef` type to define columns. Alternatively, check out the createTableHelper/createColumnHelper util for an even more type-safe way to define columns. -const defaultColumns: Array> = [ +const columns: Array> = [ { accessorKey: 'firstName', + header: 'First Name', cell: (info) => info.getValue(), - footer: (info) => info.column.id, }, { accessorFn: (row) => row.lastName, id: 'lastName', - cell: (info) => {info.getValue()}, header: () => Last Name, - footer: (info) => info.column.id, + cell: (info) => {info.getValue()}, }, { - accessorKey: 'age', + accessorFn: (row) => Number(row.age), + id: 'age', header: () => 'Age', - footer: (info) => info.column.id, + cell: (info) => info.renderValue(), }, { accessorKey: 'visits', header: () => Visits, - footer: (info) => info.column.id, }, { accessorKey: 'status', header: 'Status', - footer: (info) => info.column.id, }, { accessorKey: 'progress', header: 'Profile Progress', - footer: (info) => info.column.id, }, ] function App() { - // 5. Store data with a stable reference - const [data, setData] = createSignal(defaultData) - const rerender = () => setData(defaultData) + const [data, setData] = createSignal([...defaultData]) + const rerender = () => setData([...defaultData]) - // 6. Create the table instance with required _features, columns, and data const table = createTable({ - _features, // new required option in V9. Tell the table which features you are importing and using (better tree-shaking) - _rowModels: {}, // `Core` row model is now included by default, but you can still override it here - // add additional table options here - columns: defaultColumns, + _features, + _rowModels: {}, + columns, get data() { return data() }, @@ -160,7 +156,7 @@ function App() {
-
diff --git a/examples/solid/basic-use-table/src/index.css b/examples/solid/basic-use-table/src/index.css new file mode 100644 index 0000000000..43c09e0f6b --- /dev/null +++ b/examples/solid/basic-use-table/src/index.css @@ -0,0 +1,26 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} diff --git a/examples/solid/basic-use-table/src/index.tsx b/examples/solid/basic-use-table/src/index.tsx new file mode 100644 index 0000000000..ea9c952177 --- /dev/null +++ b/examples/solid/basic-use-table/src/index.tsx @@ -0,0 +1,6 @@ +/* @refresh reload */ +import { render } from 'solid-js/web' +import './index.css' +import App from './App' + +render(() => , document.getElementById('root') as HTMLElement) diff --git a/examples/solid/basic-use-table/tsconfig.json b/examples/solid/basic-use-table/tsconfig.json new file mode 100644 index 0000000000..7ab027b8d2 --- /dev/null +++ b/examples/solid/basic-use-table/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/solid/basic-use-table/vite.config.ts b/examples/solid/basic-use-table/vite.config.ts new file mode 100644 index 0000000000..d27427972d --- /dev/null +++ b/examples/solid/basic-use-table/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import solidPlugin from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [solidPlugin()], + build: { + target: 'esnext', + polyfillDynamicImport: false, + }, +}) diff --git a/examples/solid/with-tanstack-query/.gitignore b/examples/solid/with-tanstack-query/.gitignore new file mode 100644 index 0000000000..76add878f8 --- /dev/null +++ b/examples/solid/with-tanstack-query/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/examples/solid/with-tanstack-query/README.md b/examples/solid/with-tanstack-query/README.md new file mode 100644 index 0000000000..b168d3c4b1 --- /dev/null +++ b/examples/solid/with-tanstack-query/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm run start` or `yarn start` diff --git a/examples/solid/with-tanstack-query/index.html b/examples/solid/with-tanstack-query/index.html new file mode 100644 index 0000000000..7d9b4f7c51 --- /dev/null +++ b/examples/solid/with-tanstack-query/index.html @@ -0,0 +1,16 @@ + + + + + + + + Solid App + + + +
+ + + + diff --git a/examples/solid/with-tanstack-query/package.json b/examples/solid/with-tanstack-query/package.json new file mode 100644 index 0000000000..cbe6091d06 --- /dev/null +++ b/examples/solid/with-tanstack-query/package.json @@ -0,0 +1,25 @@ +{ + "name": "tanstack-table-example-solid-with-tanstack-query", + "version": "0.0.0", + "description": "", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "lint": "eslint ./src" + }, + "license": "MIT", + "devDependencies": { + "@faker-js/faker": "^10.2.0", + "typescript": "5.9.3", + "vite": "^7.3.1", + "vite-plugin-solid": "^2.11.10" + }, + "dependencies": { + "@tanstack/solid-query": "^5.90.20", + "@tanstack/solid-store": "^0.9.2", + "@tanstack/solid-table": "^9.0.0-alpha.10", + "solid-js": "^1.9.11" + } +} diff --git a/examples/solid/with-tanstack-query/src/App.tsx b/examples/solid/with-tanstack-query/src/App.tsx new file mode 100644 index 0000000000..9afad1a617 --- /dev/null +++ b/examples/solid/with-tanstack-query/src/App.tsx @@ -0,0 +1,193 @@ +import { keepPreviousData, useQuery } from '@tanstack/solid-query' +import { createStore, useStore } from '@tanstack/solid-store' +import { + createColumnHelper, + createTable, + flexRender, + getInitialTableState, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/solid-table' +import { For } from 'solid-js' +import { fetchData } from './fetchData' +import type { Person } from './fetchData' + +const _features = tableFeatures({ + rowPaginationFeature, +}) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('lastName', { + header: 'Last Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { + header: 'Age', + }), + columnHelper.accessor('visits', { + header: 'Visits', + }), + columnHelper.accessor('status', { + header: 'Status', + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + }), +]) + +const myTableStore = createStore( + getInitialTableState(_features, { + pagination: { pageIndex: 0, pageSize: 10 }, + }), +) + +function App() { + const state = useStore(myTableStore) + + const dataQuery = useQuery(() => ({ + queryKey: ['data', state().pagination], + queryFn: () => fetchData(state().pagination), + placeholderData: keepPreviousData, + })) + + const defaultData: Array = [] + + const table = createTable({ + _features, + _rowModels: {}, + columns, + get data() { + return dataQuery.data?.rows ?? defaultData + }, + get rowCount() { + return dataQuery.data?.rowCount + }, + store: myTableStore, + manualPagination: true, + debugTable: true, + }) + + return ( +
+
+ + + + {(headerGroup) => ( + + + {(header) => ( + + )} + + + )} + + + + + {(row) => ( + + + {(cell) => ( + + )} + + + )} + + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} +
+
+
+ + + + + +
Page
+ + {state().pagination.pageIndex + 1} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = e.currentTarget.value + ? Number(e.currentTarget.value) - 1 + : 0 + table.setPageIndex(page) + }} + class="border p-1 rounded w-16" + /> + + + {dataQuery.isFetching ? 'Loading...' : null} +
+
+ Showing {table.getRowModel().rows.length.toLocaleString()} of{' '} + {dataQuery.data?.rowCount.toLocaleString()} Rows +
+
{JSON.stringify(state(), null, 2)}
+
+ ) +} + +export default App diff --git a/examples/solid/with-tanstack-query/src/fetchData.ts b/examples/solid/with-tanstack-query/src/fetchData.ts new file mode 100644 index 0000000000..0ad745b375 --- /dev/null +++ b/examples/solid/with-tanstack-query/src/fetchData.ts @@ -0,0 +1,66 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} + +const data = makeData(10000) + +export async function fetchData(options: { + pageIndex: number + pageSize: number +}) { + await new Promise((resolve) => setTimeout(resolve, 500)) + + return { + rows: data.slice( + options.pageIndex * options.pageSize, + (options.pageIndex + 1) * options.pageSize, + ), + pageCount: Math.ceil(data.length / options.pageSize), + rowCount: data.length, + } +} diff --git a/examples/solid/with-tanstack-query/src/index.css b/examples/solid/with-tanstack-query/src/index.css new file mode 100644 index 0000000000..43c09e0f6b --- /dev/null +++ b/examples/solid/with-tanstack-query/src/index.css @@ -0,0 +1,26 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} diff --git a/examples/solid/with-tanstack-query/src/index.tsx b/examples/solid/with-tanstack-query/src/index.tsx new file mode 100644 index 0000000000..43087f3ce9 --- /dev/null +++ b/examples/solid/with-tanstack-query/src/index.tsx @@ -0,0 +1,15 @@ +import { render } from 'solid-js/web' +import { QueryClient, QueryClientProvider } from '@tanstack/solid-query' +import './index.css' +import App from './App' + +const queryClient = new QueryClient() + +render( + () => ( + + + + ), + document.getElementById('root') as HTMLElement, +) diff --git a/examples/solid/with-tanstack-query/tsconfig.json b/examples/solid/with-tanstack-query/tsconfig.json new file mode 100644 index 0000000000..7ab027b8d2 --- /dev/null +++ b/examples/solid/with-tanstack-query/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/solid/with-tanstack-query/vite.config.ts b/examples/solid/with-tanstack-query/vite.config.ts new file mode 100644 index 0000000000..b80a99b402 --- /dev/null +++ b/examples/solid/with-tanstack-query/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import solidPlugin from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [solidPlugin()], + build: { + target: 'esnext', + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a75f1f4f68..c15c4d049c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2628,7 +2628,7 @@ importers: specifier: ^7.3.1 version: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2) - examples/solid/basic: + examples/solid/basic-app-table: dependencies: '@tanstack/solid-table': specifier: ^9.0.0-alpha.10 @@ -2647,7 +2647,7 @@ importers: specifier: ^2.11.10 version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2)) - examples/solid/basic-app-table: + examples/solid/basic-external-state: dependencies: '@tanstack/solid-table': specifier: ^9.0.0-alpha.10 @@ -2656,6 +2656,9 @@ importers: specifier: ^1.9.11 version: 1.9.11 devDependencies: + '@faker-js/faker': + specifier: ^10.2.0 + version: 10.2.0 typescript: specifier: 5.9.3 version: 5.9.3 @@ -2666,7 +2669,29 @@ importers: specifier: ^2.11.10 version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2)) - examples/solid/basic-table-helper: + examples/solid/basic-external-store: + dependencies: + '@tanstack/solid-table': + specifier: ^9.0.0-alpha.10 + version: link:../../../packages/solid-table + solid-js: + specifier: ^1.9.11 + version: 1.9.11 + devDependencies: + '@faker-js/faker': + specifier: ^10.2.0 + version: 10.2.0 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2) + vite-plugin-solid: + specifier: ^2.11.10 + version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2)) + + examples/solid/basic-use-table: dependencies: '@tanstack/solid-table': specifier: ^9.0.0-alpha.10 @@ -2864,6 +2889,34 @@ importers: specifier: ^2.11.10 version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2)) + examples/solid/with-tanstack-query: + dependencies: + '@tanstack/solid-query': + specifier: ^5.90.20 + version: 5.90.23(solid-js@1.9.11) + '@tanstack/solid-store': + specifier: ^0.9.2 + version: 0.9.2(solid-js@1.9.11) + '@tanstack/solid-table': + specifier: ^9.0.0-alpha.10 + version: link:../../../packages/solid-table + solid-js: + specifier: ^1.9.11 + version: 1.9.11 + devDependencies: + '@faker-js/faker': + specifier: ^10.2.0 + version: 10.2.0 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2) + vite-plugin-solid: + specifier: ^2.11.10 + version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2)) + examples/svelte/basic: devDependencies: '@rollup/plugin-replace': @@ -7294,6 +7347,11 @@ packages: peerDependencies: solid-js: '>=1.9.7' + '@tanstack/solid-query@5.90.23': + resolution: {integrity: sha512-pbZc4+Kgm7ktzIuu01R3KOWfazQKgNp4AZvW0RSvv+sNMpYoileUDAkXEcjDJe6RJmb3fVvTR4LlcSL5pxDElQ==} + peerDependencies: + solid-js: ^1.6.0 + '@tanstack/solid-store@0.9.2': resolution: {integrity: sha512-W6D6WnIL0Fch2zcLd7qCFf3eLD6AqwZiPoQnoRFKhe8DFXdxM4GRvfLpsAIQQHsun97EcttbwbfVq14SZfT7jg==} peerDependencies: @@ -7308,13 +7366,12 @@ packages: '@tanstack/store@0.8.1': resolution: {integrity: sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw==} + '@tanstack/store@0.9.1': + resolution: {integrity: sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg==} + '@tanstack/store@0.9.2': resolution: {integrity: sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA==} - '@tanstack/store@https://pkg.pr.new/@tanstack/store@289': - resolution: {tarball: https://pkg.pr.new/@tanstack/store@289} - version: 0.9.1 - '@tanstack/svelte-store@0.10.1': resolution: {integrity: sha512-heeyV9bZQHbEJyJ7oWegQXmcyA8NSPP58JsZgRpvf8+lwEMfX+MW1IvPJbGZqmH+poULAz7DDxjC4JEe7l57LA==} peerDependencies: @@ -15673,7 +15730,7 @@ snapshots: '@tanstack/preact-store@0.11.1(preact@10.28.2)': dependencies: - '@tanstack/store': https://pkg.pr.new/@tanstack/store@289 + '@tanstack/store': 0.9.1 preact: 10.28.2 '@tanstack/publish-config@0.2.2': @@ -15823,6 +15880,11 @@ snapshots: - csstype - utf-8-validate + '@tanstack/solid-query@5.90.23(solid-js@1.9.11)': + dependencies: + '@tanstack/query-core': 5.90.20 + solid-js: 1.9.11 + '@tanstack/solid-store@0.9.2(solid-js@1.9.11)': dependencies: '@tanstack/store': 0.9.2 @@ -15834,13 +15896,13 @@ snapshots: '@tanstack/store@0.8.1': {} - '@tanstack/store@0.9.2': {} + '@tanstack/store@0.9.1': {} - '@tanstack/store@https://pkg.pr.new/@tanstack/store@289': {} + '@tanstack/store@0.9.2': {} '@tanstack/svelte-store@0.10.1(svelte@5.48.5)': dependencies: - '@tanstack/store': https://pkg.pr.new/@tanstack/store@289 + '@tanstack/store': 0.9.1 svelte: 5.48.5 '@tanstack/typedoc-config@0.3.3(typescript@5.9.3)':