Skip to content

Commit e44c770

Browse files
committed
perf improvements and test coverage
1 parent 848fce6 commit e44c770

File tree

3 files changed

+133
-11
lines changed

3 files changed

+133
-11
lines changed

Diff for: .changeset/rare-beans-reflect.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@careswitch/svelte-data-table': patch
3+
---
4+
5+
perf: filter matching, sort handling null/undefined, non-capturing group for global filter regex

Diff for: src/index.test.ts

+96
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,45 @@ describe('DataTable', () => {
150150
const ageGroupSortState = table.getSortState('ageGroup');
151151
expect(ageSortState).not.toBe(ageGroupSortState);
152152
});
153+
154+
it('should handle sorting with custom getValue returning undefined', () => {
155+
const customColumns: ColumnDef<any>[] = [
156+
{
157+
id: 'customSort',
158+
key: 'value',
159+
name: 'Custom Sort',
160+
sortable: true,
161+
getValue: (row) => (row.value === 3 ? undefined : row.value)
162+
}
163+
];
164+
const customData = [
165+
{ id: 1, value: 3 },
166+
{ id: 2, value: 1 },
167+
{ id: 3, value: 2 }
168+
];
169+
const table = new DataTable({ data: customData, columns: customColumns });
170+
table.toggleSort('customSort');
171+
expect(table.rows[0].value).toBe(1);
172+
expect(table.rows[1].value).toBe(2);
173+
expect(table.rows[2].value).toBe(3);
174+
});
175+
176+
it('should maintain sort stability for equal elements', () => {
177+
const data = [
178+
{ id: 1, value: 'A', order: 1 },
179+
{ id: 2, value: 'B', order: 2 },
180+
{ id: 3, value: 'A', order: 3 },
181+
{ id: 4, value: 'C', order: 4 },
182+
{ id: 5, value: 'B', order: 5 }
183+
];
184+
const columns: ColumnDef<(typeof data)[0]>[] = [
185+
{ id: 'value', key: 'value', name: 'Value', sortable: true },
186+
{ id: 'order', key: 'order', name: 'Order', sortable: true }
187+
];
188+
const table = new DataTable({ data, columns });
189+
table.toggleSort('value');
190+
expect(table.rows.map((r) => r.id)).toEqual([1, 3, 2, 5, 4]);
191+
});
153192
});
154193

155194
describe('Enhanced Sorting', () => {
@@ -332,6 +371,48 @@ describe('DataTable', () => {
332371
table.setFilter('ageGroup', ['Young']);
333372
expect(table.rows).toHaveLength(0); // No rows match both filters
334373
});
374+
375+
it('should handle filtering with complex custom filter function', () => {
376+
const customColumns: ColumnDef<any>[] = [
377+
{
378+
id: 'complexFilter',
379+
key: 'value',
380+
name: 'Complex Filter',
381+
filter: (value, filterValue, row) => {
382+
return value > filterValue && row.id % 2 === 0;
383+
}
384+
}
385+
];
386+
const customData = [
387+
{ id: 1, value: 10 },
388+
{ id: 2, value: 20 },
389+
{ id: 3, value: 30 },
390+
{ id: 4, value: 40 }
391+
];
392+
const table = new DataTable({ data: customData, columns: customColumns });
393+
table.setFilter('complexFilter', [15]);
394+
expect(table.rows).toHaveLength(2);
395+
expect(table.rows[0].id).toBe(2);
396+
expect(table.rows[1].id).toBe(4);
397+
});
398+
399+
it('should handle filtering with extremely long filter lists', () => {
400+
const longFilterList = Array.from({ length: 10000 }, (_, i) => i);
401+
const table = new DataTable({ data: sampleData, columns });
402+
table.setFilter('age', longFilterList);
403+
expect(table.rows).toHaveLength(5); // All rows should match
404+
});
405+
406+
it('should handle global filter with special regex characters', () => {
407+
const data = [
408+
{ id: 1, name: 'Alice (Manager)' },
409+
{ id: 2, name: 'Bob [Developer]' }
410+
] as any;
411+
const table = new DataTable({ data, columns });
412+
table.globalFilter = '(Manager)';
413+
expect(table.rows).toHaveLength(1);
414+
expect(table.rows[0].name).toBe('Alice (Manager)');
415+
});
335416
});
336417

337418
describe('Pagination', () => {
@@ -375,6 +456,21 @@ describe('DataTable', () => {
375456
expect(table.rows).toHaveLength(5);
376457
expect(table.totalPages).toBe(1);
377458
});
459+
460+
it('should handle setting page size to 0', () => {
461+
const table = new DataTable({ data: sampleData, columns, pageSize: 0 });
462+
expect(table.rows).toHaveLength(5); // Should default to showing all rows
463+
});
464+
465+
it('should handle navigation near total page count', () => {
466+
const table = new DataTable({ data: sampleData, columns, pageSize: 2 });
467+
table.currentPage = 3;
468+
expect(table.canGoForward).toBe(false);
469+
expect(table.canGoBack).toBe(true);
470+
table.currentPage = 2;
471+
expect(table.canGoForward).toBe(true);
472+
expect(table.canGoBack).toBe(true);
473+
});
378474
});
379475

