Comment paginer avec Meteor.js

Comment paginer avec Meteor.js

Alexandre P. dans Dev - Le 07-07-2022

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.

Gestion de la pagination dans un HoC withTracker

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.

Organisation de la page

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.

Composant de pagination

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 :

paginate-meteor-mongo ><

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.

#meteor#meteorjs#mongodb#pagination#conseil#coding

user picture
Alexandre P.

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.