Skip to content

Commit

Permalink
feat: implement server pagination mode for TablePagination
Browse files Browse the repository at this point in the history
Signed-off-by: Mason Hu <[email protected]>
  • Loading branch information
mas-who committed Jan 31, 2024
1 parent 91e1814 commit 8b5575b
Show file tree
Hide file tree
Showing 6 changed files with 557 additions and 161 deletions.
100 changes: 100 additions & 0 deletions src/components/TablePagination/ClientPagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, {
ChangeEvent,
HTMLAttributes,
PropsWithChildren,
useRef,
useState,
} from "react";
import classnames from "classnames";
import { usePagination } from "hooks";
import Select from "components/Select";
import TablePaginationControls from "./TablePaginationControls";
import "./TablePagination.scss";
import { BasePaginationProps } from "./TablePagination";
import {
DEFAULT_PAGE_LIMITS,
generatePagingOptions,
getDescription,
renderChildren,
useFigureSmallScreen,
} from "./utils";

export type ClientPaginationProps = BasePaginationProps & {
/**
* set pagination to be server based making the component purely presentational or
* set pagination to be client based which will delegate control to this component
*/
type?: "client-pagination";
};

export type Props = PropsWithChildren<ClientPaginationProps> &
HTMLAttributes<HTMLDivElement>;

const ClientPagination = ({
data,
className,
itemName = "item",
description,
position = "above",
dataForwardProp = "rows",
pageLimits = DEFAULT_PAGE_LIMITS,
children,
...divProps
}: Props) => {
const descriptionRef = useRef<HTMLDivElement>(null);
const isSmallScreen = useFigureSmallScreen({ descriptionRef });
const [pageSize, setPageSize] = useState(() => {
return generatePagingOptions(pageLimits)[0].value;
});
const { paginate, currentPage, pageData, totalItems } = usePagination(data, {
itemsPerPage: pageSize,
autoResetPage: true,
});

const handlePageSizeChange = (e: ChangeEvent<HTMLSelectElement>) => {
paginate(1);
setPageSize(parseInt(e.target.value));
};

const totalPages = Math.ceil(data.length / pageSize);
const clonedChildren = renderChildren(children, dataForwardProp, pageData);
const descriptionDisplay = getDescription({
description,
data: pageData,
isSmallScreen,
totalItems,
itemName,
});

return (
<>
{position === "below" && clonedChildren}
<div
className={classnames("pagination", className)}
{...divProps}
role="navigation"
>
<div className="description" ref={descriptionRef}>
{descriptionDisplay}
</div>
<TablePaginationControls
onPageChange={paginate}
currentPage={currentPage}
totalPages={totalPages}
/>
<Select
className="items-per-page"
label="Items per page"
labelClassName="u-off-screen"
id="itemsPerPage"
options={generatePagingOptions(pageLimits)}
onChange={handlePageSizeChange}
value={pageSize}
/>
</div>
{position === "above" && clonedChildren}
</>
);
};

export default ClientPagination;
119 changes: 119 additions & 0 deletions src/components/TablePagination/ServerPagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, {
ChangeEvent,
HTMLAttributes,
PropsWithChildren,
useRef,
} from "react";
import classnames from "classnames";
import Select from "components/Select";
import TablePaginationControls from "./TablePaginationControls";
import "./TablePagination.scss";
import { BasePaginationProps } from "./TablePagination";
import {
DEFAULT_PAGE_LIMITS,
generatePagingOptions,
getDescription,
renderChildren,
useFigureSmallScreen,
} from "./utils";

export type ServerPaginationProps = BasePaginationProps & {
/**
* set pagination to be server based making the component purely presentational or
* set pagination to be client based which will delegate control to this component
*/
type?: "server-pagination";
/**
* the total number of items available within the data. This prop is only relevant if
* `type` is set to `server-pagination`.
*/
totalItems: number;
/**
* the current page that's showing. This prop is only relevant if
* `type` is set to `server-pagination`.
*/
currentPage: number;
/**
* size per page. This prop is only relevant if `type` is set to `server-pagination`.
*/
pageSize: number;
/**
* callback indicating a page change event to the parent component.
* This prop is only relevant if `type` is set to `server-pagination`.
*/
onPageChange: (page: number) => void;
/**
* callback indicating a page size change event to the parent component.
* This prop is only relevant if `type` is set to `server-pagination`.
*/
onPageSizeChange: (page: number) => void;
};

export type Props = PropsWithChildren<ServerPaginationProps> &
HTMLAttributes<HTMLDivElement>;

const ServerPagination = ({
data,
className,
itemName = "item",
description,
position = "above",
dataForwardProp = "rows",
pageLimits = DEFAULT_PAGE_LIMITS,
totalItems,
currentPage,
pageSize,
onPageChange,
onPageSizeChange,
children,
...divProps
}: Props) => {
const descriptionRef = useRef<HTMLDivElement>(null);
const isSmallScreen = useFigureSmallScreen({ descriptionRef });

const handlePageSizeChange = (e: ChangeEvent<HTMLSelectElement>) => {
onPageSizeChange(parseInt(e.target.value));
};

const totalPages = Math.ceil(totalItems / pageSize);
const clonedChildren = renderChildren(children, dataForwardProp, data);
const descriptionDisplay = getDescription({
description,
data,
isSmallScreen,
totalItems,
itemName,
});

return (
<>
{position === "below" && clonedChildren}
<div
className={classnames("pagination", className)}
{...divProps}
role="navigation"
>
<div className="description" ref={descriptionRef}>
{descriptionDisplay}
</div>
<TablePaginationControls
onPageChange={onPageChange}
currentPage={currentPage}
totalPages={totalPages}
/>
<Select
className="items-per-page"
label="Items per page"
labelClassName="u-off-screen"
id="itemsPerPage"
options={generatePagingOptions(pageLimits)}
onChange={handlePageSizeChange}
value={pageSize}
/>
</div>
{position === "above" && clonedChildren}
</>
);
};

