UNPKG

@tanstack/svelte-table

Version:

Headless UI for building powerful tables & datagrids for Svelte.

179 lines (178 loc) 7.66 kB
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, }; }