Skip to content

Commit 43dc77e

Browse files
authored
feat(Add optional sort function to columns): Add optional sort function to columns (#12)
The default sorting sorts by strings, however in the case where you have more complex data such as dates, you may need to specify your own sorting function
1 parent 22f1b96 commit 43dc77e

8 files changed

Lines changed: 151 additions & 41 deletions

File tree

package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@
3838
"devDependencies": {
3939
"@testing-library/jest-dom": "^5.11.3",
4040
"@testing-library/react": "^10.4.8",
41+
"@types/faker": "^4.1.12",
4142
"@types/react": "^16.9.46",
4243
"@types/react-dom": "^16.9.8",
4344
"codecov": "^3.7.2",
45+
"faker": "^4.1.0",
4446
"husky": "^4.2.5",
4547
"react": "^16.13.1",
4648
"react-dom": "^16.13.1",

src/hooks.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
DataType,
1010
UseTableReturnType,
1111
UseTableOptionsType,
12+
RowType,
1213
} from './types';
1314
import { byTextAscending, byTextDescending } from './utils';
1415

@@ -24,9 +25,22 @@ const createReducer = <T extends DataType>() => (
2425

2526
let isAscending = null;
2627

28+
let sortedRows: RowType<T>[] = [];
29+
2730
const columnCopy = state.columns.map(column => {
2831
if (action.columnName === column.name) {
2932
isAscending = column.sorted.asc;
33+
if (column.sort) {
34+
sortedRows = isAscending
35+
? state.rows.sort(column.sort)
36+
: state.rows.sort(column.sort).reverse();
37+
} else {
38+
sortedRows = state.rows.sort(
39+
isAscending
40+
? byTextAscending(object => object.original[action.columnName])
41+
: byTextDescending(object => object.original[action.columnName])
42+
);
43+
}
3044
return {
3145
...column,
3246
sorted: {
@@ -47,11 +61,7 @@ const createReducer = <T extends DataType>() => (
4761
return {
4862
...state,
4963
columns: columnCopy,
50-
rows: state.rows.sort(
51-
isAscending
52-
? byTextAscending(object => object.original[action.columnName])
53-
: byTextDescending(object => object.original[action.columnName])
54-
),
64+
rows: sortedRows,
5565
columnsById: getColumnsById(columnCopy),
5666
};
5767
case 'GLOBAL_FILTER':
@@ -154,7 +164,7 @@ const createReducer = <T extends DataType>() => (
154164
};
155165

156166
export const useTable = <T extends DataType>(
157-
columns: ColumnType[],
167+
columns: ColumnType<T>[],
158168
data: T[],
159169
options?: UseTableOptionsType<T>
160170
): UseTableReturnType<T> => {
@@ -245,7 +255,7 @@ const makeRender = <T extends DataType>(
245255

246256
const sortDataInOrder = <T extends DataType>(
247257
data: T[],
248-
columns: ColumnType[]
258+
columns: ColumnType<T>[]
249259
): T[] => {
250260
return data.map((row: any) => {
251261
const newRow: any = {};
@@ -259,7 +269,9 @@ const sortDataInOrder = <T extends DataType>(
259269
});
260270
};
261271

262-
const getColumnsById = (columns: ColumnType[]): ColumnByIdsType => {
272+
const getColumnsById = <T extends DataType>(
273+
columns: ColumnType<T>[]
274+
): ColumnByIdsType => {
263275
const columnsById: ColumnByIdsType = {};
264276
columns.forEach(column => {
265277
const col: any = {

src/test/makeData.tsx

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ColumnType } from 'types';
1+
import { ColumnType, DataType } from 'types';
2+
import { date } from 'faker';
23

34
// from json-generator.com
45
const randomData = [
@@ -275,11 +276,51 @@ export type UserType = {
275276
address: string;
276277
};
277278

278-
export const makeData = (
279+
export const makeData = <T extends {}>(
279280
rowNum: number
280-
): { columns: ColumnType[]; data: UserType[] } => {
281+
): { columns: ColumnType<T>[]; data: UserType[] } => {
281282
return {
282283
columns,
283284
data: randomData.slice(0, rowNum),
284285
};
285286
};
287+
288+
export const makeSimpleData = <T extends DataType>() => {
289+
const columns: ColumnType<T>[] = [
290+
{
291+
name: 'firstName',
292+
label: 'First Name',
293+
},
294+
{
295+
name: 'lastName',
296+
label: 'Last Name',
297+
},
298+
{
299+
name: 'birthDate',
300+
label: 'Birth Date',
301+
},
302+
];
303+
304+
const recentDate = date.recent();
305+
const pastDate = date.past(undefined, recentDate);
306+
const oldestDate = date.past(100, pastDate);
307+
308+
const data = [
309+
{
310+
firstName: 'Samwise',
311+
lastName: 'Gamgee',
312+
birthDate: pastDate.toISOString(),
313+
},
314+
{
315+
firstName: 'Frodo',
316+
lastName: 'Baggins',
317+
birthDate: recentDate.toISOString(), // must be youngest for tests
318+
},
319+
{
320+
firstName: 'Bilbo',
321+
lastName: 'Baggins',
322+
birthDate: oldestDate.toISOString(),
323+
},
324+
];
325+
return { columns, data };
326+
};

src/test/selectionGlobalFiltering.spec.tsx

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import React, { useCallback, useState } from 'react';
22
import { render, fireEvent } from '@testing-library/react';
33
import '@testing-library/jest-dom/extend-expect';
44
import { useTable } from '../hooks';
5-
import { ColumnType, RowType } from '../types';
6-
import { makeData, UserType } from './makeData';
5+
import { ColumnType, RowType, DataType } from '../types';
6+
import { makeData } from './makeData';
77

88
const columns = [
99
{
@@ -27,17 +27,12 @@ const data = [
2727
},
2828
];
2929

30-
type TestDataType = {
31-
firstName: string;
32-
lastName: string;
33-
};
34-
35-
const TableWithSelection = ({
30+
const TableWithSelection = <T extends DataType>({
3631
columns,
3732
data,
3833
}: {
39-
columns: ColumnType[];
40-
data: Object[];
34+
columns: ColumnType<T>[];
35+
data: T[];
4136
}) => {
4237
const { headers, rows, selectRow, selectedRows, toggleAll } = useTable(
4338
columns,
@@ -122,14 +117,14 @@ test('Should be able to select rows', async () => {
122117
expect(rtl.queryAllByTestId('selected-row')).toHaveLength(0);
123118
});
124119

125-
const TableWithFilter = ({
120+
const TableWithFilter = <T extends DataType>({
126121
columns,
127122
data,
128123
filter,
129124
}: {
130-
columns: ColumnType[];
131-
data: TestDataType[];
132-
filter: (row: RowType<TestDataType>[]) => RowType<TestDataType>[];
125+
columns: ColumnType<T>[];
126+
data: T[];
127+
filter: (row: RowType<T>[]) => RowType<T>[];
133128
}) => {
134129
const { headers, rows } = useTable(columns, data, {
135130
filter,
@@ -171,20 +166,20 @@ test('Should be able to filter rows', () => {
171166
expect(rtl.getAllByTestId('table-row')).toHaveLength(1);
172167
});
173168

174-
const TableWithSelectionAndFiltering = ({
169+
const TableWithSelectionAndFiltering = <T extends DataType>({
175170
columns,
176171
data,
177172
}: {
178-
columns: ColumnType[];
179-
data: UserType[];
173+
columns: ColumnType<T>[];
174+
data: T[];
180175
}) => {
181176
const [searchString, setSearchString] = useState('');
182177
const [filterOn, setFilterOn] = useState(false);
183178

184179
const { headers, rows, selectRow, selectedRows } = useTable(columns, data, {
185180
selectable: true,
186181
filter: useCallback(
187-
(rows: RowType<UserType>[]) => {
182+
(rows: RowType<T>[]) => {
188183
return rows.filter(row => {
189184
return (
190185
row.cells.filter(cell => {

src/test/sorting.spec.tsx

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import '@testing-library/jest-dom/extend-expect';
44

55
import { useTable } from '../hooks';
66
import { ColumnType } from '../types';
7-
import { makeData } from './makeData';
7+
import { makeData, makeSimpleData } from './makeData';
88

9-
const Table = ({
9+
const Table = <T extends {}>({
1010
columns,
1111
data,
1212
}: {
13-
columns: ColumnType[];
14-
data: Object[];
13+
columns: ColumnType<T>[];
14+
data: T[];
1515
}) => {
1616
const { headers, rows, toggleSort } = useTable(columns, data, {
1717
sortable: true,
@@ -75,3 +75,48 @@ test('Should render a table with sorting enabled', () => {
7575
({ getByText } = within(firstRow));
7676
expect(getByText('Yesenia')).toBeInTheDocument();
7777
});
78+
79+
test('Should sort by dates correctly', () => {
80+
const { columns, data } = makeSimpleData<{
81+
firstName: string;
82+
lastName: string;
83+
birthDate: string;
84+
}>();
85+
columns[2] = {
86+
name: 'birthDate',
87+
label: 'Birth Date',
88+
sort: (objectA, objectB) => {
89+
return (
90+
Number(new Date(objectA.original.birthDate)) -
91+
Number(new Date(objectB.original.birthDate))
92+
);
93+
},
94+
};
95+
const rtl = render(<Table columns={columns} data={data} />);
96+
97+
const dateColumn = rtl.getByTestId('column-birthDate');
98+
99+
// should be sorted in ascending order
100+
fireEvent.click(dateColumn);
101+
102+
expect(rtl.queryByTestId('sorted-birthDate')).toBeInTheDocument();
103+
104+
let firstRow = rtl.getByTestId('row-0');
105+
let lastRow = rtl.getByTestId('row-2');
106+
107+
let { getByText } = within(firstRow);
108+
expect(getByText('Bilbo')).toBeInTheDocument();
109+
({ getByText } = within(lastRow));
110+
expect(getByText('Frodo')).toBeInTheDocument();
111+
112+
// should be sorted in descending order
113+
fireEvent.click(dateColumn);
114+
115+
firstRow = rtl.getByTestId('row-0');
116+
lastRow = rtl.getByTestId('row-2');
117+
118+
({ getByText } = within(firstRow));
119+
expect(getByText('Frodo')).toBeInTheDocument();
120+
({ getByText } = within(lastRow));
121+
expect(getByText('Bilbo')).toBeInTheDocument();
122+
});

src/test/table.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const Table = ({
3030
columns,
3131
data,
3232
}: {
33-
columns: ColumnType[];
33+
columns: any[];
3434
data: { firstName: string; lastName: string }[];
3535
}) => {
3636
const { headers, rows } = useTable<{ firstName: string; lastName: string }>(
@@ -87,7 +87,7 @@ test('Should be equal regardless of field order in data', () => {
8787
expect(normalTl.asFragment()).toEqual(reverseTl.asFragment());
8888
});
8989

90-
const columnsWithRender: ColumnType[] = [
90+
const columnsWithRender: ColumnType<any>[] = [
9191
{
9292
name: 'firstName',
9393
label: 'First Name',

src/types.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
export type ColumnType = {
1+
export type ColumnType<T> = {
22
name: string;
33
label?: string;
44
hidden?: boolean;
5+
sort?: ((a: RowType<T>, b: RowType<T>) => number) | undefined;
56
render?: (value: any) => React.ReactNode;
67
};
78

8-
export type HeaderType = {
9+
// this is the type saved as state and returned
10+
export type HeaderType<T> = {
911
name: string;
1012
label?: string;
1113
hidden?: boolean;
1214
sorted: {
1315
on: boolean;
1416
asc: boolean;
1517
};
18+
sort?: ((a: RowType<T>, b: RowType<T>) => number) | undefined;
1619
render?: (value: any) => React.ReactNode;
1720
};
1821

@@ -52,7 +55,7 @@ export type CellType = {
5255
};
5356

5457
export interface UseTableTypeParams<T extends DataType> {
55-
columns: ColumnType[];
58+
columns: ColumnType<T>[];
5659
data: T[];
5760
options?: {
5861
sortable?: boolean;
@@ -63,7 +66,7 @@ export interface UseTableTypeParams<T extends DataType> {
6366
}
6467

6568
export interface UseTablePropsType<T> {
66-
columns: ColumnType[];
69+
columns: ColumnType<T>[];
6770
data: T[];
6871
options?: {
6972
sortable?: boolean;
@@ -79,7 +82,7 @@ export interface UseTableOptionsType<T> {
7982
}
8083

8184
export interface UseTableReturnType<T> {
82-
headers: HeaderType[];
85+
headers: HeaderType<T>[];
8386
originalRows: RowType<T>[];
8487
rows: RowType<T>[];
8588
selectedRows: RowType<T>[];
@@ -91,7 +94,7 @@ export interface UseTableReturnType<T> {
9194

9295
export type TableState<T extends DataType> = {
9396
columnsById: ColumnByIdsType;
94-
columns: HeaderType[];
97+
columns: HeaderType<T>[];
9598
rows: RowType<T>[];
9699
originalRows: RowType<T>[];
97100
selectedRows: RowType<T>[];

0 commit comments

Comments
 (0)