Skip to content

Commit f73a061

Browse files
manni497neotob
andauthored
Strongly Typed Filters and Sorting Rule (#62)
Co-authored-by: Tobias Merki <103021062+neotob@users.noreply.github.com>
1 parent e5bbc61 commit f73a061

19 files changed

Lines changed: 203 additions & 69 deletions

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- the `reset icon` will now reset the table to the initialState if provided, otherwise to the first value of the state
13+
- :boom: the `sorting` accepts now a strongly typed object instead of a list
14+
- :boom: the `columnFilters` can be used only defining a `filter type` to datatable hooks
15+
16+
### Removed
17+
18+
- :boom: utilities `getColumnFilterFromModel` and `getModelFromColumnFilter` are not exposed anymore
19+
1020
## [4.2.0] - 2024-03-22
1121

1222
### Added

src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,3 @@ export * from "./lib/ReactDataTable/ReactDataTable";
55
export * from "./lib/utils/getStronglyTypedColumnFilter";
66
export * from "./lib/useFullyControlledReactDataTable/useFullyControlledReactDataTable";
77
export * from "./lib/utils/createReactDataTableColumnHelper";
8-
export * from "./lib/utils/getModelFromColumnFilter";
9-
export * from "./lib/utils/getColumnFilterFromModel";

src/lib/ReactDataTable/ReactDataTable.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { Table, flexRender } from "@tanstack/react-table";
66
import { Table as ReactStrapTable, Input } from "reactstrap";
77
import { reactDataTableTranslations } from "../translations/translations";
88
import { ReactDataTableProps } from "./ReactDataTableProps";
9+
import { FilterModel } from "../types/TableState";
10+
import { getModelFromColumnFilter } from "../utils/getModelFromColumnFilter";
911
import { CSSProperties, Fragment } from "react";
1012
import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
1113
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
@@ -18,11 +20,11 @@ interface TableBodyProps<TData> {
1820
rowStyle?: (row: TData) => CSSProperties;
1921
}
2022

21-
/**
23+
/**b
2224
* The table renderer for the react data table
2325
* @param props according to {@link ReactDataTableProps}
2426
*/
25-
const ReactDataTable = <TData,>(props: ReactDataTableProps<TData>) => {
27+
const ReactDataTable = <TData, TFilter extends FilterModel = Record<string, never>>(props: ReactDataTableProps<TData, TFilter>) => {
2628
const {
2729
isLoading,
2830
isFetching,
@@ -147,7 +149,7 @@ const ReactDataTable = <TData,>(props: ReactDataTableProps<TData>) => {
147149
<FontAwesomeIcon
148150
style={{ cursor: "pointer", marginBottom: "4px", marginRight: "5px" }}
149151
icon={faSearch}
150-
onClick={() => onEnter(table.getState().columnFilters)}
152+
onClick={() => onEnter(getModelFromColumnFilter(table.getState().columnFilters))}
151153
/>
152154
)}
153155

@@ -156,15 +158,14 @@ const ReactDataTable = <TData,>(props: ReactDataTableProps<TData>) => {
156158
icon={faTimes}
157159
onClick={() => {
158160
if (onEnter) {
159-
onEnter([]);
161+
onEnter(getModelFromColumnFilter(table.initialState.columnFilters));
160162
}
161163

162-
table.resetColumnFilters(true);
164+
table.setColumnFilters(table.initialState.columnFilters);
163165
}}
164166
/>
165167
</>
166168
)}
167-
168169
{header.column.getCanFilter() && (
169170
<>
170171
{meta?.customFilter ? (
@@ -181,7 +182,7 @@ const ReactDataTable = <TData,>(props: ReactDataTableProps<TData>) => {
181182
}}
182183
onKeyUp={({ key }) => {
183184
if (key === "Enter" && onEnter) {
184-
onEnter(table.getState().columnFilters);
185+
onEnter(getModelFromColumnFilter(table.getState().columnFilters));
185186
}
186187
}}
187188
bsSize="sm"
@@ -201,7 +202,7 @@ const ReactDataTable = <TData,>(props: ReactDataTableProps<TData>) => {
201202
}}
202203
onKeyUp={({ key }) => {
203204
if (key === "Enter" && onEnter) {
204-
onEnter(table.getState().columnFilters);
205+
onEnter(getModelFromColumnFilter(table.getState().columnFilters));
205206
}
206207
}}
207208
bsSize="sm"

src/lib/ReactDataTable/ReactDataTableProps.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { ColumnFiltersState, Table } from "@tanstack/react-table";
1+
import { Table } from "@tanstack/react-table";
22
import { CSSProperties } from "react";
3+
import { FilterModel } from "../types/TableState";
34
import { DragAndDropOptions } from "./DragAndDropOptions";
45

56
/**
67
* The props for the ReactDataTable component
78
*/
8-
export interface ReactDataTableProps<TData> {
9+
export interface ReactDataTableProps<TData, TFilter extends FilterModel> {
910
/**
1011
* the table instance returned from useReactDataTable or useReactTable
1112
*/
@@ -65,7 +66,7 @@ export interface ReactDataTableProps<TData> {
6566
/**
6667
* callback that gets trigger by pressing enter or clicking the search icon
6768
*/
68-
onEnter?: (columnFilters: ColumnFiltersState) => void;
69+
onEnter?: (columnFilters: TFilter) => void;
6970

7071
/**
7172
* To draw the table without headers (titles + filters)

src/lib/types/ColumnFilterState.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { AllNullable } from "./NullableTypes";
2+
3+
/**
4+
* Type making the object possibly undefined if all properties are nullable
5+
*/
6+
export type ColumnFilterState<TFilter> = TFilter extends AllNullable<TFilter> ? TFilter | undefined : TFilter;

src/lib/types/NullableTypes.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Type taking all nullable props from object T
3+
*/
4+
type PickNullable<T> = {
5+
[P in keyof T as undefined extends T[P] ? P : never]: T[P];
6+
};
7+
8+
/**
9+
* Type taking all not nullable props from object T
10+
*/
11+
type PickNotNullable<T> = {
12+
[P in keyof T as undefined extends T[P] ? never : P]: T[P];
13+
};
14+
15+
/**
16+
* Type defining an object with all nullable props as optional
17+
*/
18+
type OptionalNullable<T> = {
19+
[K in keyof PickNullable<T>]?: Exclude<T[K], undefined>;
20+
} & {
21+
[K in keyof PickNotNullable<T>]: T[K];
22+
};
23+
24+
/**
25+
* Object type in which all props are nullable
26+
*/
27+
type AllNullable<T> = {
28+
[K in keyof T]: undefined extends T[K] ? T[K] : never;
29+
};
30+
31+
export { OptionalNullable, AllNullable };

src/lib/types/SortingState.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* The type sorting state
3+
*/
4+
export interface SortingState<TData> {
5+
/**
6+
* the id of the sortable row
7+
*/
8+
id: keyof TData;
9+
10+
/**
11+
* the descending
12+
*/
13+
desc: boolean;
14+
}

src/lib/types/TableState.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { CoreOptions } from "@tanstack/react-table";
2+
import { SortingState } from "./SortingState";
3+
4+
/**
5+
* Object type implementation
6+
*/
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
type FilterModel = { [k: string]: any };
9+
10+
/**
11+
* The table sorting state
12+
*/
13+
interface TableState<TData, TFilter extends FilterModel> extends Pick<CoreOptions<TData>["state"], "pagination"> {
14+
/**
15+
* The column filters state
16+
*/
17+
columnFilters: TFilter;
18+
/**
19+
* The sorting state
20+
*/
21+
sorting: SortingState<TData> | undefined;
22+
}
23+
24+
export { TableState, FilterModel };
Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
1-
import { TableState } from "@tanstack/react-table";
2-
import { useReactDataTable } from "../useReactDataTable/useReactDataTable";
1+
import { useReactDataTable } from "../useReactDataTable/useReactDataTable";
32
import { useReactDataTableProps } from "../useReactDataTable/useReactDataTableProps";
43
import { useReactDataTableResult } from "../useReactDataTable/useReactDataTableResult";
4+
import { FilterModel, TableState } from "../types/TableState";
55

66
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
77

8-
interface useFullyControlledReactDataTableProps<TData>
8+
interface useFullyControlledReactDataTableProps<TData, TFilter extends FilterModel>
99
extends WithRequired<
10-
Omit<useReactDataTableProps<TData>, "manualFiltering" | "manualPagination" | "manualSorting">,
10+
Omit<useReactDataTableProps<TData, TFilter>, "manualFiltering" | "manualPagination" | "manualSorting">,
1111
"onColumnFiltersChange" | "onPaginationChange" | "onSortingChange"
1212
> {
1313
state: {
14-
columnFilters: TableState["columnFilters"];
15-
pagination: TableState["pagination"];
16-
sorting: TableState["sorting"];
14+
columnFilters: TableState<TData, TFilter>["columnFilters"];
15+
pagination: TableState<TData, TFilter>["pagination"];
16+
sorting: TableState<TData, TFilter>["sorting"];
1717
};
1818
}
1919

2020
/**
2121
* A helper hook to use the useReactDataTable hook which is fully controlled. Usefull for server side filtering, sorting and pagination.
2222
*/
23-
const useFullyControlledReactDataTable = <TData,>(props: useFullyControlledReactDataTableProps<TData>): useReactDataTableResult<TData> =>
24-
useReactDataTable<TData>({ manualFiltering: true, manualPagination: true, manualSorting: true, ...props });
23+
const useFullyControlledReactDataTable = <TData, TFilter extends FilterModel>(
24+
props: useFullyControlledReactDataTableProps<TData, TFilter>,
25+
): useReactDataTableResult<TData, TFilter> =>
26+
useReactDataTable<TData, TFilter>({ manualFiltering: true, manualPagination: true, manualSorting: true, ...props });
2527

2628
export { useFullyControlledReactDataTable };

src/lib/useReactDataTable/useReactDataTable.tsx

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import { getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
2-
import { useReactDataTableState } from "../useReactDataTableState/useReactDataTableState";
2+
import { useReactDataTableState, useReactDataTableStateProps } from "../useReactDataTableState/useReactDataTableState";
33
import Skeleton from "react-loading-skeleton";
44
import { useReactDataTableProps } from "./useReactDataTableProps";
55
import { useReactDataTableResult } from "./useReactDataTableResult";
6+
import { getColumnFilterFromModel } from "../utils/getColumnFilterFromModel";
7+
import { FilterModel } from "../types/TableState";
8+
import { getModelFromColumnFilter } from "../utils/getModelFromColumnFilter";
9+
import { getSortingStateFromModel } from "../utils/getSortingStateFromModel";
10+
import { getModelFromSortingState } from "../utils/getModelFromSortingState";
11+
import { OptionalNullable } from "../types/NullableTypes";
612

713
/**
814
* A react hook that returns a react table instance and the state of the table
915
*/
10-
const useReactDataTable = <TData,>(props: useReactDataTableProps<TData>): useReactDataTableResult<TData> => {
16+
const useReactDataTable = <TData, TFilter extends FilterModel = Record<string, never>>(
17+
props: useReactDataTableProps<TData, TFilter>,
18+
): useReactDataTableResult<TData, TFilter> => {
1119
const {
1220
data = [],
1321
columns,
@@ -32,11 +40,11 @@ const useReactDataTable = <TData,>(props: useReactDataTableProps<TData>): useRea
3240
setColumnFilters: setColumnFiltersInternal,
3341
setPagination: setPaginationInternal,
3442
setSorting: setSortingInternal,
35-
} = useReactDataTableState({
36-
initialColumnFilters: columnFiltersInitial,
43+
} = useReactDataTableState<TData, TFilter>({
44+
initialColumnFilters: columnFiltersInitial as TFilter,
3745
initialPagination: paginationInitial,
3846
initialSorting: sortingInitial,
39-
});
47+
} as unknown as OptionalNullable<useReactDataTableStateProps<TData, TFilter>>);
4048

4149
const effectiveColumnFilters = columnFiltersExternal ?? columnFiltersInternal;
4250
const effectivePagination = paginationExternal ?? paginationInternal;
@@ -45,7 +53,7 @@ const useReactDataTable = <TData,>(props: useReactDataTableProps<TData>): useRea
4553
const effectiveOnPaginationChange = onPaginationChange ?? setPaginationInternal;
4654
const effectiveOnSortingChange = onSortingChange ?? setSortingInternal;
4755

48-
// If we active the manual filtering, we have to unset the filterfunction, else it still does automatic filtering
56+
// If we active the manual filtering, we have to unset the filter function, else it still does automatic filtering
4957
if (manualFiltering) columns.forEach((x) => (x.filterFn = undefined));
5058

5159
const internalColumns = columns.filter((x) => x.meta?.isHidden !== true);
@@ -60,22 +68,32 @@ const useReactDataTable = <TData,>(props: useReactDataTableProps<TData>): useRea
6068
columns: isLoading ? skeletonColumns : internalColumns,
6169

6270
onColumnFiltersChange: (filtersOrUpdaterFn) => {
63-
const newFilter = typeof filtersOrUpdaterFn !== "function" ? filtersOrUpdaterFn : filtersOrUpdaterFn(effectiveColumnFilters);
64-
return effectiveOnColumnFiltersChange(newFilter);
71+
const newFilter =
72+
typeof filtersOrUpdaterFn !== "function"
73+
? filtersOrUpdaterFn
74+
: filtersOrUpdaterFn(getColumnFilterFromModel(effectiveColumnFilters));
75+
return effectiveOnColumnFiltersChange(getModelFromColumnFilter(newFilter));
6576
},
6677
onPaginationChange: (paginationOrUpdaterFn) => {
6778
const newFilter = typeof paginationOrUpdaterFn !== "function" ? paginationOrUpdaterFn : paginationOrUpdaterFn(effectivePagination);
6879
return effectiveOnPaginationChange(newFilter);
6980
},
7081
onSortingChange: (sortingOrUpdaterFn) => {
71-
const newFilter = typeof sortingOrUpdaterFn !== "function" ? sortingOrUpdaterFn : sortingOrUpdaterFn(effectiveSorting);
72-
return effectiveOnSortingChange(newFilter);
82+
const newFilter =
83+
typeof sortingOrUpdaterFn !== "function" ? sortingOrUpdaterFn : sortingOrUpdaterFn(getSortingStateFromModel(effectiveSorting));
84+
return effectiveOnSortingChange(getModelFromSortingState(newFilter));
7385
},
7486

7587
state: {
76-
columnFilters: effectiveColumnFilters,
88+
columnFilters: getColumnFilterFromModel(effectiveColumnFilters),
7789
pagination: effectivePagination,
78-
sorting: effectiveSorting,
90+
sorting: getSortingStateFromModel(effectiveSorting),
91+
},
92+
93+
initialState: {
94+
columnFilters: getColumnFilterFromModel(columnFiltersInitial ?? columnFiltersExternal ?? {}),
95+
pagination: paginationInitial ?? paginationExternal,
96+
sorting: getSortingStateFromModel(sortingInitial ?? sortingExternal),
7997
},
8098

8199
getCoreRowModel: getCoreRowModel(),
@@ -99,7 +117,7 @@ const useReactDataTable = <TData,>(props: useReactDataTableProps<TData>): useRea
99117

100118
return {
101119
table,
102-
columnFilters: effectiveColumnFilters,
120+
columnFilters: effectiveColumnFilters as TFilter,
103121
pagination: effectivePagination,
104122
sorting: effectiveSorting,
105123
setColumnFilters: effectiveOnColumnFiltersChange,

0 commit comments

Comments
 (0)