@tanstack/svelte-table
Version:
Headless UI for building powerful tables & datagrids for Svelte.
179 lines (178 loc) • 7.66 kB
JavaScript
import { getContext, setContext } from 'svelte';
import { createColumnHelper as coreCreateColumnHelper } from '@tanstack/table-core';
import { createTable } from './createTable.svelte';
import { mergeObjects } from './merge-objects';
import { cellContextKey, headerContextKey, tableContextKey, } from './context-keys.js';
import AppTableSvelte from './AppTable.svelte';
import AppCellSvelte from './AppCell.svelte';
import AppHeaderSvelte from './AppHeader.svelte';
import FlexRenderSvelte from './FlexRender.svelte';
// =============================================================================
// createTableHook Factory
// =============================================================================
/**
* Creates a custom table hook with pre-bound components for composition.
*
* This is the table equivalent of TanStack Form's `createFormHook`. It allows you to:
* - Define features, row models, and default options once, shared across all tables
* - Register reusable table, cell, and header components
* - Access table/cell/header instances via context in those components
* - Get a `createAppTable` hook that returns an extended table with App wrapper components
* - Get a `createAppColumnHelper` function pre-bound to your features
*
* @example
* ```ts
* // hooks/table.ts
* export const {
* createAppTable,
* createAppColumnHelper,
* useTableContext,
* useCellContext,
* useHeaderContext,
* } = createTableHook({
* features: tableFeatures({
* rowPaginationFeature,
* rowSortingFeature,
* columnFilteringFeature,
* paginatedRowModel: createPaginatedRowModel(),
* sortedRowModel: createSortedRowModel(),
* filteredRowModel: createFilteredRowModel(),
* sortFns,
* filterFns,
* }),
* tableComponents: { PaginationControls, RowCount },
* cellComponents: { TextCell, NumberCell },
* headerComponents: { SortIndicator, ColumnFilter },
* })
* ```
*/
export function createTableHook({ tableComponents, cellComponents, headerComponents, ...defaultTableOptions }) {
/**
* 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`).
*/
function createAppColumnHelper() {
return coreCreateColumnHelper();
}
/**
* 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.
*/
function useTableContext() {
const table = getContext(tableContextKey);
if (!table) {
throw new Error('`useTableContext` must be used within an `AppTable` component. ' +
'Make sure your component is wrapped with `<table.AppTable>...</table.AppTable>`.');
}
// `<table.AppTable>` provides the extended table (the App* wrapper
// components and `tableComponents` are Object.assign-ed onto the same
// instance `createAppTable` returns), so this asserts the runtime shape.
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.
*/
function useCellContext() {
const cell = getContext(cellContextKey);
if (!cell) {
throw new Error('`useCellContext` must be used within an `AppCell` component. ' +
'Make sure your component is wrapped with `<table.AppCell cell={cell}>...</table.AppCell>`.');
}
// `<table.AppCell>` Object.assign-es `cellComponents` and `FlexRender` onto
// the same cell instance it puts in context, so this asserts the runtime
// shape.
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.
*/
function useHeaderContext() {
const header = getContext(headerContextKey);
if (!header) {
throw new Error('`useHeaderContext` must be used within an `AppHeader` or `AppFooter` component.');
}
// `<table.AppHeader>` / `<table.AppFooter>` Object.assign `headerComponents`
// and `FlexRender` onto the same header instance they put in context.
return header;
}
/**
* Enhanced createTable 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) {
// Merge default options with provided options (provided takes precedence)
const mergedTableOptions = mergeObjects(defaultTableOptions, tableOptions);
const table = createTable(mergedTableOptions);
// Build cellComponents with FlexRender included
const cellComponentsWithFlexRender = {
FlexRender: FlexRenderSvelte,
...(cellComponents ?? {}),
};
// Build headerComponents with FlexRender included
const headerComponentsWithFlexRender = {
FlexRender: FlexRenderSvelte,
...(headerComponents ?? {}),
};
// Create wrapper components using the svelte-form (internal, props) => pattern.
// setContext is called in the closure — this runs during component
// initialization, so Svelte's context API works correctly.
// With keyed {#each} blocks, components are recreated on reorder,
// so context is always fresh.
const AppTable = ((internal, props) => {
setContext(tableContextKey, table);
return AppTableSvelte(internal, { ...props });
});
const AppCell = ((internal, { children, cell }) => {
setContext(cellContextKey, cell);
return AppCellSvelte(internal, {
cell,
cellComponents: cellComponentsWithFlexRender,
children,
});
});
const AppHeader = ((internal, { children, header }) => {
setContext(headerContextKey, header);
return AppHeaderSvelte(internal, {
header,
headerComponents: headerComponentsWithFlexRender,
children,
});
});
// AppFooter reuses AppHeaderSvelte (footers use Header type in table-core)
const AppFooter = ((internal, { children, header }) => {
setContext(headerContextKey, header);
return AppHeaderSvelte(internal, {
header,
headerComponents: headerComponentsWithFlexRender,
children,
});
});
// Combine everything into the extended table API
return Object.assign(table, {
AppTable,
AppCell,
AppHeader,
AppFooter,
FlexRender: FlexRenderSvelte,
...(tableComponents ?? {}),
});
}
return {
appFeatures: defaultTableOptions.features,
createAppColumnHelper,
createAppTable,
useTableContext,
useCellContext,
useHeaderContext,
};
}