おはようございます。株式会社GxP 向山です。年中半袖Tシャツで過ごしています。
以前の記事から"新卒入社"の肩書が消えました。何故でしょうか🤔
そんなことより、こちらはグロースエクスパートナーズ Advent Calendar 2023の9日目の記事となります。
今回は普段から一覧画面とかで見かけるアレをBootstrapであれしてA.R.E.していきたいと思います。
Paginationってなんだよ
ページャー(pager)、またの名をページネーション(pagination)
一覧画面で1画面に収まりきらない場合に予め表示する件数を決めておき、溢れた分を次ページにして行き来ができるアレです。
Qiitaだと2023/12/08現在はこんな感じになってますね。
矢印で1ページごとに進む感じ。
Bootstrap1ではpagination
の名前で部品が提供されているので、今回はpagination
で進めます。
いざ作ろうとした時に検索ワードが悪かったのか自分が求めているものがヒットせず、作るのに少し苦労したので備忘録として残していきます。
結論
他サイトの記事2を参考にしつつ、自分が作りたかったpaginationがこちらになります。
See the Pen Untitled by smukoyama (@smukoyama-gxp) on CodePen.
はい、こんな感じに最初と最後のページにいてもpaginationの横幅が変動しないモノを作りたかったのでした。
上のはCodePen上のみで動かせるように調整したものなので、実際に組む時はこんな感じ。(Thymeleafの場合)
/**
* paginationを生成
* @param currentPage 現在のページ数
* @param maxPage 計算済みの最大ページ数
* @param visiblePage 現在のページから表示する前後のページ数
*/
function createPagination(currentPage, maxPage, visiblePage) {
// 表示するページの範囲を計算
let startPage = currentPage - visiblePage
let endPage = currentPage + visiblePage
// paginationの設定
let pagination = $('<ul class="pagination justify-content-center">')
// 最初のページ
let first = createPaginationLink(1, '<i class="fa-solid fa-angles-left"></i>', currentPage !== 1)
pagination.append(first)
// 前のページ
let back = createPaginationLink(currentPage - 1, '<i class="fa-solid fa-angle-left"></i>', startPage > 1)
pagination.append(back)
// ページ番号
if (startPage < 1) {
endPage += -(startPage - 1) // 1未満のページ数をendPageに加算
startPage = 1
} else if (maxPage < endPage) {
startPage -= (endPage - maxPage) // maxPage以上のページ数をstartPageに加算
if (startPage < 1) {
startPage = 1
}
endPage = maxPage
}
for (let i = startPage; i <= endPage && i <= maxPage; i++) {
let number = createPaginationLink(i, i, true)
if (i === currentPage) {
number.addClass('active')
number.css('pointer-events', 'none')
}
pagination.append(number)
}
// 後のページ
let next = createPaginationLink(currentPage + 1, '<i class="fa-solid fa-angle-right"></i>', endPage < maxPage)
pagination.append(next)
// 最後のページ
let last = createPaginationLink(maxPage, '<i class="fa-solid fa-angles-right"></i>', currentPage !== maxPage)
pagination.append(last)
// 生成したpaginationを設定
$('#pagination').html(pagination);
}
/*
* URLから指定したパラメータを取得
*/
function getQueryParam($key) {
if (1 < document.location.search.length) {
const query = document.location.search.substring(1);
const parameters = query.split('&');
for (let i = 0; i < parameters.length; i++) {
// パラメータ名とパラメータ値に分割する
const element = parameters[i].split('=');
if (element[0] === $key) {
return element[1];
}
}
}
return null;
}
/*
* paginationのリンクを生成
*/
function createPaginationLink(currentPage, label, isEnable) {
let pageLink = $(`<a class="page-link" href="?page=${currentPage}">`).html(`<div class="text-center" style="width: 1.5rem">${label}</div>`)
let pageItem = $('<li class="page-item">').html(pageLink)
if (!isEnable) {
pageItem.addClass('disabled')
}
return pageItem
}
<div class="container-fluid pt-3">
<!-- list -->
<div class="row">
<div class="col">
<table class="table">
...
</table>
</div>
</div>
<!-- pagination -->
<div class="row">
<nav class="col" id="pagination"></nav>
</div>
</div>
<script th:inline="javascript" type="text/javascript">
/*<![CDATA[*/
$(function () {
// paginationを生成
let maxPage = /*[[${maxPage}]]*/ 2
let visiblePage = /*[[${visiblePage}]]*/ 2
// ページ数を取得
let currentPage = Number(getQueryParam('page'));
if (currentPage < 1) {
currentPage = 1;
}
createPagination(currentPage, maxPage, visiblePage)
})
/*]]>*/
</script>
paginationはjavascriptを使って組み立て、最終的に<nav id="pagination"></nav>
に埋め込まれます。
説明
今回記事にした理由はもう一つあって、paginationを調べると実装方法がサンプルみたいで3ページぐらいまでしかないものが多く、動的にページ数を計算しているかつ説明している記事が無かったため作りました。
見た目のためにBootstrapとFont Awesomeを使ってますが、基本的にはそのままでもある程度動きます。
最大ページ数の計算
今回、Javaでサーバー側を開発しているため、DBから一覧に表示するためのリストを取得し、画面を表示する前に最大ページ数を計算しています。
計算ロジックは言うまででもないかなぁ、とは思いますが一応載せておくとこんな感じ。
// 一覧に表示するリストを取得
List<Entity> results = service.find();
// 最大のページ数を計算
Integer maxPage = 1;
if (results.size() > 0) {
double div = (double) results.size() / (double) pageSize; // pageSizeには1ページで表示する件数を設定
maxPage = (int) Math.ceil(div); // 小数点以下を切り上げ
}
paginationの生成
ブラウザにページが表示されるタイミングでURLのクエリパラメータにあるpage
から現在表示しているページ数を取得します。
取得できなかった場合はデフォルトで最初のページである1
を設定します。
createPagination
を呼び出し、paginationの生成が始まります。
// ページ数を取得
let currentPage = Number(getQueryParam('page'));
if (currentPage < 1) {
currentPage = 1;
}
paginationリストを組み立て
先に矢印やページ数を詰めるための要素を作っておきます。
let pagination = $('<ul class="pagination justify-content-center">')
justify-content-center
があるのは画面で中央に表示させるためです。
ここからpaginationのリンクを生成していきますが、処理を共通化させたかったのでcreatePaginationLink
を作成しています。
/*
* paginationのリンクを生成
*/
function createPaginationLink(currentPage, label, isEnable) {
let pageLink = $(`<a class="page-link" href="?page=${currentPage}">`).html(`<div class="text-center" style="width: 1.5rem">${label}</div>`)
let pageItem = $('<li class="page-item">').html(pageLink)
if (!isEnable) {
pageItem.addClass('disabled')
}
return pageItem
}
引数には移動先のページ数(page)
, 表示するラベル(label)
, リンクの活性状態(isEnable)
を指定していきます。
最初のページ, 前のページ
最初のページである1ページへ移動するリンクと、現在表示しているページから1つ前のページへ移動するリンクです。
// 最初のページ
let first = createPaginationLink(1, '<i class="fa-solid fa-angles-left"></i>', currentPage !== 1)
pagination.append(first)
// 前のページ
let back = createPaginationLink(currentPage - 1, '<i class="fa-solid fa-angle-left"></i>', startPage > 1)
pagination.append(back)
1ページ目を表示するため、最初のページへ移動するリンクには1を固定で指定し、前のページには現在のページから-1の値を指定します。
リンクの活性状態については最初のページへ移動するリンクの場合は1ページ目以外にいるか、
前のページへ移動するリンクの場合はstartPage
が1より大きい、つまりは1が表示されていないタイミングで活性となるようにしています。
現在のページ, 前後のページ
数字部分のページのリンクを作成します。
// ページ番号
if (startPage < 1) {
endPage += -(startPage - 1) // 1未満のページ数をendPageに加算
startPage = 1
} else if (maxPage < endPage) {
startPage -= (endPage - maxPage) // maxPage以上のページ数をstartPageから減算
if (startPage < 1) {
startPage = 1
}
endPage = maxPage
}
for (let i = startPage; i <= endPage && i <= maxPage; i++) {
let number = createPaginationLink(i, i, true)
if (i === page) {
number.addClass('active')
number.css('pointer-events', 'none')
}
pagination.append(number)
}
リンクを作成する部分については単純で、startPage
からendPage
までのページ数をループで回しています。
リンクの活性状態についてはどれも有効となるためtrue
としていますが、現在のページについてはリンクは不要なのとclassにactive
を追加するため、別でclassとcssを設定しています。
startPage
, endPage
のデフォルトの値として、現在表示しているページから前後に表示するページを計算しますが、最初や最後のページだった場合にマイナスや最大ページ数以上となって欠けてしまったページ数を逆側に拡張するため、条件を追加しています。
startPage
が1未満となった場合はendPage
に欠けてしまったページ数を加算したいため、startPage - 1
で0からのマイナス分として計算しておき、計算結果にマイナスを付けておくことで負数を正数に反転した値をendPage
に加算していきます。
例として「現在のページから表示する前後のページ数」に2
、「現在表示しているページ」が1
だった場合、startPage
には-1
が設定されています。この溢れた1ページをendPage
に加算したいため、-(-1) + endPage(1+2)
となりendPage
には4
が設定されます。
endPage
が最大ページ数以上となった場合はstartPage
から欠けてしまったページ数を減算したいため、こちらも同じ要領でendPage - maxPage
で溢れた分のページを計算し、startPage
を減算していきます。
この時、例えば「現在のページから表示する前後のページ数」に2
「最大ページ数」に3
、「現在表示しているページ」が3
だった場合、startPage
には5(endPage) - 3(maxPage)
で2
が算出され、1
だったstartPage
から引かれて1未満となってしまうため、この場合は1
を設定するようにしています。
因みにendPage
についてはfor文にてi <= maxPage
があるため上限を超えることはありません。
次のページ, 最後のページ
最初のページ, 前のページとあまり変化ないので説明は省略します。
// 後のページ
let next = createPaginationLink(currentPage + 1, '<i class="fa-solid fa-angle-right"></i>', endPage < maxPage)
pagination.append(next)
// 最後のページ
let last = createPaginationLink(maxPage, '<i class="fa-solid fa-angles-right"></i>', currentPage !== maxPage)
pagination.append(last)
paginationを埋め込み
ここまで組み立てたHTMLを<nav id="pagination"></nav>
へ埋め込みます。
// 生成したpaginationを設定
$('#pagination').html(pagination);
感想
よく見る部品なのでたくさん情報が転がっていると思っていたのですが、実際探してみると数字をハードコーディングで設定している記事が多く、意外に求めているものがなくてびっくりしました。
久々にロジックを考えさせられたので、夢中になった部分もあり、ちょっと楽しかったです。
最近はGoogleの検索一覧を見てみると「Load More」とか「無限スクロール」が採用されていてスマホに特化したUXなのかな、と思います。
ただ、これらのいずれもメリデメがあるため、利用者やシーンを考慮して適切な技術を採用して頂ければと思います。3