@tanstack/lit-table
Version:
Headless UI for building powerful tables & datagrids for Lit.
273 lines (271 loc) • 8.44 kB
JavaScript
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 };