SpringBoot x Thymeleafでページングのパーツを作る

Webアプリケーションにそこそこよくあるものの一つに一覧表示とページングがあります。 SpringBootでは、JPAなどを使用すれば簡単にページング処理を使用してレコードを取得することができます。 しかしそのページングの操作をUIとして提供してくれる機構はザックリ調べた限り見つかりませんでした。 Railsならkaminariなどのページング用のコンポーネントがあるようですが…。

そこで、今後自前で実装する時に再利用できるように、コピペすればザックリ動くスニペット化したものを作りました。今日はその紹介をします。

まず実装イメージ

f:id:blackawa:20160320220940p:plain

このtableの上下の

  • n件中n~n件表示中
  • ページングのバー

が、今回作成した部分です。では構成を見ていきましょう。

今回のサンプルはgithubに、すぐに実行可能な状態で上がっていますのでそちらもご確認ください。

全体の構成

サーバサイドからは、ページングバーを描画するための情報や件数の情報を、専用のオブジェクトをThymeleafに渡して描画を行っています。

@Data
public class PagingView {
    private int totalRecordNum;
    private int fromRecordNum;
    private int toRecordNum;

    private int currentPageNum;

    private int recordPerPage;

    private boolean canGoNext;
    private String nextHref;

    private boolean canGoPrevious;
    private String previousHref;

    private boolean canGoLast;
    private String lastHref;

    private boolean canGoFirst;
    private String firstHref;

    private List<PagingViewElement> pagingViewElements;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PagingViewElement {
    private String name;
    private String href;
}

これをThymeleafに渡して、オブジェクトをバラしながら描画を行っていくことになります。

<!-- 件数表示 -->
<p th:text="
${paging.totalRecordNum} + '件中'
+ ${paging.fromRecordNum} + '~'
+ ${paging.toRecordNum} + '件表示中'">100件中1~10件表示中</p>
<!-- ページングバー -->
<div class="ui pagination small menu">
  <a class="icon item"
     th:classappend="!${paging.canGoFirst} ? 'disabled'"
     th:href="${paging.firstHref}"><i class="angle double left icon"></i></a>
  <a class="icon item"
     th:classappend="!${paging.canGoPrevious} ? 'disabled'"
     th:href="${paging.previousHref}"><i class="angle left icon"></i></a>
  <a class="item"
     th:each="element : ${paging.pagingViewElements}"
     th:href="${element.href}"
     th:text="${element.name}"
     th:classappend="${element.name} == ${paging.currentPageNum} ? 'disabled'"
  >1</a>
  <a href="#" class="icon item"
     th:classappend="!${paging.canGoNext} ? 'disabled'"
     th:href="${paging.nextHref}"><i class="angle right icon"></i></a>
  <a class="icon item"
     th:classappend="!${paging.canGoLast} ? 'disabled'"
     th:href="${paging.lastHref}"><i class="angle double right icon"></i></a>
</div>

サーバサイド

サーバサイドでは、件数を渡すのは簡単なのですがページングバーに表示する文字列とリンクの値を算出するのが少し複雑でした。 ページングバーのスペックは以下としました。

  • 現在のページがなるべく中央に表示される
  • 最初のページ、最後のページ、前ページ、次ページへのリンクが表示される。ただし遷移できる時にだけクリックできる
  • ページングバーのうち現在ページはクリックできない
  • ページングバーの長さは呼び出し元から指定できる

この要素を計算するロジックが以下です。

protected static List<PagingViewElement> generatePagingViewElements(
        int currentPageNum,
        int totalPageNum,
        int length,
        String preAppendPageNum
) {
    /* 偶数個のリストが要求された場合は現在のページが前寄せになる。
       例) [] がついているのが現在ページ
         << < 1 2 [3] 4 5 6 > >>
    */
    int backSpan = (length - 1) / 2;
    int forthSpan = (length - 1) - backSpan;

    int startIndex;
    int endIndex;

    if (currentPageNum - backSpan < 1) {
        // 表示幅に従うと存在しないページ(0ページ以下)が生成されるので、1ページから始める
        startIndex = 1;
        endIndex = length < totalPageNum ? length : totalPageNum;
    } else if (currentPageNum + forthSpan > totalPageNum) {
        // 表示幅に従うと存在しないページ(最終ページ以降)が生成されるので、表示領域を最終ページから逆算する
        startIndex = totalPageNum - (length - 1) > 1 ? totalPageNum - (length - 1) : 1;
        endIndex = totalPageNum;
    } else {
        // その間なので、中央にcurrentPageNumがくるように配置する。
        // ページのリストの端に当たっていないので、単純に中央にくるような両端を考えればよい。
        startIndex = currentPageNum - backSpan;
        endIndex = currentPageNum + forthSpan;
    }
    return IntStream.range(startIndex, endIndex + 1)
            .mapToObj(n -> new PagingViewElement(String.valueOf(n), preAppendPageNum + n))
            .collect(Collectors.toList());
}

クライアントサイド

クライアントサイドでは、渡された値を整理して描画を行います。

オブジェクト自身は今自分が何ページにいるかという情報のみを持っていてページングバーのどこをクリックできなくするかは知りません。そこで、ページングバーの要素を描画するたびに現在ページと突き合わせてクラス指定を制御しています。

th:classappend="${element.name} == ${paging.currentPageNum} ? 'disabled'"

まとめ

これらを組み合わせれば、ページングバーのフロントエンドを簡単に埋め込むことができます。

本当はjarとして配布してみたりしたいですが、htmlとjavaの混成をThymeleafで呼べる形で配布できるのでしょうか?

本格的に再利用を考えるならそのあたりも勘案する必要がありますね。