380476
describe('baseRows', () => {

Diff for: src/lib/DataTable.svelte.ts

+32-11
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,19 @@ type TableConfig<T> = {
2828
* @template T The type of data items in the table.
2929
*/
3030
export class DataTable<T> {
31+
#columns: ColumnDef<T>[];
32+
#pageSize: number;
33+
3134
#originalData = $state<T[]>([]);
32-
#columns = $state<ColumnDef<T>[]>([]);
33-
#pageSize = $state(10);
3435
#currentPage = $state(1);
3536
#sortState = $state<{ columnId: string | null; direction: SortDirection }>({
3637
columnId: null,
3738
direction: null
3839
});
3940
#filterState = $state<{ [id: string]: Set<any> }>({});
4041
#globalFilter = $state<string>('');
41-
#globalFilterRegex = $state<RegExp | null>(null);
4242

43+
#globalFilterRegex: RegExp | null = null;
4344
#isFilterDirty = true;
4445
#isSortDirty = true;
4546
#filteredData: T[] = [];
@@ -93,8 +94,7 @@ export class DataTable<T> {
9394
};
9495

9596
#matchesFilters = (row: T): boolean => {
96-
return Object.keys(this.#filterState).every((columnId) => {
97-
const filterSet = this.#filterState[columnId];
97+
return Object.entries(this.#filterState).every(([columnId, filterSet]) => {
9898
if (!filterSet || filterSet.size === 0) return true;
9999

100100
const colDef = this.#getColumnDef(columnId);
@@ -103,7 +103,12 @@ export class DataTable<T> {
103103
const value = this.#getValue(row, columnId);
104104

105105
if (colDef.filter) {
106-
return Array.from(filterSet).some((filterValue) => colDef.filter!(value, filterValue, row));
106+
for (const filterValue of filterSet) {
107+
if (colDef.filter(value, filterValue, row)) {
108+
return true;
109+
}
110+
}
111+
return false;
107112
}
108113

109114
return filterSet.has(value);
@@ -130,12 +135,19 @@ export class DataTable<T> {
130135
const aVal = this.#getValue(a, columnId);
131136
const bVal = this.#getValue(b, columnId);
132137

138+
if (aVal === undefined || aVal === null) return direction === 'asc' ? 1 : -1;
139+
if (bVal === undefined || bVal === null) return direction === 'asc' ? -1 : 1;
140+
133141
if (colDef && colDef.sorter) {
134142
return direction === 'asc'
135143
? colDef.sorter(aVal, bVal, a, b)
136144
: colDef.sorter(bVal, aVal, b, a);
137145
}
138146

147+
if (typeof aVal === 'string' && typeof bVal === 'string') {
148+
return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
149+
}
150+
139151
if (aVal < bVal) return direction === 'asc' ? -1 : 1;
140152
if (aVal > bVal) return direction === 'asc' ? 1 : -1;
141153
return 0;
@@ -170,13 +182,14 @@ export class DataTable<T> {
170182
get rows() {
171183
// React to changes in original data, filter state, and sort state
172184
this.#originalData;
173-
this.#filterState;
174185
this.#sortState;
175-
this.#globalFilterRegex;
186+
this.#filterState;
187+
this.#globalFilter;
176188

177189
this.#applyFilters();
178190
this.#applySort();
179-
const startIndex = (this.currentPage - 1) * this.#pageSize;
191+
192+
const startIndex = (this.#currentPage - 1) * this.#pageSize;
180193
const endIndex = startIndex + this.#pageSize;
181194
return this.#sortedData.slice(startIndex, endIndex);
182195
}
@@ -212,9 +225,10 @@ export class DataTable<T> {
212225
get totalPages() {
213226
// React to changes in filter state
214227
this.#filterState;
215-
this.#globalFilterRegex;
228+
this.#globalFilter;
216229

217230
this.#applyFilters();
231+
218232
return Math.max(1, Math.ceil(this.#filteredData.length / this.#pageSize));
219233
}
220234

@@ -270,7 +284,14 @@ export class DataTable<T> {
270284
*/
271285
set globalFilter(value: string) {
272286
this.#globalFilter = value;
273-
this.#globalFilterRegex = value.trim() !== '' ? new RegExp(value, 'i') : null;
287+
288+
try {
289+
this.#globalFilterRegex = value.trim() !== '' ? new RegExp(`(?:${value})`, 'i') : null;
290+
} catch (error) {
291+
console.error('Invalid regex pattern:', error);
292+
this.#globalFilterRegex = null;
293+
}
294+
274295
this.#currentPage = 1;
275296
this.#isFilterDirty = true;
276297
}

0 commit comments

Comments
 (0)