Spring Boot 2 で、なるべく標準的なやり方で、トラディショナルな Spring MVC による Web Application を作成するチュートリアルを数回に分けて。
の4回目です。
目次
- Spring MVC で Hello World
- Spring Data JPA でデータベースアクセス
- 登録・更新処理と Bean Validataion
- Bootstrap と Thymeleaf でページネーション
- Spring Boot DevTools で Automatic Restart
- Spring Security でログイン認証
- ファイルアップロード
- T.B.D
今回は 「Bootstrap と Thymeleaf でページネーション」の回となります。
前回までで以下のような登録更新画面を作成しました。
一覧画面から遷移可能とし、一覧画面のページネーション表示をやっていきましょう。
一覧画面の修正
ここまでの一覧画面は以下のようになっています。
登録ボタンと編集用のリンクを追加しましょう。
src/main/resources/templates/members/membersList.html
を以下のように変更します。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Members</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous"> </head> <body> <div class="container"> <h2>Members</h2> <table class="table table-striped table-sm"> <thead> <tr> <th>ID</th> <th>name</th> <th>e-mail</th> </tr> </thead> <tbody> <tr th:each="member : ${members}"> <td th:text="${member.id}"/> <td> <a th:href="@{/members/__${member.id}__/edit}" th:text="${member.name}"/></a> </td> <td th:text="${member.email}"/> </tr> </tbody> </table> <a class="btn btn-outline-primary" th:href="@{/members/new}" role="button">Add Member</a> </div> </body> </html>
th:href
は <a>
タグのhref属性に値を設定します。
値にはリンク式 @{}
を使います。
リンク式では /
で始まる相対URLにコンテキストパスを自動で付与します。
リンク式中に変数を定義でき、ここでは __${member.id}__"
という形で Member の id をパス指定しています。
二重のアンダースコアで囲まれた式は、通常の式より先に評価されるプリプロセッシング式となります。
リンク式としての評価の前に id を埋め込む形となります。
新規登録用のボタンも定義し、th:href="@{/members/new}"
のようにしました。こちらは単純なリンクです。
画面はこのようになりました。
では、この一覧画面をページングできるように変更していきます。
Pagination の基本
まず、Bootstrap のPagination は以下形が基本になります。
<ul class="pagination"> <li class="page-item"> <a class="page-link" href="#" aria-label="Previous"><span>«</span></a> </li> <li class="page-item"><a class="page-link" href="#">1</a></li> <li class="page-item"><a class="page-link" href="#">2</a></li> <li class="page-item"><a class="page-link" href="#">3</a></li> <li class="page-item"> <a class="page-link" href="#" aria-label="Next"><span>»</span></a> </li> </ul>
これで以下のような Pagination 表示になります。
Previous とNext のリンクは、ページが存在しない場合は非活性にします。
これは クラスに disabled
を付け、リンクには tabindex="-1"
を指定してタブ選択できないようにします。
<li class="page-item disabled"> <a class="page-link" href="#" aria-label="Previous" tabindex="-1"> <span>«</span> </a> </li>
また、現在ページのリンクはクラスに active
をつけることで選択状態にします。
<li class="page-item active"> <a class="page-link" href="#">2</a> </li>
Previous リンクの作成
最初に Previous 部分(と Next部分)を作成しましょう。
該当ページが存在しない場合にリンクを非活性化する処理になります。
<li th:class="|page-item ${page.first ? 'disabled' : ''}|"> <a class="page-link" aria-label="Previous" th:href="@{''(page=${page.number - 1}, size=5)}"> <span>«</span> </a> </li> ・・・ <li th:class="|page-item ${page.last ? 'disabled' : ''}|"> <a class="page-link" aria-label="Next" th:href="@{''(page=${page.number + 1}, size=5)}"> <span>»</span> </a> </li>
Thymeleaf では、"|・・・|"
とすることでリテラル置換ができます。
${}
で書くこともできますが、クラス指定などはリテラル置換で書くと読みやすくなります。
上記では、先頭ページと最終ページの場合に disabled
を付けてリンクを非活性にしています。
th:href
にはリンク式で前後のページへのリンクURLを出力しています。リンクURL中で (page=999)
のように記載することで、クエリパラメータとして展開されます。 複数パラメータを付ける場合にはカンマ区切りで記載すれば良いです。
@{''(page=${page.number - 1}, size=5)}
の ''
の部分には、通常リンク先のURLを書きますが、空にすることで現在のURLとして補完されます。
ページリンクの作成
ページリンク部分を以下のように作成します。
現在ページから前後に3ページ分のリンクを表示する処理になります(先頭ページにいる場合や最終ページにいる場合などがあるので少し面倒です)。
<div th:with="s = ${(page.number + 3 > page.totalPages - 1) ? (page.totalPages - 1 - 6) : (page.number - 3)}"> <ul class="pagination pagination-sm"> ・・・ <li th:each='i : ${#numbers.sequence((s < 0 ? 0 : s), ((s < 0 ? 0 : s) + 6) > (page.totalPages - 1) ? (page.totalPages - 1) : ((s < 0 ? 0 : s) + 6))}' th:class="|page-item ${(page.number == i) ? 'active' : ''}|"> <a class="page-link" th:href="@{''(page=${i})}"> <span th:text='${i + 1}'>1</span> </a> </li> ・・・ </ul> </div>
テンプレートでやる範囲を超えていますね。。
th:with
でローカル変数を定義できます。
上記では、ページネーションで表示する開始ページを s
としてローカル変数定義しています。
th:each
で当該のタグを繰り返し出力します。
#numbers.sequence(x, y)
は xからyまでの整数のシーケンス(配列)を作成するユーティリティオブジェクトです。
ページのインデックスを生成して th:each
でリンクを生成しています。
先頭ページの表示
中間ページの表示
最終ページの表示
といった具合に表示されます。
全体は以下のようになります。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Members</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous"> </head> <body> <div class="container" th:with="page = ${members}"> <h2>TEST3</h2> <h2>Members</h2> <table class="table table-striped table-sm"> <thead> <tr> <th>ID</th> <th>name</th> <th>e-mail</th> </tr> </thead> <tbody> <tr th:each="member : ${members}"> <td th:text="${member.id}"/> <td> <a th:href="@{/members/__${member.id}__/edit}" th:text="${member.name}"/></a> </td> <td th:text="${member.email}"/> </tr> </tbody> </table> <div class="float-right" th:with="s = ${(page.number + 3 > page.totalPages - 1) ? (page.totalPages - 1 - 6) : (page.number - 3)}"> <ul class="pagination pagination-sm"> <li th:class="|page-item ${page.first ? 'disabled' : ''}|"> <a class="page-link" aria-label="Previous" th:href="@{''(page=${page.number - 1})}"> <span>«</span> </a> </li> <li th:each='i : ${#numbers.sequence((s < 0 ? 0 : s), ((s < 0 ? 0 : s) + 6) > (page.totalPages - 1) ? (page.totalPages - 1) : ((s < 0 ? 0 : s) + 6))}' th:class="|page-item ${(page.number == i) ? 'active' : ''}|"> <a class="page-link" th:href="@{''(page=${i})}"> <span th:text='${i + 1}'>1</span> </a> </li> <li th:class="|page-item ${page.last ? 'disabled' : ''}|"> <a class="page-link" aria-label="Next" th:href="@{''(page=${page.number + 1})}"> <span>»</span> </a> </li> </ul> </div> <a class="btn btn-outline-primary" th:href="@{/members/new}" role="button">Add Member</a> </div> </body> </html>
コントローラの変更
MemberController
を変更し、ページ要求を受け取れるようにします。
@GetMapping("/members") public String members(Model model, @PageableDefault(size = 10, sort = "id") Pageable pageable) { Page<Member> results = service.finaAll(pageable); model.addAttribute("members", results); return "members/membersList"; }
@PageableDefault(size = 10, sort = "id") Pageable pageable
を引数に追加すれば、ページのクエリがマッピングされます。
そして、@PageableDefault
アノテーションでページ要求のデフォルト値を定義できます。ここではページサイズ(1ページに何レコード表示するか)とソート条件をデフォルト指定しました。
実行結果
ページングするように初期データを増やしておきましょう。
@SpringBootApplication public class Main { private static final Logger log = LoggerFactory.getLogger(Main.class); public static void main(String[] args){ SpringApplication.run(Main.class, args); } @Bean public CommandLineRunner demo(MemberRepository repository) { return (args) -> { Arrays.asList( "jack", "david", "james", "helen", "linda","rafael", "henry","sharon","george","betty", "eduardo", "harold", "peter", "jean", "jeff", "maria", "carlos", "charlie", "mickey", "dan", "thom", "teddy", "gordon", "elvis", "colin", "joseph", "hayden", "lionel", "jeff", "tristan") .stream().forEach(name -> repository.save(new Member(name, name + "@example.com"))); for (Member member : repository.findAll()) { log.info(member.toString()); } }; } }
実行してみましょう。
$ ./gradlew bootRun
起動したら http://localhost:8080/members へアクセスすると以下のような表示となります。
今回はここまでで、次回に続きます。