Paginationの...(省略記号)とかを「現在のページ」と「全体のページ数」からよしなに生成するコンポーネントの実装

8 min read読了の目安(約7500字

概要

下記のようなPaginationを実装しようとした際に、良いサンプルの実装を見つけたのでご紹介します。

詳細の仕様

詳細の仕様は下記のとおりです。

  • 「最初のページ数(1)」と「最後のページ数」は必ず表示する
  • 「現在のページ数-2」 ~ 「現在のページ数+2」の範囲のページ数は、「最初のページ数(1)」~「最後のページ数」の範囲を超えない限り表示する
  • 「現在のページ数-2」が4以上のとき
    • 「1」と「現在のページ数-2」の間に省略記号を表示する
  • 「現在のページ数-2」が3のとき
    • 「1」と「現在のページ数-2」の間に「2」を表示する
  • 「現在のページ数+2」が「最後のページ数-3」以下のとき
    • 「現在のページ数+2」と「最後のページ数」の間に省略記号を表示する
  • 「現在のページ数+2」が「最後のページ数-2」のとき
    • 「現在のページ数+2」と「最後のページ数」の間に「最後のページ数-1」を表示する

参考にしたサンプル

上記の仕様について、自分で実装方法考えようとしても車輪の再発明にしかならないので、ググって同じような実装ロジックを探したところ、下記のような実装を見つけました。

https://gist.github.com/kottenator/9d936eb3e4e3c3e02598
function pagination(c, m) {
    var current = c,
        last = m,
        delta = 2,
        left = current - delta,
        right = current + delta + 1,
        range = [],
        rangeWithDots = [],
        l;

    for (let i = 1; i <= last; i++) {
        if (i == 1 || i == last || i >= left && i < right) {
            range.push(i);
        }
    }

    for (let i of range) {
        if (l) {
            if (i - l === 2) {
                rangeWithDots.push(l + 1);
            } else if (i - l !== 1) {
                rangeWithDots.push('...');
            }
        }
        rangeWithDots.push(i);
        l = i;
    }

    return rangeWithDots;
}

/* 
Test it:
for (let i = 1, l = 20; i <= l; i++)
    console.log(`Selected page ${i}:`, pagination(i, l));
Expected output:
Selected page 1: [1, 2, 3, "...", 20]
Selected page 2: [1, 2, 3, 4, "...", 20]
Selected page 3: [1, 2, 3, 4, 5, "...", 20]
Selected page 4: [1, 2, 3, 4, 5, 6, "...", 20]
Selected page 5: [1, 2, 3, 4, 5, 6, 7, "...", 20]
Selected page 6: [1, "...", 4, 5, 6, 7, 8, "...", 20]
Selected page 7: [1, "...", 5, 6, 7, 8, 9, "...", 20]
Selected page 8: [1, "...", 6, 7, 8, 9, 10, "...", 20]
Selected page 9: [1, "...", 7, 8, 9, 10, 11, "...", 20]
Selected page 10: [1, "...", 8, 9, 10, 11, 12, "...", 20]
Selected page 11: [1, "...", 9, 10, 11, 12, 13, "...", 20]
Selected page 12: [1, "...", 10, 11, 12, 13, 14, "...", 20]
Selected page 13: [1, "...", 11, 12, 13, 14, 15, "...", 20]
Selected page 14: [1, "...", 12, 13, 14, 15, 16, "...", 20]
Selected page 15: [1, "...", 13, 14, 15, 16, 17, "...", 20]
Selected page 16: [1, "...", 14, 15, 16, 17, 18, 19, 20]
Selected page 17: [1, "...", 15, 16, 17, 18, 19, 20]
Selected page 18: [1, "...", 16, 17, 18, 19, 20]
Selected page 19: [1, "...", 17, 18, 19, 20]
Selected page 20: [1, "...", 18, 19, 20]
*/

react-bootstrapのPagnationで実装を書き換え

上記のロジックをreact-bootstrapのPagnationを使い、コンポーネントとして使用できるように書き換えます。

https://react-bootstrap.github.io/components/pagination/
import React, { FC } from "react";
import { Pagination as BootstrapPagination } from "react-bootstrap";

interface Props {
    currentPage: number;
    totalPage: number;
    onClick: (page: number) => void;
}

export const YoshinaPagination: FC<Props> = ({
    currentPage,
    totalPage,
    onClick,
}: Props) => {
    const displayPages = [];
    const items = [];

    for (let i = 1; i <= totalPage; i++) {
        if (
            i == 1 ||
            i == totalPage ||
            (i >= currentPage - 2 && i <= currentPage + 2)
        ) {
            displayPages.push(i);
        }
    }
    
    let previousPage;

    for (const page of displayPages) {
        if (previousPage) {
            if (page - previousPage === 2) {
                items.push(
                    <BootstrapPagination.Item
                        key={previousPage + 1}
                        active={previousPage + 1 === currentPage}
                        onClick={() => {
                            onClick(previousPage + 1);
                        }}
                    >
                        {previousPage + 1}
                    </BootstrapPagination.Item>
                );
            } else if (page - previousPage > 2) {
                items.push(<BootstrapPagination.Ellipsis key={previousPage + 1} />);
            }
        }
        items.push(
            <BootstrapPagination.Item
                key={page}
                active={page === currentPage}
                onClick={() => {
                    onClick(page);
                }}
            >
                {page}
            </BootstrapPagination.Item>
        );
        previousPage = page;
    }

    return (
            <BootstrapPagination>
                <BootstrapPagination.First />
                <BootstrapPagination.Prev />
                {items}
                <BootstrapPagination.Next />
                <BootstrapPagination.Last />
            </BootstrapPagination>
    );
};

変数名も既存のサンプルと比べて意味のある名前に変更し直しました。

少しだけ解説

上記で記事の最初のコンポーネントは作れるので、すぐ実装したい方はコピペして、ここで読むのをやめていただいても構いません。
ですが、せっかくなので、簡単にロジックの解説をします。

    for (let i = 1; i <= totalPage; i++) {
        if (
            i == 1 ||
            i == totalPage ||
            (i >= currentPage - 2 && i <= currentPage + 2)
        ) {
            displayPages.push(i);
        }
    }

ここでは、

  • 「最初のページ数(1)」と「最後のページ数」は必ず表示する
  • 「現在のページ数-2」 ~ 「現在のページ数+2」の範囲のページ数は、「最初のページ数(1)」~「最後のページ数」の範囲を超えない限り表示する

の2つの仕様を満たすために、表示すべきページ数のリストをdisplayPagesとして取得しています。

この2つの仕様だけであれば、

    for (const page of displayPages) {
        items.push(
            <BootstrapPagination.Item
                key={page}
                active={page === currentPage}
                onClick={() => {
                    onClick(page);
                }}
            >
                {page}
            </BootstrapPagination.Item>
        );
    }

これだけで表示すべきページ数のコンポーネントのリストを作ることができますが、他に下記の要件があります。

  • 「現在のページ数-2」が4以上のとき
    • 「1」と「現在のページ数-2」の間に省略記号を表示する
  • 「現在のページ数-2」が3のとき
    • 「1」と「現在のページ数-2」の間に「2」を表示する
  • 「現在のページ数+2」が「最後のページ数-3」以下のとき
    • 「現在のページ数+2」と「最後のページ数」の間に省略記号を表示する
  • 「現在のページ数+2」が「最後のページ数-2」のとき
    • 「現在のページ数+2」と「最後のページ数」の間に「最後のページ数-1」を表示する

これを満たすためにpreviousPageという変数に一個前のページ数を保持しておきます。

    let previousPage;

    for (const page of displayPages) {
        ...
        items.push(
            <BootstrapPagination.Item
                key={page}
                active={page === currentPage}
                onClick={() => {
                    onClick(page);
                }}
            >
                {page}
            </BootstrapPagination.Item>
        );
        previousPage = page;
    }

そして下記のロジックが、

            if (page - previousPage === 2) {
                items.push(
                    <BootstrapPagination.Item
                        key={previousPage + 1}
                        active={previousPage + 1 === currentPage}
                        onClick={() => {
                            onClick(previousPage + 1);
                        }}
                    >
                        {previousPage + 1}
                    </BootstrapPagination.Item>
                );
            }
  • 「現在のページ数-2」が3のとき
    • 「1」と「現在のページ数-2」の間に「2」を表示する
  • 「現在のページ数+2」が「最後のページ数-2」のとき
    • 「現在のページ数+2」と「最後のページ数」の間に「最後のページ数-1」を表示する

に対応しており、

下記のロジックが、

	   else if (page - previousPage > 2) {
                items.push(<BootstrapPagination.Ellipsis key={previousPage + 1} />);
	   }
  • 「現在のページ数-2」が4以上のとき
    • 「1」と「現在のページ数-2」の間に省略記号を表示する
  • 「現在のページ数+2」が「最後のページ数-3」以下のとき
    • 「現在のページ数+2」と「最後のページ数」の間に省略記号を表示する

の要件に対応しているロジックになります。

previousPageという変数を用いることで

  • 前のページから3ページ以上離れている場合には省略記号のコンポーネントを追加する
  • 前のページから2ページ離れている場合にはあいだのページ数のコンポーネントを追加する

というロジックを実現できる、という仕組みになっています。

まとめ

よくあるUIに関する実装はすでに誰かしら実装しているはずなので、まずググりましょう。