UNPKG

@tanstack/svelte-table

Version:

Headless UI for building powerful tables & datagrids for Svelte.

193 lines (192 loc) • 8.11 kB
import { getContext } 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 // its cell and puts a live view of the current cell 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 their header and put a live view of it 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. // They only add props: each App*Svelte component sets its own context while // it initializes. Calling setContext here would run before that component // exists and write into the context of the component rendering it. // Props are forwarded through getters: Svelte props are getters, and reading // them here would pass the first value to the inner component forever. const AppTable = ((internal, props) => { return AppTableSvelte(internal, { get children() { return props.children; }, table, }); }); const AppCell = ((internal, props) => { return AppCellSvelte(internal, { get cell() { return props.cell; }, cellComponents: cellComponentsWithFlexRender, get children() { return props.children; }, }); }); const AppHeader = ((internal, props) => { return AppHeaderSvelte(internal, { get header() { return props.header; }, headerComponents: headerComponentsWithFlexRender, get children() { return props.children; }, }); }); // AppFooter reuses AppHeaderSvelte (footers use Header type in table-core) const AppFooter = ((internal, props) => { return AppHeaderSvelte(internal, { get header() { return props.header; }, headerComponents: headerComponentsWithFlexRender, get children() { return props.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, }; }