diff --git a/.changeset/tasty-plums-sparkle.md b/.changeset/tasty-plums-sparkle.md
new file mode 100644
index 0000000..e2523dd
--- /dev/null
+++ b/.changeset/tasty-plums-sparkle.md
@@ -0,0 +1,5 @@
+---
+'@careswitch/svelte-data-table': minor
+---
+
+Introduce `id` in column definition to allow multiple columns for the same key
diff --git a/README.md b/README.md
index 7c9ecf8..9779b84 100644
--- a/README.md
+++ b/README.md
@@ -33,9 +33,9 @@ _Requires Svelte 5 peer dependency_
{ id: 2, name: 'Jane Doe', status: 'inactive' }
],
columns: [
- { key: 'id', name: 'ID' },
- { key: 'name', name: 'Name' },
- { key: 'status', name: 'Status' }
+ { id: 'id', key: 'id', name: 'ID' },
+ { id: 'name', key: 'name', name: 'Name' },
+ { id: 'status', key: 'status', name: 'Status' }
]
});
@@ -43,7 +43,7 @@ _Requires Svelte 5 peer dependency_
- {#each table.columns as column (column.key)}
+ {#each table.columns as column (column.id)}
{column.name} |
{/each}
@@ -51,7 +51,7 @@ _Requires Svelte 5 peer dependency_
{#each table.rows as row (row.id)}
- {#each table.columns as column (column.key)}
+ {#each table.columns as column (column.id)}
{row[column.key]} |
{/each}
diff --git a/src/index.test.ts b/src/index.test.ts
index 41c9b8d..d2f3f8f 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -11,9 +11,16 @@ describe('DataTable', () => {
];
const columns = [
- { key: 'id', name: 'ID', sortable: true },
- { key: 'name', name: 'Name', sortable: true },
- { key: 'age', name: 'Age', sortable: true }
+ { id: 'id', key: 'id', name: 'ID', sortable: true },
+ { id: 'name', key: 'name', name: 'Name', sortable: true },
+ { id: 'age', key: 'age', name: 'Age', sortable: true },
+ {
+ id: 'ageGroup',
+ key: 'age',
+ name: 'Age Group',
+ sortable: true,
+ getValue: (row) => (row.age < 30 ? 'Young' : 'Adult')
+ }
] satisfies ColumnDef<(typeof sampleData)[0]>[];
describe('Initialization', () => {
@@ -60,12 +67,18 @@ describe('DataTable', () => {
const incompleteData = [
{ id: 1, name: 'Alice' },
{ id: 2, age: 25 }
- ];
+ ] as any[];
const table = new DataTable({ data: incompleteData, columns });
expect(table.rows).toHaveLength(2);
expect(table.rows[0].age).toBeUndefined();
expect(table.rows[1].name).toBeUndefined();
});
+
+ it('should handle multiple columns with the same key', () => {
+ const table = new DataTable({ data: sampleData, columns });
+ expect(table.columns).toHaveLength(4);
+ expect(table.columns.filter((col) => col.key === 'age')).toHaveLength(2);
+ });
});
describe('Sorting', () => {
@@ -101,7 +114,7 @@ describe('DataTable', () => {
it('should handle sorting with custom sorter function', () => {
const customColumns = columns.map((col) =>
- col.key === 'name'
+ col.id === 'name'
? {
...col,
sorter: (a, b) => b.name.localeCompare(a.name) // Reverse alphabetical order
@@ -136,7 +149,7 @@ describe('DataTable', () => {
{ id: 1, name: 'Alice', age: null },
{ id: 2, name: 'Bob', age: null },
{ id: 3, name: 'Charlie', age: null }
- ];
+ ] as any[];
const table = new DataTable({ data: nullData, columns });
table.toggleSort('age');
expect(table.rows).toHaveLength(3);
@@ -144,6 +157,14 @@ describe('DataTable', () => {
expect(table.rows[0].name).toBe('Alice');
expect(table.rows[2].name).toBe('Charlie');
});
+
+ it('should sort independently for columns with the same key', () => {
+ const table = new DataTable({ data: sampleData, columns });
+ table.toggleSort('age');
+ const ageSortState = table.getSortState('age');
+ const ageGroupSortState = table.getSortState('ageGroup');
+ expect(ageSortState).not.toBe(ageGroupSortState);
+ });
});
describe('Filtering', () => {
@@ -167,6 +188,13 @@ describe('DataTable', () => {
).toBe(true);
});
+ it('should filter derived columns', () => {
+ const table = new DataTable({ data: sampleData, columns });
+ table.setFilter('ageGroup', ['Young']);
+ expect(table.rows).toHaveLength(2);
+ expect(table.rows.every((row) => row.age < 30)).toBe(true);
+ });
+
it('should clear filter', () => {
const table = new DataTable({ data: sampleData, columns });
table.setFilter('age', [30]);
@@ -231,6 +259,13 @@ describe('DataTable', () => {
expect(table.rows).toHaveLength(2);
expect(table.rows.every((row) => row.name === 'Alice' || row.name === 'Bob')).toBe(true);
});
+
+ it('should filter independently for columns with the same key', () => {
+ const table = new DataTable({ data: sampleData, columns });
+ table.setFilter('age', [30, 35]);
+ table.setFilter('ageGroup', ['Young']);
+ expect(table.rows).toHaveLength(0); // No rows match both filters
+ });
});
describe('Pagination', () => {
@@ -276,19 +311,7 @@ describe('DataTable', () => {
});
});
- describe('baseRows functionality', () => {
- const sampleData = [
- { id: 1, name: 'Alice', age: 30 },
- { id: 2, name: 'Bob', age: 25 },
- { id: 3, name: 'Charlie', age: 35 }
- ];
-
- const columns = [
- { key: 'id', name: 'ID', sortable: true },
- { key: 'name', name: 'Name', sortable: true },
- { key: 'age', name: 'Age', sortable: true }
- ] satisfies ColumnDef<(typeof sampleData)[0]>[];
-
+ describe('baseRows', () => {
it('should return the original data when getting baseRows', () => {
const table = new DataTable({ data: sampleData, columns });
expect(table.baseRows).toEqual(sampleData);
diff --git a/src/lib/DataTable.svelte.ts b/src/lib/DataTable.svelte.ts
index deb2300..841ceaa 100644
--- a/src/lib/DataTable.svelte.ts
+++ b/src/lib/DataTable.svelte.ts
@@ -3,6 +3,7 @@ type Sorter = (a: T, b: T) => number;
type Filter = (value: V, filterValue: V, row: T) => boolean;
export interface ColumnDef {
+ id: string;
key: keyof T;
name: string;
sortable?: boolean;
@@ -17,9 +18,9 @@ type TableConfig = {
data: T[];
columns: ColumnDef[];
pageSize?: number;
- initialSort?: keyof T;
+ initialSort?: string;
initialSortDirection?: SortDirection;
- initialFilters?: { [K in keyof T]?: any[] };
+ initialFilters?: { [id: string]: any[] };
};
/**
@@ -31,11 +32,11 @@ export class DataTable {
#columns = $state[]>([]);
#pageSize = $state(10);
#currentPage = $state(1);
- #sortState = $state<{ column: keyof T | null; direction: SortDirection }>({
- column: null,
+ #sortState = $state<{ columnId: string | null; direction: SortDirection }>({
+ columnId: null,
direction: null
});
- #filterState = $state<{ [K in keyof T]: Set }>({} as any);
+ #filterState = $state<{ [id: string]: Set }>({});
#globalFilter = $state('');
#globalFilterRegex = $state(null);
@@ -55,51 +56,54 @@ export class DataTable {
this.#pageSize = config.pageSize || 10;
if (config.initialSort) {
this.#sortState = {
- column: config.initialSort,
+ columnId: config.initialSort,
direction: config.initialSortDirection || 'asc'
};
}
this.#initializeFilterState(config.initialFilters);
}
- #initializeFilterState(initialFilters?: { [K in keyof T]?: any[] }) {
+ #initializeFilterState(initialFilters?: { [id: string]: any[] }) {
this.#columns.forEach((column) => {
- const initialFilterValues = initialFilters?.[column.key];
+ const initialFilterValues = initialFilters?.[column.id];
if (initialFilterValues) {
- this.#filterState[column.key] = new Set(initialFilterValues);
+ this.#filterState[column.id] = new Set(initialFilterValues);
} else {
- this.#filterState[column.key] = new Set();
+ this.#filterState[column.id] = new Set();
}
});
}
- #getColumnDef(key: keyof T): ColumnDef | undefined {
- return this.#columns.find((col) => col.key === key);
+ #getColumnDef(id: string): ColumnDef | undefined {
+ return this.#columns.find((col) => col.id === id);
}
- #getValue(row: T, key: K): T[K] | any {
- const colDef = this.#getColumnDef(key);
- return colDef && colDef.getValue ? colDef.getValue(row) : row[key];
+ #getValue(row: T, columnId: string): any {
+ const colDef = this.#getColumnDef(columnId);
+ if (!colDef) return undefined;
+ return colDef.getValue ? colDef.getValue(row) : row[colDef.key];
}
#matchesGlobalFilter = (row: T): boolean => {
if (!this.#globalFilterRegex) return true;
return this.#columns.some((col) => {
- const value = this.#getValue(row, col.key);
+ const value = this.#getValue(row, col.id);
return typeof value === 'string' && this.#globalFilterRegex!.test(value);
});
};
#matchesFilters = (row: T): boolean => {
- return (Object.keys(this.#filterState) as Array).every((key) => {
- const filterSet = this.#filterState[key];
+ return Object.keys(this.#filterState).every((columnId) => {
+ const filterSet = this.#filterState[columnId];
if (!filterSet || filterSet.size === 0) return true;
- const colDef = this.#getColumnDef(key);
- const value = this.#getValue(row, key);
+ const colDef = this.#getColumnDef(columnId);
+ if (!colDef) return true;
- if (colDef && colDef.filter) {
+ const value = this.#getValue(row, columnId);
+
+ if (colDef.filter) {
return Array.from(filterSet).some((filterValue) => colDef.filter!(value, filterValue, row));
}
@@ -120,15 +124,15 @@ export class DataTable {
#applySort() {
if (!this.#isSortDirty) return;
- const { column, direction } = this.#sortState;
- if (column && direction) {
- const colDef = this.#getColumnDef(column);
+ const { columnId, direction } = this.#sortState;
+ if (columnId && direction) {
+ const colDef = this.#getColumnDef(columnId);
this.#sortedData = [...this.#filteredData].sort((a, b) => {
if (colDef && colDef.sorter) {
return direction === 'asc' ? colDef.sorter(a, b) : colDef.sorter(b, a);
}
- const aVal = this.#getValue(a, column);
- const bVal = this.#getValue(b, column);
+ const aVal = this.#getValue(a, columnId);
+ const bVal = this.#getValue(b, columnId);
if (aVal < bVal) return direction === 'asc' ? -1 : 1;
if (aVal > bVal) return direction === 'asc' ? 1 : -1;
return 0;
@@ -251,17 +255,17 @@ export class DataTable {
}
/**
- * Toggles the sort state for a given column.
- * @param {keyof T} column - The key of the column to toggle sorting for.
+ * Toggles the sort direction for the specified column.
+ * @param {string} columnId - The column id to toggle sorting for.
*/
- toggleSort = (column: keyof T) => {
- const colDef = this.#getColumnDef(column);
+ toggleSort = (columnId: string) => {
+ const colDef = this.#getColumnDef(columnId);
if (!colDef || colDef.sortable === false) return;
this.#isSortDirty = true;
- if (this.#sortState.column === column) {
+ if (this.#sortState.columnId === columnId) {
this.#sortState = {
- column,
+ columnId,
direction:
this.#sortState.direction === 'asc'
? 'desc'
@@ -270,77 +274,71 @@ export class DataTable {
: 'asc'
};
} else {
- this.#sortState = { column, direction: 'asc' };
+ this.#sortState = { columnId, direction: 'asc' };
}
};
/**
- * Gets the current sort state for a given column.
- * @param {keyof T} column - The key of the column to get the sort state for.
- * @returns {SortDirection} The current sort direction for the column.
+ * Gets the current sort state for the specified column.
+ * @param {string} columnId - The column id to get the sort state for.
*/
- getSortState = (column: keyof T): SortDirection => {
- return this.#sortState.column === column ? this.#sortState.direction : null;
+ getSortState = (columnId: string): SortDirection => {
+ return this.#sortState.columnId === columnId ? this.#sortState.direction : null;
};
/**
- * Checks if a column is sortable.
- * @param {keyof T} column - The key of the column to check.
- * @returns {boolean} True if the column is sortable, false otherwise.
+ * Indicates whether the specified column is sortable.
+ * @param {string} columnId - The column id to check.
*/
- isSortable = (column: keyof T): boolean => {
- const colDef = this.#getColumnDef(column);
+ isSortable = (columnId: string): boolean => {
+ const colDef = this.#getColumnDef(columnId);
return colDef?.sortable !== false;
};
/**
- * Sets the filter for a specific column.
- * @param {K} column - The key of the column to set the filter for.
- * @param {any[]} values - The values to set as the filter.
- * @template K
+ * Sets the filter values for the specified column.
+ * @param {string} columnId - The column id to set the filter values for.
+ * @param {any[]} values - The filter values to set.
*/
- setFilter = (column: K, values: any[]) => {
+ setFilter = (columnId: string, values: any[]) => {
this.#isFilterDirty = true;
- this.#filterState = { ...this.#filterState, [column]: new Set(values) };
+ this.#filterState = { ...this.#filterState, [columnId]: new Set(values) };
this.#currentPage = 1;
};
/**
- * Clears the filter for a specific column.
- * @param {keyof T} column - The key of the column to clear the filter for.
+ * Clears the filter values for the specified column.
+ * @param {string} columnId - The column id to clear the filter values for.
*/
- clearFilter = (column: keyof T) => {
+ clearFilter = (columnId: string) => {
this.#isFilterDirty = true;
- this.#filterState = { ...this.#filterState, [column]: new Set() };
+ this.#filterState = { ...this.#filterState, [columnId]: new Set() };
this.#currentPage = 1;
};
/**
- * Toggles a filter value for a specific column.
- * @param {K} column - The key of the column to toggle the filter for.
- * @param {any} value - The value to toggle in the filter.
- * @template K
+ * Toggles the filter value for the specified column.
+ * @param {string} columnId - The column id to toggle the filter value for.
+ * @param {any} value - The filter value to toggle.
*/
- toggleFilter = (column: K, value: any) => {
+ toggleFilter = (columnId: string, value: any) => {
this.#isFilterDirty = true;
this.#filterState = {
...this.#filterState,
- [column]: this.isFilterActive(column, value)
- ? new Set([...this.#filterState[column]].filter((v) => v !== value))
- : new Set([...this.#filterState[column], value])
+ [columnId]: this.isFilterActive(columnId, value)
+ ? new Set([...this.#filterState[columnId]].filter((v) => v !== value))
+ : new Set([...this.#filterState[columnId], value])
};
this.#currentPage = 1;
};
/**
- * Checks if a specific filter value is active for a column.
- * @param {K} column - The key of the column to check the filter for.
- * @param {any} value - The value to check in the filter.
- * @returns {boolean} True if the filter is active for the given value, false otherwise.
- * @template K
+ * Indicates whether the specified filter value is active for the specified column.
+ * @param {string} columnId - The column id to check.
+ * @param {any} value - The filter value to check.
*/
- isFilterActive = (column: K, value: any): boolean => {
- return this.#filterState[column].has(value);
+ isFilterActive = (columnId: string, value: any): boolean => {
+ return this.#filterState[columnId].has(value);
};
}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 996fc83..f4cf84f 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -17,9 +17,9 @@
pageSize: 25,
data: data.users,
columns: [
- { key: 'id', name: 'ID' },
- { key: 'name', name: 'Name' },
- { key: 'status', name: 'Status', sortable: false }
+ { id: 'id', key: 'id', name: 'ID' },
+ { id: 'name', key: 'name', name: 'Name' },
+ { id: 'status', key: 'status', name: 'Status', sortable: false }
]
});
@@ -83,19 +83,19 @@
- {#each table.columns as column (column.key)}
+ {#each table.columns as column (column.id)}