Skip to content

Commit 49e4735

Browse files
committed
fix(pagination): Enhance useDataViewPagination with URL persisting
1 parent 12b5bfd commit 49e4735

File tree

4 files changed

+161
-21
lines changed

4 files changed

+161
-21
lines changed

packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ import { useDataViewPagination, useDataViewSelection } from '@patternfly/react-d
1818
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
1919
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';
2020
import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
21+
import { BrowserRouter, useSearchParams } from 'react-router-dom';
2122

2223
This is a list of functionality you can use to manage data displayed in the **data view**.
2324

2425
# Pagination
2526
Allows to display data records on multiple pages and display the pagination state.
2627

2728
### Toolbar usage
28-
Data view toolbar can display a pagination using the `pagination` property accepting a React node. You can also pass a custom `ouiaId` for testing purposes.
29+
Data view toolbar can display a pagination using the `pagination` property accepting a React node. You can also pass a custom `ouiaId` for testing purposes. Additionally, it offers an option to persist pagination values in the URL, which makes it easier to share or bookmark specific pages of your data.
2930

3031
### Pagination state
3132

@@ -34,8 +35,12 @@ The `useDataViewPagination` hook manages the pagination state of the data view.
3435
**Initial values:**
3536
- `perPage` initial value
3637
- (optional) `page` initial value
38+
- (optional) `searchParams` object
39+
- (optional) `seSearchParams` function
3740

38-
The retrieved values are named to match the PatternFly [pagination](/components/pagination) component props, so you can easily spread them.
41+
While the hook works seamlessly with React Router library, you do not need to use it to take advantage of URL persistence. The `searchParams` and `setSearchParams` props can be managed using native browser APIs (`URLSearchParams` and `window.history.pushState`) or any other routing library of your choice. If you don't pass these two props, the pagination state will be stored internally without the URL usage.
42+
43+
The retrieved values are named to match the PatternFly [pagination](/components/pagination) component props, so you can easily spread them to the component.
3944