export default ServerPagination;
82 changes: 70 additions & 12 deletions src/components/TablePagination/TablePagination.stories.mdx
Original file line number Diff line number Diff line change
@@ -1,20 +1,55 @@
import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs";
import { ArgsTable, Canvas, Meta, Story } from "@storybook/blocks";

import TablePagination from "./TablePagination";
import MainTable from "../MainTable";

<Meta title="TablePagination" component={TablePagination} />
<Meta
title="TablePagination"
component={TablePagination}
argTypes={{
totalItems: {
if: { arg: 'type', eq: "server-pagination" },
},
totalItems: {
if: { arg: 'type', eq: "server-pagination" },
},
currentPage: {
if: { arg: 'type', eq: "server-pagination" },
},
pageSize: {
if: { arg: 'type', eq: "server-pagination" },
},
onPageChange: {
if: { arg: 'type', eq: "server-pagination" },
},
onPageSizeChange: {
if: { arg: 'type', eq: "server-pagination" },
},
}}
/>

export const Template = (args) => <TablePagination {...args} />;

### TablePagination

This is an HOC [React](https://reactjs.org/) component for applying pagination to input data for direct child components.
This component is un-opinionated about the structure of the input data and can be used with any child component that displays
a list of data. However, the styling and behaviour of this component were designed to work nicely with the ```MainTable``` component.
This is an HOC [React](https://reactjs.org/) component for applying pagination to direct children components. This component is un-opinionated about
the structure of the input data and can be used with any child component that displays a list of data. However, the styling and behaviour of this component were designed
to work nicely with the `MainTable` component. To use this component, simply wrap a child component with it and provide the data that you want
to paginate to the `data` prop. This component will then pass the paged data to all <b>direct</b> child components via a child prop specified by `dataForwardProp`.
The component supports both client side and server side pagination techniques which are described in the subsequent two sections.

To use this component, simply wrap a child component with it and provide the data that you want to paginate to the ```data``` prop.
This component will then pass the paged data to all <b>direct</b> child components via a child prop specified by ```dataForwardProp```.
#### Server side pagination

For Server side pagination, the control logic is delegated to the server and therefore the component will be purely presentational.
The pagination behaviour is controlled outside of this component. Note the data injection to child components is essentially a passthrough in this case.
To enable server side pagination mode for this component, set the `type` prop to `server-pagination`. From there, it is your responsibility
to ensure that the following props `totalItems`, `currentPage`, `pageSize`, `onPageChange` and `onPageSizeChange` are set properly.
You can refer to the props table below on how to set these props.

#### Client side pagination

Client side pagination assumes that the input data is not paginated. The component will implement the control logic to page the input data
then inject it into direct child components. To enable client side pagination mode for this component, , set the `type` prop to `client-pagination`.

### Props

Expand All @@ -26,7 +61,14 @@ This component will then pass the paged data to all <b>direct</b> child componen
<Story
name="Default"
args={{
data: [{id: "row-1"}, {id: "row-2"}, {id: "row-3"}, {id: "row-4"}, {id: "row-5"}],
data: [
{ id: "row-1" },
{ id: "row-2" },
{ id: "row-3" },
{ id: "row-4" },
{ id: "row-5" },
],
type: "client-pagination"
}}
>
{Template.bind({})}
Expand All @@ -39,8 +81,15 @@ This component will then pass the paged data to all <b>direct</b> child componen
<Story
name="CustomPageLimit"
args={{
data: [{id: "row-1"}, {id: "row-2"}, {id: "row-3"}, {id: "row-4"}, {id: "row-5"}],
pageLimits: [1, 2, 3]
data: [
{ id: "row-1" },
{ id: "row-2" },
{ id: "row-3" },
{ id: "row-4" },
{ id: "row-5" },
],
pageLimits: [1, 2, 3],
type: "client-pagination"
}}
>
{Template.bind({})}
Expand All @@ -53,8 +102,15 @@ This component will then pass the paged data to all <b>direct</b> child componen
<Story
name="CustomDisplayTitle"
args={{
data: [{id: "row-1"}, {id: "row-2"}, {id: "row-3"}, {id: "row-4"}, {id: "row-5"}],
description: <b>Hello there</b>
data: [
{ id: "row-1" },
{ id: "row-2" },
{ id: "row-3" },
{ id: "row-4" },
{ id: "row-5" },
],
description: <b>Hello there</b>,
type: "client-pagination"
}}
>
{Template.bind({})}
Expand Down Expand Up @@ -134,6 +190,7 @@ This component will then pass the paged data to all <b>direct</b> child componen
</TablePagination>
);
}}

</Story>
</Canvas>

Expand Down Expand Up @@ -210,5 +267,6 @@ This component will then pass the paged data to all <b>direct</b> child componen
</TablePagination>
);
}}

</Story>
</Canvas>
Loading

0 comments on commit 8b5575b

Please sign in to comment.