Skip to content

Commit

Permalink
perf improvements and test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
pheuter committed Jul 17, 2024
1 parent 848fce6 commit e44c770
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/rare-beans-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@careswitch/svelte-data-table': patch
---

perf: filter matching, sort handling null/undefined, non-capturing group for global filter regex
96 changes: 96 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,45 @@ describe('DataTable', () => {
const ageGroupSortState = table.getSortState('ageGroup');
expect(ageSortState).not.toBe(ageGroupSortState);
});

it('should handle sorting with custom getValue returning undefined', () => {
const customColumns: ColumnDef<any>[] = [
{
id: 'customSort',
key: 'value',
name: 'Custom Sort',
sortable: true,
getValue: (row) => (row.value === 3 ? undefined : row.value)
}
];
const customData = [
{ id: 1, value: 3 },
{ id: 2, value: 1 },
{ id: 3, value: 2 }
];
const table = new DataTable({ data: customData, columns: customColumns });
table.toggleSort('customSort');
expect(table.rows[0].value).toBe(1);
expect(table.rows[1].value).toBe(2);
expect(table.rows[2].value).toBe(3);
});

it('should maintain sort stability for equal elements', () => {
const data = [
{ id: 1, value: 'A', order: 1 },
{ id: 2, value: 'B', order: 2 },
{ id: 3, value: 'A', order: 3 },
{ id: 4, value: 'C', order: 4 },
{ id: 5, value: 'B', order: 5 }
];
const columns: ColumnDef<(typeof data)[0]>[] = [
{ id: 'value', key: 'value', name: 'Value', sortable: true },
{ id: 'order', key: 'order', name: 'Order', sortable: true }
];
const table = new DataTable({ data, columns });
table.toggleSort('value');
expect(table.rows.map((r) => r.id)).toEqual([1, 3, 2, 5, 4]);
});
});

describe('Enhanced Sorting', () => {
Expand Down Expand Up @@ -332,6 +371,48 @@ describe('DataTable', () => {
table.setFilter('ageGroup', ['Young']);
expect(table.rows).toHaveLength(0); // No rows match both filters
});

it('should handle filtering with complex custom filter function', () => {
const customColumns: ColumnDef<any>[] = [
{
id: 'complexFilter',
key: 'value',
name: 'Complex Filter',
filter: (value, filterValue, row) => {
return value > filterValue && row.id % 2 === 0;
}
}
];
const customData = [
{ id: 1, value: 10 },
{ id: 2, value: 20 },
{ id: 3, value: 30 },
{ id: 4, value: 40 }
];
const table = new DataTable({ data: customData, columns: customColumns });
table.setFilter('complexFilter', [15]);
expect(table.rows).toHaveLength(2);
expect(table.rows[0].id).toBe(2);
expect(table.rows[1].id).toBe(4);
});

it('should handle filtering with extremely long filter lists', () => {
const longFilterList = Array.from({ length: 10000 }, (_, i) => i);
const table = new DataTable({ data: sampleData, columns });
table.setFilter('age', longFilterList);
expect(table.rows).toHaveLength(5); // All rows should match
});

it('should handle global filter with special regex characters', () => {
const data = [
{ id: 1, name: 'Alice (Manager)' },
{ id: 2, name: 'Bob [Developer]' }
] as any;
const table = new DataTable({ data, columns });
table.globalFilter = '(Manager)';
expect(table.rows).toHaveLength(1);
expect(table.rows[0].name).toBe('Alice (Manager)');
});
});

describe('Pagination', () => {
Expand Down Expand Up @@ -375,6 +456,21 @@ describe('DataTable', () => {
expect(table.rows).toHaveLength(5);
expect(table.totalPages).toBe(1);
});

it('should handle setting page size to 0', () => {
const table = new DataTable({ data: sampleData, columns, pageSize: 0 });
expect(table.rows).toHaveLength(5); // Should default to showing all rows
});

it('should handle navigation near total page count', () => {
const table = new DataTable({ data: sampleData, columns, pageSize: 2 });
table.currentPage = 3;
expect(table.canGoForward).toBe(false);
expect(table.canGoBack).toBe(true);
table.currentPage = 2;
expect(table.canGoForward).toBe(true);
expect(table.canGoBack).toBe(true);
});
});

describe('baseRows', () => {
Expand Down
43 changes: 32 additions & 11 deletions src/lib/DataTable.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,19 @@ type TableConfig<T> = {
* @template T The type of data items in the table.
*/
export class DataTable<T> {
#columns: ColumnDef<T>[];
#pageSize: number;

#originalData = $state<T[]>([]);
#columns = $state<ColumnDef<T>[]>([]);
#pageSize = $state(10);
#currentPage = $state(1);
#sortState = $state<{ columnId: string | null; direction: SortDirection }>({
columnId: null,
direction: null
});
#filterState = $state<{ [id: string]: Set<any> }>({});
#globalFilter = $state<string>('');
#globalFilterRegex = $state<RegExp | null>(null);

#globalFilterRegex: RegExp | null = null;
#isFilterDirty = true;
#isSortDirty = true;
#filteredData: T[] = [];
Expand Down Expand Up @@ -93,8 +94,7 @@ export class DataTable<T> {
};

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

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

if (colDef.filter) {
return Array.from(filterSet).some((filterValue) => colDef.filter!(value, filterValue, row));
for (const filterValue of filterSet) {
if (colDef.filter(value, filterValue, row)) {
return true;
}
}
return false;
}

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

if (aVal === undefined || aVal === null) return direction === 'asc' ? 1 : -1;
if (bVal === undefined || bVal === null) return direction === 'asc' ? -1 : 1;

if (colDef && colDef.sorter) {
return direction === 'asc'
? colDef.sorter(aVal, bVal, a, b)
: colDef.sorter(bVal, aVal, b, a);
}

if (typeof aVal === 'string' && typeof bVal === 'string') {
return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
}

if (aVal < bVal) return direction === 'asc' ? -1 : 1;
if (aVal > bVal) return direction === 'asc' ? 1 : -1;
return 0;
Expand Down Expand Up @@ -170,13 +182,14 @@ export class DataTable<T> {
get rows() {
// React to changes in original data, filter state, and sort state
this.#originalData;
this.#filterState;
this.#sortState;
this.#globalFilterRegex;
this.#filterState;
this.#globalFilter;

this.#applyFilters();
this.#applySort();
const startIndex = (this.currentPage - 1) * this.#pageSize;

const startIndex = (this.#currentPage - 1) * this.#pageSize;
const endIndex = startIndex + this.#pageSize;
return this.#sortedData.slice(startIndex, endIndex);
}
Expand Down Expand Up @@ -212,9 +225,10 @@ export class DataTable<T> {
get totalPages() {
// React to changes in filter state
this.#filterState;
this.#globalFilterRegex;
this.#globalFilter;

this.#applyFilters();

return Math.max(1, Math.ceil(this.#filteredData.length / this.#pageSize));
}

Expand Down Expand Up @@ -270,7 +284,14 @@ export class DataTable<T> {
*/
set globalFilter(value: string) {
this.#globalFilter = value;
this.#globalFilterRegex = value.trim() !== '' ? new RegExp(value, 'i') : null;

try {
this.#globalFilterRegex = value.trim() !== '' ? new RegExp(`(?:${value})`, 'i') : null;
} catch (error) {
console.error('Invalid regex pattern:', error);
this.#globalFilterRegex = null;
}

this.#currentPage = 1;
this.#isFilterDirty = true;
}
Expand Down

0 comments on commit e44c770

Please sign in to comment.