UNPKG

@tanstack/lit-table

Version:

Headless UI for building powerful tables & datagrids for Lit.

273 lines (271 loc) 8.44 kB
import { FlexRender, flexRender } from "./flexRender.js"; import { TableController } from "./TableController.js"; import { createColumnHelper } from "@tanstack/table-core"; import { ContextConsumer, ContextProvider, createContext } from "@lit/context"; //#region src/createTableHook.ts /** * 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 `@lit/context` in those components * - Get a `useAppTable` hook that returns an extended table with App wrapper functions * - Get a `createAppColumnHelper` function pre-bound to your features * * @example * ```ts * // hooks/table.ts * export const { * createAppColumnHelper, * useAppTable, * useTableContext, * useCellContext, * useHeaderContext, * } = createTableHook({ * features: tableFeatures({ * rowPaginationFeature, * rowSortingFeature, * columnFilteringFeature, * paginatedRowModel: createPaginatedRowModel(), * sortedRowModel: createSortedRowModel(), * filteredRowModel: createFilteredRowModel(), * sortFns, * filterFns, * }), * tableComponents: { PaginationControls, RowCount }, * cellComponents: { TextCell, NumberCell }, * headerComponents: { SortIndicator, ColumnFilter }, * }) * * // Create column helper with TFeatures already bound * const columnHelper = createAppColumnHelper<Person>() * * // my-table.ts * @customElement('my-table') * class MyTable extends LitElement { * private appTable = useAppTable(this, { * columns, * data: this.data, * }) * * protected render() { * const table = this.appTable.table() * * return html` * <table> * <thead> * ${repeat(table.getHeaderGroups(), (hg) => hg.id, (hg) => html` * <tr> * ${hg.headers.map((h) => table.AppHeader(h, (header) => html` * <th>${header.FlexRender()}</th> * `))} * </tr> * `)} * </thead> * <tbody> * ${table.getRowModel().rows.map((row) => html` * <tr> * ${row.getAllCells().map((c) => table.AppCell(c, (cell) => html` * <td>${cell.FlexRender()}</td> * `))} * </tr> * `)} * </tbody> * </table> * ` * } * } * ``` */ function createTableHook({ tableComponents, cellComponents, headerComponents, ...defaultTableOptions }) { const tableContext = createContext(Symbol("tanstack-table")); const cellContext = createContext(Symbol("tanstack-cell")); const headerContext = createContext(Symbol("tanstack-header")); /** * 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 * ```ts * const columnHelper = createAppColumnHelper<Person>() * * const columns = [ * columnHelper.accessor('firstName', { * header: 'First Name', * cell: ({ cell }) => cell.FlexRender(), // cell has pre-bound components! * }), * columnHelper.accessor('age', { * header: 'Age', * cell: ({ cell }) => cell.NumberCell(), * }), * ] * ``` */ function createAppColumnHelper() { return createColumnHelper(); } /** * Access the table instance from within a custom element that is a descendant * of the element using `useAppTable`. * Uses `@lit/context` ContextConsumer to retrieve the table from the nearest ancestor provider. * TFeatures is already known from the createTableHook call. * * @example * ```ts * @customElement('pagination-controls') * class PaginationControls extends LitElement { * private _table = useTableContext(this) * * protected render() { * const table = this._table.value * if (!table) return html`` * return html` * <button @click=${() => table.previousPage()}>Prev</button> * <button @click=${() => table.nextPage()}>Next</button> * ` * } * } * ``` */ function useTableContext(host) { return new ContextConsumer(host, { context: tableContext, subscribe: true }); } /** * Access the cell instance from within a custom element that is a descendant * of an element providing cell context. * Uses `@lit/context` ContextConsumer to retrieve the cell. * TFeatures is already known from the createTableHook call. * * @example * ```ts * @customElement('text-cell') * class TextCell extends LitElement { * private _cell = useCellContext(this) * * protected render() { * const cell = this._cell.value * if (!cell) return html`` * return html`<span>${cell.getValue()}</span>` * } * } * ``` */ function useCellContext(host) { return new ContextConsumer(host, { context: cellContext, subscribe: true }); } /** * Access the header instance from within a custom element that is a descendant * of an element providing header context. * Uses `@lit/context` ContextConsumer to retrieve the header. * TFeatures is already known from the createTableHook call. * * @example * ```ts * @customElement('sort-indicator') * class SortIndicator extends LitElement { * private _header = useHeaderContext(this) * * protected render() { * const header = this._header.value * if (!header) return html`` * const sorted = header.column.getIsSorted() * return html`${sorted === 'asc' ? '🔼' : sorted === 'desc' ? '🔽' : ''}` * } * } * ``` */ function useHeaderContext(host) { return new ContextConsumer(host, { context: headerContext, subscribe: true }); } /** * Enhanced table hook that returns a controller-like object with a `table()` method. * The returned table has App wrapper functions 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. * * @example * ```ts * @customElement('my-table') * class MyTable extends LitElement { * private appTable = useAppTable(this, { * columns, * data: this.data, * }) * * protected render() { * const table = this.appTable.table() * return html`...` * } * } * ``` */ function useAppTable(host, tableOptions, selector) { const controller = new TableController(host); const provider = new ContextProvider(host, { context: tableContext }); return { table() { const mergedOptions = { ...defaultTableOptions, ...tableOptions }; const table = controller.table(mergedOptions, selector); provider.setValue(table); function AppCell(cell, renderFn) { const cellFlexRender = () => FlexRender({ cell }); const boundCellComponents = {}; for (const [key, fn] of Object.entries(cellComponents ?? {})) boundCellComponents[key] = () => fn(cell); return renderFn(Object.assign(cell, { FlexRender: cellFlexRender, ...boundCellComponents })); } function AppHeader(header, renderFn) { const headerFlexRender = () => flexRender(header.column.columnDef.header, header.getContext()); const boundHeaderComponents = {}; for (const [key, fn] of Object.entries(headerComponents ?? {})) boundHeaderComponents[key] = () => fn(header); return renderFn(Object.assign(header, { FlexRender: headerFlexRender, ...boundHeaderComponents })); } function AppFooter(header, renderFn) { const footerFlexRender = () => flexRender(header.column.columnDef.footer, header.getContext()); const boundFooterComponents = {}; for (const [key, fn] of Object.entries(headerComponents ?? {})) boundFooterComponents[key] = () => fn(header); return renderFn(Object.assign(header, { FlexRender: footerFlexRender, ...boundFooterComponents })); } return Object.assign(table, { AppCell, AppHeader, AppFooter, FlexRender, ...tableComponents ?? {} }); } }; } return { appFeatures: defaultTableOptions.features, createAppColumnHelper, useAppTable, useTableContext, useCellContext, useHeaderContext }; } //#endregion export { createTableHook };