@classic-homes/theme-svelte 0.1.30 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/components/DataTable.svelte +95 -33
- package/dist/lib/components/DataTable.svelte.d.ts +2 -0
- package/dist/lib/components/data-table/createDataTable.svelte.js +6 -2
- package/dist/lib/components/data-table/index.d.ts +1 -1
- package/dist/lib/components/data-table/types.d.ts +141 -9
- package/dist/lib/components/data-table/utils.d.ts +57 -6
- package/dist/lib/components/data-table/utils.js +65 -16
- package/package.json +1 -1
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
formatCellValue,
|
|
62
62
|
getAriaSortValue,
|
|
63
63
|
defaultGetRowId,
|
|
64
|
+
createMemoizedConverter,
|
|
64
65
|
} from './data-table/utils.js';
|
|
65
66
|
import type {
|
|
66
67
|
DataTableProps,
|
|
@@ -187,6 +188,10 @@
|
|
|
187
188
|
enableEditing?: boolean;
|
|
188
189
|
/** Callback when a cell is edited */
|
|
189
190
|
onCellEdit?: (rowId: string, columnId: string, value: unknown, row: T) => void;
|
|
191
|
+
|
|
192
|
+
// === Debug Props ===
|
|
193
|
+
/** Enable verbose debug logging to console */
|
|
194
|
+
debug?: boolean;
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
let {
|
|
@@ -241,39 +246,63 @@
|
|
|
241
246
|
// New editing props
|
|
242
247
|
enableEditing = false,
|
|
243
248
|
onCellEdit,
|
|
249
|
+
|
|
250
|
+
// Debug props
|
|
251
|
+
debug = false,
|
|
244
252
|
}: Props = $props();
|
|
245
253
|
|
|
246
|
-
// Validate props
|
|
254
|
+
// Validate props
|
|
247
255
|
$effect(() => {
|
|
248
256
|
validateNonEmptyArray(columns, 'columns', 'DataTable');
|
|
249
|
-
});
|
|
250
257
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
// Validate column IDs are unique - this MUST be enforced as duplicate IDs
|
|
259
|
+
// cause silent bugs in sorting, selection, and visibility features
|
|
260
|
+
const ids = columns.map((c) => c.id);
|
|
261
|
+
const seen = new Set<string>();
|
|
262
|
+
const duplicates: string[] = [];
|
|
263
|
+
for (const id of ids) {
|
|
264
|
+
if (seen.has(id)) {
|
|
265
|
+
duplicates.push(id);
|
|
266
|
+
}
|
|
267
|
+
seen.add(id);
|
|
268
|
+
}
|
|
269
|
+
if (duplicates.length > 0) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`[DataTable] Duplicate column IDs found: ${duplicates.join(', ')}. ` +
|
|
272
|
+
'Each column must have a unique ID. Duplicate IDs cause bugs in sorting, ' +
|
|
273
|
+
'selection, filtering, and column visibility features.'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Warn when using server-side features without a custom getRowId
|
|
278
|
+
// Using array index as row ID breaks selection when data is reordered or paginated server-side
|
|
279
|
+
const isServerSide = onSort || manualFiltering || manualPagination;
|
|
280
|
+
const hasCustomGetRowId = !!getRowId;
|
|
281
|
+
if (isServerSide && enableRowSelection && !hasCustomGetRowId) {
|
|
282
|
+
console.warn(
|
|
283
|
+
'[DataTable] Row selection is enabled with server-side data operations, but no ' +
|
|
284
|
+
'`getRowId` function was provided. The default uses array index, which will cause ' +
|
|
285
|
+
'selection to break when data is reordered, filtered, or paginated on the server. ' +
|
|
286
|
+
'Provide a `getRowId` function that returns a stable unique identifier for each row ' +
|
|
287
|
+
'(e.g., `getRowId={(row) => row.id}`).'
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
259
291
|
|
|
260
292
|
/**
|
|
261
|
-
*
|
|
293
|
+
* Memoized column definition converter with LRU cache.
|
|
262
294
|
*
|
|
263
|
-
*
|
|
264
|
-
* -
|
|
265
|
-
* -
|
|
295
|
+
* Features:
|
|
296
|
+
* - Maximum 100 cached entries (prevents unbounded growth)
|
|
297
|
+
* - LRU eviction when cache is full
|
|
298
|
+
* - Cache key includes all properties that affect the ColumnDef
|
|
299
|
+
* - Clear method for manual cache invalidation
|
|
266
300
|
*/
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (cached) {
|
|
273
|
-
return cached;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const columnDef: ColumnDef<T, unknown> = {
|
|
301
|
+
const memoizedConvertToColumnDef = createMemoizedConverter<
|
|
302
|
+
DataTableColumn<T>,
|
|
303
|
+
ColumnDef<T, unknown>
|
|
304
|
+
>(
|
|
305
|
+
(col) => ({
|
|
277
306
|
id: col.id,
|
|
278
307
|
header: col.header,
|
|
279
308
|
accessorFn:
|
|
@@ -293,18 +322,26 @@
|
|
|
293
322
|
filterPlaceholder: col.filterPlaceholder,
|
|
294
323
|
...col.meta,
|
|
295
324
|
} satisfies ColumnMeta<T>,
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
325
|
+
}),
|
|
326
|
+
// Cache key includes all properties that affect the ColumnDef
|
|
327
|
+
(col) =>
|
|
328
|
+
`${col.id}:${col.header}:${col.sortable}:${col.enableColumnFilter}:${col.enableHiding}:${col.width}:${col.align}`,
|
|
329
|
+
100 // Max 100 cached column definitions
|
|
330
|
+
);
|
|
301
331
|
|
|
302
332
|
// Clear cache when columns array changes (new reference)
|
|
303
333
|
let previousColumnsRef: DataTableColumn<T>[] | undefined;
|
|
304
334
|
$effect(() => {
|
|
305
335
|
if (previousColumnsRef !== columns) {
|
|
306
|
-
|
|
336
|
+
memoizedConvertToColumnDef.clear();
|
|
307
337
|
previousColumnsRef = columns;
|
|
338
|
+
|
|
339
|
+
if (debug) {
|
|
340
|
+
console.log('[DataTable] Columns changed, cache cleared', {
|
|
341
|
+
columnCount: columns.length,
|
|
342
|
+
columnIds: columns.map((c) => c.id),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
308
345
|
}
|
|
309
346
|
});
|
|
310
347
|
|
|
@@ -326,8 +363,8 @@
|
|
|
326
363
|
// Create TanStack column definitions
|
|
327
364
|
const columnDefs = $derived<ColumnDef<T, unknown>[]>(
|
|
328
365
|
enableRowSelection
|
|
329
|
-
? [selectionColumnDef, ...columns.map(
|
|
330
|
-
: columns.map(
|
|
366
|
+
? [selectionColumnDef, ...columns.map(memoizedConvertToColumnDef)]
|
|
367
|
+
: columns.map(memoizedConvertToColumnDef)
|
|
331
368
|
);
|
|
332
369
|
|
|
333
370
|
// Create the TanStack table instance
|
|
@@ -389,7 +426,32 @@
|
|
|
389
426
|
}));
|
|
390
427
|
|
|
391
428
|
// Get rows to display (respects pagination when enabled)
|
|
392
|
-
|
|
429
|
+
// Must depend on version to re-evaluate when data changes externally
|
|
430
|
+
const displayRows = $derived.by(() => {
|
|
431
|
+
void table.reactiveState.version;
|
|
432
|
+
return table.instance.getRowModel().rows;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Debug logging for table state changes
|
|
436
|
+
$effect(() => {
|
|
437
|
+
if (!debug) return;
|
|
438
|
+
|
|
439
|
+
// Track version to log on every state change
|
|
440
|
+
const version = table.reactiveState.version;
|
|
441
|
+
const rowCount = displayRows.length;
|
|
442
|
+
const totalRows = data.length;
|
|
443
|
+
|
|
444
|
+
console.log('[DataTable] State updated', {
|
|
445
|
+
version,
|
|
446
|
+
displayedRows: rowCount,
|
|
447
|
+
totalRows,
|
|
448
|
+
sorting: table.sorting,
|
|
449
|
+
pagination: enablePagination ? table.pagination : 'disabled',
|
|
450
|
+
globalFilter: enableGlobalFilter ? table.globalFilter : 'disabled',
|
|
451
|
+
selectedCount: enableRowSelection ? table.selectedRowCount : 'disabled',
|
|
452
|
+
cacheSize: memoizedConvertToColumnDef.size(),
|
|
453
|
+
});
|
|
454
|
+
});
|
|
393
455
|
|
|
394
456
|
// Handle row click with error boundary
|
|
395
457
|
function handleRowClick(row: T) {
|
|
@@ -123,6 +123,8 @@ declare function $$render<T extends object>(): {
|
|
|
123
123
|
enableEditing?: boolean;
|
|
124
124
|
/** Callback when a cell is edited */
|
|
125
125
|
onCellEdit?: (rowId: string, columnId: string, value: unknown, row: T) => void;
|
|
126
|
+
/** Enable verbose debug logging to console */
|
|
127
|
+
debug?: boolean;
|
|
126
128
|
};
|
|
127
129
|
exports: {};
|
|
128
130
|
bindings: "columnVisibility" | "columnFilters" | "globalFilter" | "rowSelection" | "sortColumn" | "sortDirection" | "pageSize" | "pageIndex";
|
|
@@ -275,10 +275,14 @@ export function createDataTable(options) {
|
|
|
275
275
|
// Use $derived to create a reactive options snapshot that tracks all dependencies
|
|
276
276
|
const currentOptions = $derived.by(() => {
|
|
277
277
|
const opts = options();
|
|
278
|
-
//
|
|
279
|
-
//
|
|
278
|
+
// Explicitly read all values that should trigger re-renders.
|
|
279
|
+
// Svelte 5's fine-grained reactivity requires explicit property access
|
|
280
|
+
// to establish dependencies - the spread operator alone doesn't track them.
|
|
280
281
|
return {
|
|
281
282
|
...opts,
|
|
283
|
+
// Track data and column changes (critical for external updates)
|
|
284
|
+
_data: opts.data,
|
|
285
|
+
_columns: opts.columns,
|
|
282
286
|
// Explicitly read state values to track them (these establish reactive dependencies)
|
|
283
287
|
_sorting: opts.state?.sorting ?? opts.sorting ?? internalSorting,
|
|
284
288
|
_columnFilters: opts.state?.columnFilters ?? opts.columnFilters ?? internalColumnFilters,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export { createDataTable, type CreateDataTableOptions, type DataTableInstance, type Table, type ColumnDef, type SortingState, type ColumnFiltersState, type VisibilityState, type RowSelectionState, type PaginationState, type Row, type Cell, type Header, type HeaderGroup, type Column, type CellContext, type HeaderContext, createColumnHelper, } from './createDataTable.svelte.js';
|
|
18
18
|
export { type ColumnMeta, type BaseColumnMeta, type DataColumnMeta, type FilterableColumnMeta, type SelectionColumnMeta, type DataTableProps, type DataTableCoreProps, type DataTableSortingProps, type DataTableSelectionProps, type DataTableVisibilityProps, type DataTableFilteringProps, type DataTablePaginationProps, type DataTableVirtualizationProps, type DataTableEditingProps, type DataTableInteractionProps, SVELTE_COMPONENT_SYMBOL, isMarkedComponent, markAsComponent, type MarkedSvelteComponent, type InferRowType, type FormatResult, } from './types.js';
|
|
19
|
-
export { getAlignClass, getMetaAlign, formatCellValue, formatCellValueSafe, getColumnWidthStyle, getColumnFlexStyle, getAriaSortValue, defaultGetRowId, createMemoizedConverter, } from './utils.js';
|
|
19
|
+
export { getAlignClass, getMetaAlign, formatCellValue, formatCellValueSafe, getColumnWidthStyle, getColumnFlexStyle, getAriaSortValue, defaultGetRowId, createMemoizedConverter, type MemoizedFunction, } from './utils.js';
|
|
20
20
|
export { default as FlexRender } from './FlexRender.svelte';
|
|
21
21
|
export { default as DataTableToolbar } from './DataTableToolbar.svelte';
|
|
22
22
|
export { default as DataTableSearch } from './DataTableSearch.svelte';
|
|
@@ -97,20 +97,66 @@ export interface DataTableSortingProps {
|
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
99
|
* Props for row selection functionality.
|
|
100
|
+
*
|
|
101
|
+
* ## Row ID Requirements
|
|
102
|
+
*
|
|
103
|
+
* **Critical**: When using row selection with server-side data (sorting, filtering, or pagination),
|
|
104
|
+
* you MUST provide a custom `getRowId` function. The default uses array index, which breaks
|
|
105
|
+
* selection when data is reordered.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```svelte
|
|
109
|
+
* <!-- Always provide getRowId for server-side data -->
|
|
110
|
+
* <DataTable
|
|
111
|
+
* data={serverData}
|
|
112
|
+
* columns={columns}
|
|
113
|
+
* enableRowSelection
|
|
114
|
+
* manualPagination
|
|
115
|
+
* getRowId={(row) => row.id}
|
|
116
|
+
* rowSelection={selectedRows}
|
|
117
|
+
* onRowSelectionChange={(selection) => selectedRows = selection}
|
|
118
|
+
* />
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* ## Selection State Format
|
|
122
|
+
*
|
|
123
|
+
* The `rowSelection` state is a record mapping row IDs to booleans:
|
|
124
|
+
* ```ts
|
|
125
|
+
* { "user-1": true, "user-3": true } // rows with IDs "user-1" and "user-3" are selected
|
|
126
|
+
* ```
|
|
100
127
|
*/
|
|
101
128
|
export interface DataTableSelectionProps<T> {
|
|
102
129
|
/**
|
|
103
130
|
* Enable row selection.
|
|
104
131
|
* Can be a boolean or a function that returns whether a specific row is selectable.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```svelte
|
|
135
|
+
* <!-- Conditionally disable selection for certain rows -->
|
|
136
|
+
* <DataTable
|
|
137
|
+
* enableRowSelection={(row) => row.status !== 'locked'}
|
|
138
|
+
* />
|
|
139
|
+
* ```
|
|
105
140
|
*/
|
|
106
141
|
enableRowSelection?: boolean | ((row: T) => boolean);
|
|
107
142
|
/** Enable multi-row selection (default: true when selection enabled) */
|
|
108
143
|
enableMultiRowSelection?: boolean;
|
|
109
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Controlled row selection state.
|
|
146
|
+
* Record of rowId -> boolean (true = selected).
|
|
147
|
+
*/
|
|
110
148
|
rowSelection?: RowSelectionState;
|
|
111
149
|
/** Callback when selection changes */
|
|
112
150
|
onRowSelectionChange?: (selection: RowSelectionState) => void;
|
|
113
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Function to get unique row ID.
|
|
153
|
+
*
|
|
154
|
+
* **Important**: Required for server-side data! The default uses array index,
|
|
155
|
+
* which breaks selection when data is sorted, filtered, or paginated server-side.
|
|
156
|
+
*
|
|
157
|
+
* @default (row, index) => String(index)
|
|
158
|
+
* @example getRowId={(row) => row.id}
|
|
159
|
+
*/
|
|
114
160
|
getRowId?: (row: T, index: number) => string;
|
|
115
161
|
}
|
|
116
162
|
/**
|
|
@@ -126,13 +172,47 @@ export interface DataTableVisibilityProps {
|
|
|
126
172
|
}
|
|
127
173
|
/**
|
|
128
174
|
* Props for filtering functionality (global and per-column).
|
|
175
|
+
*
|
|
176
|
+
* ## Global Filter Behavior
|
|
177
|
+
*
|
|
178
|
+
* The global filter searches across all visible columns by converting cell values to strings.
|
|
179
|
+
* This works well for primitive values (strings, numbers, booleans) but may produce
|
|
180
|
+
* unexpected results with complex objects:
|
|
181
|
+
*
|
|
182
|
+
* - Objects are stringified as `[object Object]` - not searchable
|
|
183
|
+
* - Arrays are joined with commas - `["a", "b"]` becomes `"a,b"`
|
|
184
|
+
* - Dates are converted to ISO strings
|
|
185
|
+
*
|
|
186
|
+
* **For complex data**: Use a custom `format` function on columns to control how values
|
|
187
|
+
* are displayed and searched, or implement server-side filtering with `manualFiltering`.
|
|
188
|
+
*
|
|
189
|
+
* ## Server-Side Filtering
|
|
190
|
+
*
|
|
191
|
+
* For large datasets or complex filtering logic, use server-side filtering:
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```svelte
|
|
195
|
+
* <DataTable
|
|
196
|
+
* data={serverData}
|
|
197
|
+
* columns={columns}
|
|
198
|
+
* enableFiltering
|
|
199
|
+
* manualFiltering
|
|
200
|
+
* globalFilter={searchQuery}
|
|
201
|
+
* onGlobalFilterChange={(filter) => fetchFilteredData(filter)}
|
|
202
|
+
* />
|
|
203
|
+
* ```
|
|
129
204
|
*/
|
|
130
205
|
export interface DataTableFilteringProps {
|
|
131
|
-
/** Enable filtering features */
|
|
206
|
+
/** Enable filtering features (per-column filters) */
|
|
132
207
|
enableFiltering?: boolean;
|
|
133
|
-
/** Enable global filter input */
|
|
208
|
+
/** Enable global filter input (searches all columns) */
|
|
134
209
|
enableGlobalFilter?: boolean;
|
|
135
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* Controlled global filter value.
|
|
212
|
+
*
|
|
213
|
+
* The filter searches stringified cell values across all visible columns.
|
|
214
|
+
* For complex objects, use column `format` functions to control searchable text.
|
|
215
|
+
*/
|
|
136
216
|
globalFilter?: string;
|
|
137
217
|
/** Callback when global filter changes */
|
|
138
218
|
onGlobalFilterChange?: (filter: string) => void;
|
|
@@ -140,7 +220,12 @@ export interface DataTableFilteringProps {
|
|
|
140
220
|
columnFilters?: ColumnFiltersState;
|
|
141
221
|
/** Callback when column filters change */
|
|
142
222
|
onColumnFiltersChange?: (filters: ColumnFiltersState) => void;
|
|
143
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* Use server-side filtering (manual mode).
|
|
225
|
+
*
|
|
226
|
+
* When enabled, the table will NOT filter data client-side. You must handle
|
|
227
|
+
* filtering in your data fetching logic and provide already-filtered data.
|
|
228
|
+
*/
|
|
144
229
|
manualFiltering?: boolean;
|
|
145
230
|
}
|
|
146
231
|
/**
|
|
@@ -162,13 +247,60 @@ export interface DataTablePaginationProps {
|
|
|
162
247
|
}
|
|
163
248
|
/**
|
|
164
249
|
* Props for virtualization (large dataset support).
|
|
250
|
+
*
|
|
251
|
+
* ## Important Constraints
|
|
252
|
+
*
|
|
253
|
+
* **Fixed Row Height Required**: Virtualization uses windowing to only render visible rows.
|
|
254
|
+
* This requires ALL rows to have the same height. If your rows have variable heights
|
|
255
|
+
* (e.g., wrapping text, expandable content), virtualization will cause visual glitches.
|
|
256
|
+
*
|
|
257
|
+
* **When to Use**: Enable virtualization when displaying 1000+ rows. For smaller datasets,
|
|
258
|
+
* standard rendering is often more performant due to virtualization overhead.
|
|
259
|
+
*
|
|
260
|
+
* ## Overscan Tuning
|
|
261
|
+
*
|
|
262
|
+
* The `virtualOverscan` prop controls how many rows are rendered outside the visible area.
|
|
263
|
+
* - **Lower values (5)**: Better performance, but may show blank areas during fast scrolling
|
|
264
|
+
* - **Higher values (20)**: Smoother scrolling, but renders more DOM nodes
|
|
265
|
+
* - **Default (10)**: Good balance for most use cases
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```svelte
|
|
269
|
+
* <!-- Large dataset with custom row height -->
|
|
270
|
+
* <DataTable
|
|
271
|
+
* {data}
|
|
272
|
+
* {columns}
|
|
273
|
+
* enableVirtualization
|
|
274
|
+
* virtualRowHeight={56}
|
|
275
|
+
* virtualOverscan={15}
|
|
276
|
+
* />
|
|
277
|
+
* ```
|
|
165
278
|
*/
|
|
166
279
|
export interface DataTableVirtualizationProps {
|
|
167
|
-
/**
|
|
280
|
+
/**
|
|
281
|
+
* Enable row virtualization for large datasets.
|
|
282
|
+
*
|
|
283
|
+
* Recommended for 1000+ rows. Requires fixed row heights.
|
|
284
|
+
* @default false
|
|
285
|
+
*/
|
|
168
286
|
enableVirtualization?: boolean;
|
|
169
|
-
/**
|
|
287
|
+
/**
|
|
288
|
+
* Height of each row in pixels for virtualization.
|
|
289
|
+
*
|
|
290
|
+
* **Important**: ALL rows must have this exact height. Variable height rows
|
|
291
|
+
* will cause visual glitches (misaligned rows, flickering).
|
|
292
|
+
*
|
|
293
|
+
* @default 48
|
|
294
|
+
*/
|
|
170
295
|
virtualRowHeight?: number;
|
|
171
|
-
/**
|
|
296
|
+
/**
|
|
297
|
+
* Number of rows to render outside the visible viewport.
|
|
298
|
+
*
|
|
299
|
+
* Higher values = smoother scrolling but more DOM nodes.
|
|
300
|
+
* Lower values = better performance but may flash during fast scrolling.
|
|
301
|
+
*
|
|
302
|
+
* @default 10
|
|
303
|
+
*/
|
|
172
304
|
virtualOverscan?: number;
|
|
173
305
|
}
|
|
174
306
|
/**
|
|
@@ -83,18 +83,69 @@ export declare function getAriaSortValue(sortDirection: false | 'asc' | 'desc'):
|
|
|
83
83
|
/**
|
|
84
84
|
* Default row ID function that uses the array index.
|
|
85
85
|
*
|
|
86
|
+
* **Warning**: This is only suitable for static, client-side data where the order
|
|
87
|
+
* never changes. For any of these scenarios, provide a custom `getRowId`:
|
|
88
|
+
*
|
|
89
|
+
* - Server-side sorting, filtering, or pagination
|
|
90
|
+
* - Data that can be reordered by the user
|
|
91
|
+
* - Data that can be inserted or deleted
|
|
92
|
+
* - Row selection that must persist across data updates
|
|
93
|
+
*
|
|
94
|
+
* Using array index with dynamic data causes:
|
|
95
|
+
* - Selection jumping to wrong rows after sort/filter
|
|
96
|
+
* - Selected state appearing on different rows after pagination
|
|
97
|
+
* - Inconsistent behavior when rows are added/removed
|
|
98
|
+
*
|
|
86
99
|
* @param _row - The row data (unused)
|
|
87
100
|
* @param index - The row index
|
|
88
101
|
* @returns String representation of the index
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* // DON'T use defaultGetRowId with server-side data
|
|
106
|
+
* // DO provide a custom getRowId:
|
|
107
|
+
* getRowId={(row) => row.id}
|
|
108
|
+
* getRowId={(row) => `${row.type}-${row.id}`}
|
|
109
|
+
* ```
|
|
89
110
|
*/
|
|
90
111
|
export declare function defaultGetRowId<T>(_row: T, index: number): string;
|
|
91
112
|
/**
|
|
92
|
-
*
|
|
93
|
-
* Caches the result based on a stable key derived from the column.
|
|
113
|
+
* A memoized function with cache management methods.
|
|
94
114
|
*/
|
|
95
|
-
export
|
|
115
|
+
export interface MemoizedFunction<TInput, TOutput> {
|
|
116
|
+
/** Call the memoized function */
|
|
117
|
+
(input: TInput): TOutput;
|
|
118
|
+
/** Clear all cached entries */
|
|
119
|
+
clear: () => void;
|
|
120
|
+
/** Get the current cache size */
|
|
121
|
+
size: () => number;
|
|
122
|
+
}
|
|
96
123
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
124
|
+
* Create a memoized converter with LRU cache eviction.
|
|
125
|
+
*
|
|
126
|
+
* Features:
|
|
127
|
+
* - Configurable maximum cache size (default: 100)
|
|
128
|
+
* - LRU (Least Recently Used) eviction when cache is full
|
|
129
|
+
* - Exposed `clear()` method to manually clear the cache
|
|
130
|
+
* - Exposed `size()` method to check current cache size
|
|
131
|
+
*
|
|
132
|
+
* @param converter - Function to convert input to output
|
|
133
|
+
* @param getKey - Function to generate a cache key from input
|
|
134
|
+
* @param maxSize - Maximum number of entries to cache (default: 100)
|
|
135
|
+
* @returns Memoized function with cache management methods
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* const memoizedConvert = createMemoizedConverter(
|
|
140
|
+
* (col) => convertColumn(col),
|
|
141
|
+
* (col) => col.id,
|
|
142
|
+
* 50 // max 50 entries
|
|
143
|
+
* );
|
|
144
|
+
*
|
|
145
|
+
* memoizedConvert(column); // Converts and caches
|
|
146
|
+
* memoizedConvert(column); // Returns cached
|
|
147
|
+
* memoizedConvert.clear(); // Clears cache
|
|
148
|
+
* memoizedConvert.size(); // Returns 0
|
|
149
|
+
* ```
|
|
99
150
|
*/
|
|
100
|
-
export declare function
|
|
151
|
+
export declare function createMemoizedConverter<TInput, TOutput>(converter: (input: TInput) => TOutput, getKey: (input: TInput) => string, maxSize?: number): MemoizedFunction<TInput, TOutput>;
|
|
@@ -181,39 +181,88 @@ export function getAriaSortValue(sortDirection) {
|
|
|
181
181
|
/**
|
|
182
182
|
* Default row ID function that uses the array index.
|
|
183
183
|
*
|
|
184
|
+
* **Warning**: This is only suitable for static, client-side data where the order
|
|
185
|
+
* never changes. For any of these scenarios, provide a custom `getRowId`:
|
|
186
|
+
*
|
|
187
|
+
* - Server-side sorting, filtering, or pagination
|
|
188
|
+
* - Data that can be reordered by the user
|
|
189
|
+
* - Data that can be inserted or deleted
|
|
190
|
+
* - Row selection that must persist across data updates
|
|
191
|
+
*
|
|
192
|
+
* Using array index with dynamic data causes:
|
|
193
|
+
* - Selection jumping to wrong rows after sort/filter
|
|
194
|
+
* - Selected state appearing on different rows after pagination
|
|
195
|
+
* - Inconsistent behavior when rows are added/removed
|
|
196
|
+
*
|
|
184
197
|
* @param _row - The row data (unused)
|
|
185
198
|
* @param index - The row index
|
|
186
199
|
* @returns String representation of the index
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```ts
|
|
203
|
+
* // DON'T use defaultGetRowId with server-side data
|
|
204
|
+
* // DO provide a custom getRowId:
|
|
205
|
+
* getRowId={(row) => row.id}
|
|
206
|
+
* getRowId={(row) => `${row.type}-${row.id}`}
|
|
207
|
+
* ```
|
|
187
208
|
*/
|
|
188
209
|
export function defaultGetRowId(_row, index) {
|
|
189
210
|
return String(index);
|
|
190
211
|
}
|
|
191
|
-
// =============================================================================
|
|
192
|
-
// Memoization Utilities
|
|
193
|
-
// =============================================================================
|
|
194
212
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
213
|
+
* Create a memoized converter with LRU cache eviction.
|
|
214
|
+
*
|
|
215
|
+
* Features:
|
|
216
|
+
* - Configurable maximum cache size (default: 100)
|
|
217
|
+
* - LRU (Least Recently Used) eviction when cache is full
|
|
218
|
+
* - Exposed `clear()` method to manually clear the cache
|
|
219
|
+
* - Exposed `size()` method to check current cache size
|
|
220
|
+
*
|
|
221
|
+
* @param converter - Function to convert input to output
|
|
222
|
+
* @param getKey - Function to generate a cache key from input
|
|
223
|
+
* @param maxSize - Maximum number of entries to cache (default: 100)
|
|
224
|
+
* @returns Memoized function with cache management methods
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```ts
|
|
228
|
+
* const memoizedConvert = createMemoizedConverter(
|
|
229
|
+
* (col) => convertColumn(col),
|
|
230
|
+
* (col) => col.id,
|
|
231
|
+
* 50 // max 50 entries
|
|
232
|
+
* );
|
|
233
|
+
*
|
|
234
|
+
* memoizedConvert(column); // Converts and caches
|
|
235
|
+
* memoizedConvert(column); // Returns cached
|
|
236
|
+
* memoizedConvert.clear(); // Clears cache
|
|
237
|
+
* memoizedConvert.size(); // Returns 0
|
|
238
|
+
* ```
|
|
197
239
|
*/
|
|
198
|
-
export function createMemoizedConverter(converter, getKey) {
|
|
240
|
+
export function createMemoizedConverter(converter, getKey, maxSize = 100) {
|
|
199
241
|
const cache = new Map();
|
|
200
|
-
|
|
242
|
+
const memoized = (input) => {
|
|
201
243
|
const key = getKey(input);
|
|
244
|
+
// Check cache first
|
|
202
245
|
const cached = cache.get(key);
|
|
203
246
|
if (cached !== undefined) {
|
|
247
|
+
// Move to end for LRU (delete and re-add)
|
|
248
|
+
cache.delete(key);
|
|
249
|
+
cache.set(key, cached);
|
|
204
250
|
return cached;
|
|
205
251
|
}
|
|
252
|
+
// Evict oldest entry if cache is full (LRU eviction)
|
|
253
|
+
if (cache.size >= maxSize) {
|
|
254
|
+
const oldestKey = cache.keys().next().value;
|
|
255
|
+
if (oldestKey !== undefined) {
|
|
256
|
+
cache.delete(oldestKey);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Convert and cache
|
|
206
260
|
const result = converter(input);
|
|
207
261
|
cache.set(key, result);
|
|
208
262
|
return result;
|
|
209
263
|
};
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
*/
|
|
215
|
-
export function clearMemoCache(_memoizedFn) {
|
|
216
|
-
// The cache is encapsulated, so we need to recreate the function
|
|
217
|
-
// This is a design trade-off for simplicity
|
|
218
|
-
// In practice, recreating the converter is the cleaner approach
|
|
264
|
+
// Attach cache management methods
|
|
265
|
+
memoized.clear = () => cache.clear();
|
|
266
|
+
memoized.size = () => cache.size;
|
|
267
|
+
return memoized;
|
|
219
268
|
}
|