UNPKG

@humanspeak/svelte-headless-table

Version:

A powerful, headless table library for Svelte that provides complete control over table UI while handling complex data operations like sorting, filtering, pagination, grouping, and row expansion. Build custom, accessible data tables with zero styling opin

98 lines (97 loc) 3.84 kB
import { MemoryCache } from '@humanspeak/memory-cache'; import { keyed } from '@humanspeak/svelte-keyed'; import { derived, readable } from 'svelte/store'; import { recordSetStore } from '../utils/store.js'; import { DEFAULT_ROW_STATE_CACHE_CONFIG } from './cacheConfig.js'; /** * Recursively expands rows based on the expanded IDs map. * @internal */ const withExpandedRows = (row, expandedIds) => { if (row.subRows === undefined) { return [row]; } if (expandedIds[row.id] !== true) { return [row]; } const expandedSubRows = row.subRows.flatMap((subRow) => withExpandedRows(subRow, expandedIds)); return [row, ...expandedSubRows]; }; /** * Creates an expanded rows plugin that enables expanding/collapsing rows with sub-rows. * When a row is expanded, its sub-rows are included in the flattened row list. * * @template Item - The type of data items in the table. * @param config - Configuration options. * @returns A TablePlugin that provides row expansion functionality. * @example * ```typescript * const table = createTable(data, { * expand: addExpandedRows({ * initialExpandedIds: { '0': true } // Row 0 starts expanded * }) * }) * * // Toggle expansion * const rowState = table.pluginStates.expand.getRowState(row) * rowState.isExpanded.update(v => !v) * ``` */ export const addExpandedRows = ({ initialExpandedIds = {} } = {}) => () => { const expandedIds = recordSetStore(initialExpandedIds); // LRU cache for memoized row state with automatic eviction. // Prevents unbounded memory growth when row identities change. const rowStateCache = new MemoryCache(DEFAULT_ROW_STATE_CACHE_CONFIG); const getRowState = (row) => { const cached = rowStateCache.get(row.id); if (cached !== undefined) { return cached; } const isExpanded = keyed(expandedIds, row.id); const canExpand = readable((row.subRows?.length ?? 0) > 0); const subRowExpandedIds = derived(expandedIds, ($expandedIds) => { // Check prefix with '>' to match child ids while ignoring this row's id. return Object.entries($expandedIds).filter(([id, expanded]) => id.startsWith(`${row.id}>`) && expanded); }); // If the number of expanded subRows is equal to the number of subRows // that can expand, then all subRows are expanded. const isAllSubRowsExpanded = derived(subRowExpandedIds, ($subRowExpandedIds) => { if (row.subRows === undefined) { return true; } // canExpand is derived from the presence of the `subRows` property. const expandableSubRows = row.subRows.filter((subRow) => subRow.subRows !== undefined); return $subRowExpandedIds.length === expandableSubRows.length; }); const state = { isExpanded, canExpand, isAllSubRowsExpanded }; rowStateCache.set(row.id, state); return state; }; // Clear cache when expandedIds store is cleared (data reset scenario) const unsubscribeExpandedIds = expandedIds.subscribe(($expandedIds) => { if (Object.keys($expandedIds).length === 0) { rowStateCache.clear(); } }); // Cleanup function to prevent subscription leaks when table is destroyed const invalidate = () => { unsubscribeExpandedIds(); rowStateCache.clear(); }; const pluginState = { expandedIds, getRowState, invalidate }; const deriveRows = (rows) => { return derived([rows, expandedIds], ([$rows, $expandedIds]) => { return $rows.flatMap((row) => { return withExpandedRows(row, $expandedIds); }); }); }; return { pluginState, deriveRows }; };