-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add pagination providers and components (#187)
* refs #178 Add pagination providers and components * Use PaginationResponse on the paginator * Improve docs * Use styled components * Export missing * Update paginator * Update docs * refs #178 Run prettier * Use inner page state on routed paginator On this way it will update selected page faster --------- Co-authored-by: selankon <selankon@selankon.xyz>
- Loading branch information
1 parent
12206e4
commit 89a5eaf
Showing
12 changed files
with
462 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
--- | ||
title: Pagination components | ||
description: Vocdoni Chakra components pagination | ||
--- | ||
|
||
## Pagination and RoutedPagination | ||
|
||
You can easily add pagination to any method by using the included PaginationProvider (or RoutedPaginationProvider in | ||
case you want to use it with react-router-dom). | ||
|
||
### RoutedPaginationProvider Example | ||
|
||
If you're using `react-router-dom`, you can integrate pagination with URL routing using the `RoutedPaginationProvider`. | ||
Here's how to set it up: | ||
|
||
```jsx | ||
const MyProvidedRoutedPaginatedComponent = () => { | ||
return ( | ||
<RoutedPaginationProvider path='/my-paginated-route-path'> | ||
<MyRoutedPaginatedComponent /> | ||
</RoutedPaginationProvider> | ||
) | ||
} | ||
|
||
const MyRoutedPaginatedComponent = () => { | ||
// Use the page from the pagination provider instead of the `useParams` hook. | ||
// Note: The displayed page in the URL is 1-based, but the internal page indexing starts at 0. | ||
const { page } = useRoutedPagination() | ||
|
||
// retrieve your data, specifying the desired page. | ||
const { data } = useVocdoniSDKPaginatedCall({ | ||
page, | ||
}) | ||
|
||
return ( | ||
<> | ||
{data.items.map((item) => ( | ||
<MyItem key={item.id} item={item} /> | ||
))} | ||
{/* Render the pagination component */} | ||
<Pagination pagination={data.pagination} /> | ||
</> | ||
) | ||
} | ||
``` | ||
|
||
In this setup, the `Pagination` component will automatically manage the pagination state and update the URL to reflect | ||
the current page. | ||
|
||
If we want to programatically set the page, we can use the `usePagination` hook, which provide other util methods: | ||
|
||
```ts | ||
const { | ||
getPathForPage, // Get the path for a specific page | ||
setPage, // Navigate to page | ||
page, // Current page | ||
} = useRoutedPagination() | ||
``` | ||
|
||
### PaginationProvider Example (Non-Routed) | ||
|
||
The `PaginationProvider` (non routed) uses an internal state to handle the current page, rather than taking it from the URL. | ||
|
||
```jsx | ||
const MyProvidedPaginatedComponent = () => { | ||
return ( | ||
<PaginationProvider> | ||
<MyPaginatedComponent /> | ||
</PaginationProvider> | ||
) | ||
} | ||
const MyPaginatedComponent = () => { | ||
const { page } = usePagination() | ||
|
||
// Retrieve your data, specifying the desired page. | ||
const { data } = useVocdoniSDKPaginatedCall({ | ||
page, | ||
}) | ||
|
||
return ( | ||
<> | ||
{data.items.map((item) => ( | ||
<MyItem key={item.id} item={item} /> | ||
))} | ||
{/* load the pagination itself */} | ||
<Pagination pagination={data.pagination} /> | ||
</> | ||
) | ||
} | ||
``` | ||
|
||
In this example, the `PaginationProvider` manages the pagination state internally without affecting the URL. | ||
|
||
### PaginationResponse Interface | ||
|
||
The `<Pagination />` component consumes a `PaginationResponse` object provided by the VocdoniSDK. | ||
This object contains the necessary pagination data to manage the state effectively. | ||
|
||
```ts | ||
interface PaginationResponse { | ||
pagination: { | ||
totalItems: number | ||
previousPage: number | null | ||
currentPage: number | ||
nextPage: number | null | ||
lastPage: number | ||
} | ||
} | ||
``` | ||
|
||
When using the VocdoniSDK to retrieve paginated data, this PaginationResponse object will be included in the response, | ||
allowing for easy integration with the `<Pagination />` component. | ||
|
||
### Summary | ||
|
||
- `RoutedPaginationProvider`: Use this when you want to synchronize pagination with the URL (e.g., when using `react-router-dom`). | ||
- `PaginationProvider`: Use this when URL synchronization is not needed, and you want to manage pagination state internally. | ||
|
||
Both providers work seamlessly with the `Pagination` component and the `PaginationResponse` object from the VocdoniSDK, | ||
making it simple to add pagination to any data-driven component. |
48 changes: 48 additions & 0 deletions
48
packages/chakra-components/src/components/Pagination/EllipsisButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { useState } from 'react' | ||
import { useStyleConfig } from '@chakra-ui/system' | ||
import { Button, ButtonProps, Input, InputProps } from '@chakra-ui/react' | ||
|
||
type EllipsisButtonProps = ButtonProps & { | ||
gotoPage: (page: number) => void | ||
inputProps?: InputProps | ||
} | ||
|
||
export const EllipsisButton = ({ gotoPage, inputProps, ...rest }: EllipsisButtonProps) => { | ||
const [ellipsisInput, setEllipsisInput] = useState(false) | ||
const styles = useStyleConfig('EllipsisButton', rest) | ||
|
||
if (ellipsisInput) { | ||
return ( | ||
<Input | ||
placeholder='Page #' | ||
width='50px' | ||
sx={styles} | ||
{...inputProps} | ||
onKeyDown={(e) => { | ||
if (e.target instanceof HTMLInputElement && e.key === 'Enter') { | ||
const pageNumber = Number(e.target.value) | ||
gotoPage(pageNumber) | ||
setEllipsisInput(false) | ||
} | ||
}} | ||
onBlur={() => setEllipsisInput(false)} | ||
autoFocus | ||
/> | ||
) | ||
} | ||
|
||
return ( | ||
<Button | ||
as='a' | ||
href='#goto-page' | ||
sx={styles} | ||
{...rest} | ||
onClick={(e) => { | ||
e.preventDefault() | ||
setEllipsisInput(true) | ||
}} | ||
> | ||
... | ||
</Button> | ||
) | ||
} |
187 changes: 187 additions & 0 deletions
187
packages/chakra-components/src/components/Pagination/Pagination.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import { ButtonGroup, ButtonGroupProps, ButtonProps, InputProps, Text } from '@chakra-ui/react' | ||
import { ReactElement, useMemo } from 'react' | ||
import { Link as RouterLink } from 'react-router-dom' | ||
import { useLocalize, usePagination, useRoutedPagination } from '@vocdoni/react-providers' | ||
import { PaginationResponse } from '@vocdoni/sdk' | ||
import { useMultiStyleConfig, chakra } from '@chakra-ui/system' | ||
import { EllipsisButton } from './EllipsisButton' | ||
import { PaginatorButton } from './PaginatorButton' | ||
|
||
export type PaginationProps = ButtonGroupProps & { | ||
maxButtons?: number | false | ||
buttonProps?: ButtonProps | ||
inputProps?: InputProps | ||
} & PaginationResponse | ||
|
||
type PaginatorButtonProps = { | ||
page: number | ||
currentPage: number | ||
} & ButtonProps | ||
|
||
const PageButton = ({ page, currentPage, ...rest }: PaginatorButtonProps) => ( | ||
<PaginatorButton isActive={currentPage === page} {...rest}> | ||
{page + 1} | ||
</PaginatorButton> | ||
) | ||
|
||
const RoutedPageButton = ({ page, currentPage, to, ...rest }: PaginatorButtonProps & { to: string }) => ( | ||
<PaginatorButton as={RouterLink} to={to} isActive={currentPage === page} {...rest}> | ||
{page + 1} | ||
</PaginatorButton> | ||
) | ||
|
||
type CreatePageButtonType = (i: number) => ReactElement | ||
type GotoPageType = (page: number) => void | ||
|
||
const usePaginationPages = ( | ||
currentPage: number, | ||
totalPages: number | undefined, | ||
maxButtons: number | undefined | false, | ||
gotoPage: GotoPageType, | ||
createPageButton: CreatePageButtonType, | ||
inputProps?: InputProps, | ||
buttonProps?: ButtonProps | ||
) => { | ||
return useMemo(() => { | ||
if (totalPages === undefined) return [] | ||
|
||
let pages: ReactElement[] = [] | ||
|
||
// Create an array of all page buttons | ||
for (let i = 0; i < totalPages; i++) { | ||
pages.push(createPageButton(i)) | ||
} | ||
|
||
if (!maxButtons || totalPages <= maxButtons) { | ||
return pages | ||
} | ||
|
||
const startEllipsis = ( | ||
<EllipsisButton key='start-ellipsis' gotoPage={gotoPage} inputProps={inputProps} {...buttonProps} /> | ||
) | ||
const endEllipsis = ( | ||
<EllipsisButton key='end-ellipsis' gotoPage={gotoPage} inputProps={inputProps} {...buttonProps} /> | ||
) | ||
|
||
// Add ellipsis and slice the array accordingly | ||
const sideButtons = 2 // First and last page | ||
const availableButtons = maxButtons - sideButtons // Buttons we can distribute around the current page | ||
|
||
if (currentPage <= availableButtons / 2) { | ||
// Near the start | ||
return [...pages.slice(0, availableButtons), endEllipsis, pages[totalPages - 1]] | ||
} else if (currentPage >= totalPages - 1 - availableButtons / 2) { | ||
// Near the end | ||
return [pages[0], startEllipsis, ...pages.slice(totalPages - availableButtons, totalPages)] | ||
} else { | ||
// In the middle | ||
const startPage = currentPage - Math.floor((availableButtons - 1) / 2) | ||
const endPage = currentPage + Math.floor(availableButtons / 2) | ||
return [pages[0], startEllipsis, ...pages.slice(startPage, endPage - 1), endEllipsis, pages[totalPages - 1]] | ||
} | ||
}, [currentPage, totalPages, maxButtons, gotoPage]) | ||
} | ||
|
||
const PaginationButtons = ({ | ||
totalPages, | ||
totalItems, | ||
currentPage, | ||
goToPage, | ||
createPageButton, | ||
maxButtons = 10, | ||
buttonProps, | ||
...rest | ||
}: { | ||
totalPages: number | undefined | ||
totalItems: number | undefined | ||
currentPage: number | ||
createPageButton: CreatePageButtonType | ||
goToPage: GotoPageType | ||
} & ButtonGroupProps & | ||
Pick<PaginationProps, 'maxButtons' | 'buttonProps'>) => { | ||
const styles = useMultiStyleConfig('Pagination') | ||
const t = useLocalize() | ||
|
||
const pages = usePaginationPages( | ||
currentPage, | ||
totalPages, | ||
maxButtons ? Math.max(5, maxButtons) : false, | ||
(page) => { | ||
if (page >= 0 && totalPages && page < totalPages) { | ||
goToPage(page) | ||
} | ||
}, | ||
createPageButton | ||
) | ||
|
||
return ( | ||
<chakra.div __css={styles.wrapper}> | ||
<ButtonGroup sx={styles.buttonGroup} {...rest}> | ||
{totalPages === undefined ? ( | ||
<> | ||
<PaginatorButton | ||
key='previous' | ||
onClick={() => goToPage(currentPage - 1)} | ||
isDisabled={currentPage === 0} | ||
{...buttonProps} | ||
> | ||
Previous | ||
</PaginatorButton> | ||
<PaginatorButton key='next' onClick={() => goToPage(currentPage + 1)} {...buttonProps}> | ||
Next | ||
</PaginatorButton> | ||
</> | ||
) : ( | ||
pages | ||
)} | ||
</ButtonGroup> | ||
{totalItems && ( | ||
<Text sx={styles.totalResults}> | ||
{t('pagination.total_results', { | ||
count: totalItems, | ||
})} | ||
</Text> | ||
)} | ||
</chakra.div> | ||
) | ||
} | ||
|
||
export const Pagination = ({ maxButtons = 10, buttonProps, inputProps, pagination, ...rest }: PaginationProps) => { | ||
const { setPage } = usePagination() | ||
const totalPages = pagination.lastPage + 1 | ||
const page = pagination.currentPage | ||
|
||
return ( | ||
<PaginationButtons | ||
goToPage={(page) => setPage(page)} | ||
createPageButton={(i) => ( | ||
<PageButton key={i} page={i} currentPage={page} onClick={() => setPage(i)} {...buttonProps} /> | ||
)} | ||
currentPage={page} | ||
totalPages={totalPages} | ||
totalItems={pagination.totalItems} | ||
maxButtons={maxButtons} | ||
{...rest} | ||
/> | ||
) | ||
} | ||
|
||
export const RoutedPagination = ({ maxButtons = 10, buttonProps, pagination, ...rest }: PaginationProps) => { | ||
const { getPathForPage, setPage, page } = useRoutedPagination() | ||
|
||
const totalPages = pagination.lastPage + 1 | ||
const currentPage = page | ||
|
||
return ( | ||
<PaginationButtons | ||
goToPage={(page) => setPage(page)} | ||
createPageButton={(i) => ( | ||
<RoutedPageButton key={i} to={getPathForPage(i + 1)} page={i} currentPage={page} {...buttonProps} /> | ||
)} | ||
currentPage={currentPage} | ||
totalPages={totalPages} | ||
totalItems={pagination.totalItems} | ||
{...rest} | ||
/> | ||
) | ||
} |
7 changes: 7 additions & 0 deletions
7
packages/chakra-components/src/components/Pagination/PaginatorButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { useStyleConfig } from '@chakra-ui/system' | ||
import { Button, ButtonProps, forwardRef } from '@chakra-ui/react' | ||
|
||
export const PaginatorButton = forwardRef<ButtonProps, 'div'>((props, ref) => { | ||
const styles = useStyleConfig('PageButton', props) | ||
return <Button sx={styles} {...props} /> | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './Pagination' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.