Meteor.js vous permet de déclarer vos collections de données MongoDB très simplement. Mais son implémentation de Mongo est wrappé dans une librairie custom à Meteor. De même, l'utilisation de Mongo avec le protocole DDP et le wrapper de Meteor qui permet de faire des appels à la base de données côté frontend demande un peu d'organisation. C'est pourquoi je vous propose une approche pour réaliser la pagination de vos données.
Dernièrement, je vous parlais un peu de Meteor.js, au travers de deux articles que je vous invite à lire si ce n'est pas le cas :
C'est une technologie que j'apprécie beaucoup et qui me permet d'accélérer la création de projets avec un côté temps réel. Pour rappel, un des principaux avantages de Meteor, c'est sa capacité à propager le changement d'une donnée en base à tous les clients connectés.
WithTracker est le HoC (higher order component) de Meteor.js qui vous permet de faire le lien avec votre couche de données. Il convient alors de faire toute la partie manipulation de données à cet endroit.
Nous n'entrerons pas dans les détails de la définition des types et la définitions des API (que nous avons vu dans les articles précédents).
Voici une proposition d'organisation pour paginer vos pages avec Meteor et MongoDB.
import * as React from 'react'
import { useFind, useSubscribe, withTracker } from 'meteor/react-meteor-data'
import Spinner from '@components/spinner'
import Paginator from '@components/paginator'
import { ProductCollection } from '/imports/api'
type PageProps = {
products: Products[]
count: number
limit: number
toPage: (p: number) => boolean
isLoading: () => boolean
}
function Page({
products,
toPage,
count,
limit,
isLoading
}: PageProps) {
return (
<div>
{/* ... page */}
{isLoading() && <Spinner />}
{/* ... display products */}
<Paginator
toPage={toPage}
count={count}
limit={limit}
displayPages={true}
/>
</div>
)
}
export default withTracker(() => {
const isLoading = useSubscribe('products')
const [currentPage, setCurrentPage] = React.useState(1)
const limit = 15 // Limite par défault = 15 éléments
const products = useFind(
() => ProductCollection.find({}, { limit, skip: limit * (currentPage - 1) }),
[currentPage, limit] // dépendances
) // On récupère les produits avec un offset
return {
limit,
count: ProductCollection.find({}, { fields: { _id: 1 } }).count() ?? 0,
products,
toPage: (nextPage: number) => {
const count = ProductCollection.find({}, { fields: { _id: 1 } }).count() ?? 0
if (nextPage < count) {
setCurrentPage(nextPage)
return true
}
return false
},
isLoading,
}
})(Page)
Vous remarquerez que nous avons mis un state dans le HoC, il va gérer la pagination basé sur un système d'offset/limit.
Ensuite, je pagine via ce composant :
import * as React from 'react'
import cn from 'classnames'
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa'
type PaginatorProps = {
toPage: (p: number) => boolean
defaultPage?: number
hasNextPage?: boolean
hasPrevPage?: boolean
displayPages?: boolean
maxVisiblePages?: number
limit: number
count: number
}
type FooterPage = {
currentPage: number
label: string
onClick: (e: React.SyntheticEvent) => void
}
const Paginator = ({
defaultPage = 1,
toPage,
hasNextPage,
hasPrevPage,
displayPages,
maxVisiblePages = 5,
limit,
count,
}: PaginatorProps) => {
const [page, setPage] = React.useState(defaultPage)
const [visiblePages, setVisiblePages] = React.useState<FooterPage[]>([])
const updatePage = (newPage: number) => {
const pageChanged = toPage(newPage)
if (pageChanged) {
setPage(newPage)
let newVisiblePages: FooterPage[] = []
const start = newPage < 2 ? 0 : newPage - 2
const lastPage = Math.ceil(count / limit)
const end = start + maxVisiblePages < lastPage ? start + maxVisiblePages : lastPage
for (let i = start; i < end; i++) {
newVisiblePages.push({
label: (i + 1).toString(),
currentPage: i + 1,
onClick: () => (newPage - 1 === i ? null : updatePage(i + 1)),
})
}
setVisiblePages(newVisiblePages)
}
}
React.useEffect(() => {
const lastPage = Math.ceil(count / limit)
let newVisiblePages: FooterPage[] = []
const start = page < 2 ? 0 : page - 2
for (let i = start; i < lastPage; i++) {
newVisiblePages.push({
label: (i + 1).toString(),
currentPage: i + 1,
onClick: () => (page - 1 === i ? null : updatePage(i + 1)),
})
}
setVisiblePages(newVisiblePages)
}, [limit, count])
return (
<div className="pagination">
{hasPrevPage && (
<button
className="btn btn-light"
onClick={(e) => {
e.stopPropagation()
updatePage(page - 1)
}}
>
<FaAngleLeft size={28} />
</button>
)}
{displayPages && (
<div className="inline">
{maxVisiblePages &&
visiblePages.map((p, index) => {
return (
<button
className={cn(
'btn me-2',
p.currentPage === page ? 'btn-dark' : 'btn-light'
)}
onClick={p.onClick}
key={index}
>
{p.label}
</button>
)
})}
</div>
)}
{hasNextPage && (
<button
className="btn btn-light"
onClick={(e) => {
e.stopPropagation()
updatePage(page + 1)
}}
>
<FaAngleRight size={28} />
</button>
)}
</div>
)
}
export default Paginator
Vous devriez avoir un composant qui ressemble à cela :
En sachant que vous avez les props hasNextPage et hasPrevPage qui affichent les chevrons si vous les passez à true. (Ces propriétés sont optionnelles).
De même, vous pouvez ou non afficher les pages sous forme de numéro en retirant la propriété displayPages.
Développeur passionné depuis plus de 20 ans, j'ai une appétence particulière pour les défis techniques et changer de technologie ne me fait pas froid aux yeux.
Nous utilisons des cookies sur ce site pour améliorer votre expérience d'utilisateur.