UNPKG

compote-ui

Version:

An opinionated UI component library for Svelte, built on top of [Ark UI](https://ark-ui.com) with additional components and features not available in the core Ark UI library.

246 lines (245 loc) 9.99 kB
import { createTable as createSvelteTable } from '@tanstack/svelte-table'; import { useLocaleContext } from '@ark-ui/svelte/locale'; import { onDestroy, untrack } from 'svelte'; import { renderComponent, renderSnippet } from '@tanstack/svelte-table'; import { dataTableFeatures } from './features'; import { TYPE_NUMBER_FORMAT_DEFAULTS } from './data-table-utils'; const oneOfFilterFn = (row, columnId, filterValue) => { return filterValue.includes(String(row.getValue(columnId))); }; oneOfFilterFn.autoRemove = (val) => !Array.isArray(val) || val.length === 0; export function createTable(options) { const localeCtx = useLocaleContext(); const initialColumnVisibility = { ...createColumnVisibility(options.columns), ...options.initialState?.columnVisibility }; // Recompute the resolved column defs only when the source columns change. // $derived keeps the reference stable between changes so TanStack's column // memoization isn't invalidated on every access. Pass `columns` through a // getter (like `data`) to make adding/removing/reordering columns reactive. const columnDefs = $derived(createColumns(options.columns, localeCtx)); const table = createSvelteTable({ features: dataTableFeatures, get data() { return options.data; }, get columns() { return columnDefs; }, columnResizeMode: options.columnResizeMode, getRowId: options.getRowId, enableRowSelection: options.enableRowSelection ?? false, enableMultiRowSelection: options.enableMultiRowSelection, enableSorting: options.enableSorting, debugTable: options.debugTable, initialState: { columnVisibility: initialColumnVisibility, columnSizing: { ...createColumnSizing(options.columns), ...options.initialState?.columnSizing }, columnPinning: options.initialState?.columnPinning ?? createColumnPinning(options.columns), rowSelection: options.initialState?.rowSelection ?? {}, sorting: options.initialState?.sorting ?? [], columnFilters: options.initialState?.columnFilters ?? [] } }); // The svelte adapter's internal $effect.pre only tracks `data` and `state` — not // `columns` — so reactive add/remove/reorder of columns never reaches the table. // Track the derived column defs here and push them through setOptions ourselves. // Re-supply the `data` getter so spreading `prev` doesn't freeze data reactivity. // CAUTION: upstream guidance says a second setOptions sync can race the adapter's // own $effect.pre (which getter-merges the original options on data/state change). // Both syncs write consistent values here, but revisit if the beta adapter ever // starts tracking `columns` itself. $effect.pre(() => { const columns = columnDefs; untrack(() => { table.setOptions((prev) => ({ ...prev, get data() { return options.data; }, columns })); }); }); // Notify consumers of visibility changes without taking control of the slice // (overriding the table's own `onColumnVisibilityChange` option would suppress // internal updates). The atom's `subscribe` is an explicit observer that runs // the callback outside Svelte's dependency tracking, so consumer-side reads // (e.g. a persisted-state proxy the callback writes into) can never become // dependencies that re-trigger the notification — the failure mode of // observing via $effect. The initial-replay guard compares by reference: // table state updates are immutable, so only the subscription's initial // emission (if the atom flavor emits one) can match the snapshot. if (options.onColumnVisibilityChange) { const initialVisibility = table.atoms.columnVisibility.get(); const subscription = table.atoms.columnVisibility.subscribe((visibility) => { if (visibility === initialVisibility) return; options.onColumnVisibilityChange?.(visibility); }); onDestroy(() => subscription.unsubscribe()); } return table; } function createColumnVisibility(columns) { return getLeafColumns(columns).reduce((visibility, column) => { visibility[getColumnId(column)] = true; return visibility; }, {}); } function createColumnSizing(columns) { return getLeafColumns(columns).reduce((sizes, column) => { if (typeof column.size === 'number') { sizes[getColumnId(column)] = column.size; } return sizes; }, {}); } function createColumnPinning(columns) { const leafCols = getLeafColumns(columns); return { left: leafCols.filter((c) => c.pinned === 'left').map(getColumnId), right: leafCols.filter((c) => c.pinned === 'right').map(getColumnId) }; } function createColumns(columns, localeCtx) { return columns.map((column) => { if (isGroupColumn(column)) { return { id: getGroupColumnId(column), header: column.header, columns: createColumns(column.columns, localeCtx), meta: { align: column.align } }; } const columnId = getColumnId(column); const derivedFilterFn = column.filterFn ?? getFilterFnForType(column.type); const columnDef = { id: columnId, header: column.header, size: column.size, minSize: column.minSize, maxSize: column.maxSize, enableResizing: column.enableResizing, enableHiding: column.enableHiding, enableSorting: column.enableSorting, sortDescFirst: column.sortDescFirst, enableColumnFilter: column.enableColumnFilter, ...(derivedFilterFn !== undefined ? { filterFn: derivedFilterFn } : {}), meta: { align: column.align, type: column.type, formatOptions: column.formatOptions, formatLocale: column.formatLocale, grow: column.grow, sum: column.sum, footer: column.footer } }; if (typeof column.accessorFn === 'function') { return { ...columnDef, accessorFn: column.accessorFn, cell: (context) => formatCellValue(column, context.getValue(), context.row.original, localeCtx) }; } return { ...columnDef, accessorKey: column.accessorKey, cell: (context) => formatCellValue(column, context.getValue(), context.row.original, localeCtx) }; }); } function isGroupColumn(column) { return Array.isArray(column.columns); } function getLeafColumns(columns) { return columns.flatMap((column) => isGroupColumn(column) ? getLeafColumns(column.columns) : column); } function getGroupColumnId(column) { return column.id ?? column.header; } export function getColumnId(column) { if (column.id !== undefined) return column.id; if ('accessorKey' in column && column.accessorKey !== undefined) return column.accessorKey; throw new Error('DataTableColumn with accessorFn requires an id.'); } const TYPE_DATE_FORMAT_DEFAULTS = { date: { day: '2-digit', month: '2-digit', year: 'numeric' }, time: { hour: '2-digit', minute: '2-digit' }, 'date-time': { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' } }; function getFilterFnForType(type) { switch (type) { case 'number': case 'currency': case 'percent': return 'inNumberRange'; case 'boolean': return 'equals'; case 'select': return oneOfFilterFn; default: return undefined; } } function applyTypeFormat(column, value, localeCtx) { if (value === null || value === undefined || value === '') return undefined; const numDefaults = column.type ? TYPE_NUMBER_FORMAT_DEFAULTS[column.type] : undefined; if (numDefaults !== undefined) { const numericValue = Number(value); if (isNaN(numericValue)) return undefined; const locale = column.formatLocale ?? localeCtx().locale; return new Intl.NumberFormat(locale, { ...numDefaults, ...column.formatOptions }).format(numericValue); } if (column.type === 'date' || column.type === 'time' || column.type === 'date-time') { const dateValue = value instanceof Date ? value : new Date(value); if (isNaN(dateValue.getTime())) return undefined; const locale = column.formatLocale ?? localeCtx().locale; return new Intl.DateTimeFormat(locale, { ...TYPE_DATE_FORMAT_DEFAULTS[column.type], ...column.formatOptions }).format(dateValue); } if (column.type === 'boolean') return value ? 'Yes' : 'No'; return value; } function formatCellValue(column, value, row, localeCtx) { if (column.cellComponent) { return renderComponent(column.cellComponent, getCellComponentProps(column, value, row)); } if (column.cellSnippet) { return renderSnippet(column.cellSnippet, { value, row }); } const rendered = column.cell ? column.cell(value, row) : applyTypeFormat(column, value, localeCtx); if (rendered === null || rendered === undefined || rendered === '') { return '-'; } return String(rendered); } function getCellComponentProps(column, value, row) { return column.cellProps ? column.cellProps(value, row) : { value, row }; }