4045
**Return values:**
4146
- current `page` number
@@ -44,6 +49,8 @@ The retrieved values are named to match the PatternFly [pagination](/components/
4449
- `onPerPageSelect` to modify per page value
4550

4651
### Pagination example
52+
This example uses the URL for persisting the pagination state.
53+
4754
```js file="./PaginationExample.tsx"
4855

4956
```

packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/PaginationExample.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Table, Tbody, Th, Thead, Tr, Td } from '@patternfly/react-table';
44
import { useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks';
55
import DataView from '@patternfly/react-data-view/dist/dynamic/DataView';
66
import DataViewToolbar from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
7+
import { BrowserRouter, useSearchParams } from 'react-router-dom';
78

89
const perPageOptions = [
910
{ title: '5', value: 5 },
@@ -37,12 +38,12 @@ const cols = {
3738

3839
const ouiaId = 'LayoutExample';
3940

40-
export const BasicExample: React.FunctionComponent = () => {
41-
const pagination = useDataViewPagination({ perPage: 5 });
41+
const MyTable: React.FunctionComponent = () => {
42+
const [ searchParams, setSearchParams ] = useSearchParams()
43+
const pagination = useDataViewPagination({ perPage: 5, searchParams, setSearchParams });
4244
const { page, perPage } = pagination;
4345

4446
const data = useMemo(() => repositories.slice((page - 1) * perPage, ((page - 1) * perPage) + perPage), [ page, perPage ]);
45-
4647
return (
4748
<DataView>
4849
<DataViewToolbar ouiaId='DataViewHeader' pagination={<Pagination perPageOptions={perPageOptions} itemCount={repositories.length} {...pagination} />} />
@@ -62,4 +63,11 @@ export const BasicExample: React.FunctionComponent = () => {
6263
</Table>
6364
<DataViewToolbar ouiaId='DataViewFooter' pagination={<Pagination isCompact perPageOptions={perPageOptions} itemCount={repositories.length} {...pagination} />} />
6465
</DataView>
65-
)}
66+
)
67+
}
68+
69+
export const BasicExample: React.FunctionComponent = () => (
70+
<BrowserRouter>
71+
<MyTable/>
72+
</BrowserRouter>
73+
)

packages/module/src/Hooks/pagination.test.tsx

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,81 @@ describe('useDataViewPagination', () => {
4949
expect(result.current).toEqual({
5050
onPerPageSelect: expect.any(Function),
5151
onSetPage: expect.any(Function),
52-
page: 3,
52+
page: 1,
5353
perPage: 50
5454
})
5555
});
5656

57+
it('should read pagination state from URL', () => {
58+
const mockSearchParams = new URLSearchParams('page=2&perPage=10');
59+
const { result } = renderHook(() =>
60+
useDataViewPagination({
61+
searchParams: mockSearchParams,
62+
setSearchParams: jest.fn(),
63+
page: 1,
64+
perPage: 5,
65+
})
66+
);
67+
68+
expect(result.current).toEqual({
69+
onPerPageSelect: expect.any(Function),
70+
onSetPage: expect.any(Function),
71+
page: 2,
72+
perPage: 10,
73+
});
74+
});
75+
76+
it('should set pagination state in URL when page changes', () => {
77+
const mockSetSearchParams = jest.fn();
78+
const { result } = renderHook(() =>
79+
useDataViewPagination({
80+
searchParams: new URLSearchParams(),
81+
setSearchParams: mockSetSearchParams,
82+
page: 1,
83+
perPage: 5,
84+
})
85+
);
86+
87+
act(() => {
88+
result.current.onSetPage(undefined, 4);
89+
});
90+
91+
expect(mockSetSearchParams).toHaveBeenCalledTimes(2);
92+
});
93+
94+
it('should set pagination state in URL when perPage changes', () => {
95+
const mockSetSearchParams = jest.fn();
96+
const { result } = renderHook(() =>
97+
useDataViewPagination({
98+
searchParams: new URLSearchParams('page=2'),
99+
setSearchParams: mockSetSearchParams,
100+
page: 1,
101+
perPage: 5,
102+
})
103+
);
104+
105+
act(() => {
106+
result.current.onPerPageSelect(undefined, 20);
107+
});
108+
109+
expect(mockSetSearchParams).toHaveBeenCalledWith(
110+
new URLSearchParams('page=1&perPage=20')
111+
);
112+
});
113+
114+
it('should initialize URL with default values if not present', () => {
115+
const mockSetSearchParams = jest.fn();
116+
renderHook(() =>
117+
useDataViewPagination({
118+
searchParams: new URLSearchParams(),
119+
setSearchParams: mockSetSearchParams,
120+
page: 1,
121+
perPage: 5,
122+
})
123+
);
124+
125+
expect(mockSetSearchParams).toHaveBeenCalledWith(
126+
new URLSearchParams('page=1&perPage=5')
127+
);
128+
});
57129
});
Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,84 @@
1-
import { useState } from "react";
1+
import { useState, useEffect } from "react";
22

33
export interface UseDataViewPaginationProps {
44
/** Initial page */
55
page?: number;
66
/** Items per page */
77
perPage: number;
8+
/** Current search parameters as a string */
9+
searchParams?: URLSearchParams;
10+
/** Function to set search parameters */
11+
setSearchParams?: (params: URLSearchParams) => void;
812
}
913

1014
export interface DataViewPaginationProps extends UseDataViewPaginationProps {
1115
/** Current page number */
12-
page: number;
16+
page: number;
1317
}
1418

15-
export const useDataViewPagination = ({ page = 1, perPage }: UseDataViewPaginationProps) => {
16-
const [ state, setState ] = useState({ page, perPage });
17-
18-
const onPerPageSelect = (_event: React.MouseEvent | React.KeyboardEvent | MouseEvent | undefined, newPerPage: number) => {
19-
setState(prev => ({ ...prev, perPage: newPerPage }));
20-
}
21-
22-
const onSetPage = (_event: React.MouseEvent | React.KeyboardEvent | MouseEvent | undefined, newPage: number) => {
19+
export const useDataViewPagination = ({
20+
page = 1,
21+
perPage,
22+
searchParams,
23+
setSearchParams,
24+
}: UseDataViewPaginationProps) => {
25+
const [ state, setState ] = useState({
26+
page: searchParams?.get('page') !== null
27+
? parseInt(searchParams?.get('page') || `${page}`)
28+
: page,
29+
perPage: searchParams?.get('perPage') !== null
30+
? parseInt(searchParams?.get('perPage') || `${perPage}`)
31+
: perPage,
32+
});
33+
34+
useEffect(() => {
35+
if (searchParams && setSearchParams) {
36+
const params = new URLSearchParams(searchParams);
37+
let updated = false;
38+
39+
if (!params.has('page')) {
40+
params.set('page', `${page}`);
41+
updated = true;
42+
}
43+
44+
if (!params.has('perPage')) {
45+
params.set('perPage', `${perPage}`);
46+
updated = true;
47+
}
48+
49+
updated && setSearchParams(params);
50+
}
51+
// eslint-disable-next-line react-hooks/exhaustive-deps
52+
}, []);
53+
54+
const onPerPageSelect = (
55+
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent | undefined,
56+
newPerPage: number
57+
) => {
58+
if (searchParams && setSearchParams) {
59+
const params = new URLSearchParams(searchParams);
60+
params.set('perPage', newPerPage.toString());
61+
params.set('page', '1');
62+
setSearchParams(params);
63+
}
64+
setState({ perPage: newPerPage, page: 1 });
65+
};
66+
67+
const onSetPage = (
68+
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent | undefined,
69+
newPage: number
70+
) => {
71+
if (searchParams && setSearchParams) {
72+
const params = new URLSearchParams(searchParams);
73+
params.set('page', newPage.toString());
74+
setSearchParams(params);
75+
}
2376
setState(prev => ({ ...prev, page: newPage }));
24-
}
25-
77+
};
78+
2679
return {
2780
...state,
2881
onPerPageSelect,
2982
onSetPage
30-
}
31-
}
83+
};
84+
};

0 commit comments

Comments
 (0)