【結論】全件を一度に渡し、ThymeleafでJSON埋め込み → Wijmo CollectionView(pageSize) → MultiRowにバインド、Prev/Nextだけの超ミニマル構成で「必要最低限のページング」を実装する。
【要点】
- DTOは表示用に絞る:id・表示名など必要項目のみ(画像列はあればimageId程度)。
-
Thymeleafで初期データ埋め込み:
th:inline="javascript"
でitems
とpageSize
をJS変数化。 -
クライアント側だけでページング:
CollectionView({ pageSize })
を作り、MultiRow
のitemsSource
に渡す。 -
UIはPrev/Nextとページ情報のみ:余計なコンポーネントは不使用。
pageIndex
/pageCount
に連動してボタン活性/非活性。 -
MultiRowは2行レイアウトの最小定義:
rowsPerItem: 2
とlayoutDefinition
だけ。並べ替え・複数選択などはオフ。
【例】
1) DTO(表示用最小)
// 表示用DTO:必要な列だけ(必要に応じて項目を追加)
public record ItemDto(
Long id,
String code,
String name,
String category,
Integer price,
Long imageId // 画像を表示するならAPIに渡すID
) {}
※Controller 側(参考・任意):
model.addAttribute("items", itemService.findAllAsDto());
model.addAttribute("pageSize", 20);
2) Thymeleaf + HTML(最小ページ)
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>MultiRow Paging (Minimal)</title>
<style>
.toolbar { display:flex; gap:.5rem; align-items:center; margin: .5rem 0; }
.toolbar button:disabled { opacity:.4; cursor:not-allowed; }
#grid { height: 520px; }
</style>
<!-- Wijmo(手元の導入パスに置き換え) -->
<link rel="stylesheet" href="/lib/wijmo/styles/wijmo.css">
<script src="/lib/wijmo/wijmo.min.js"></script>
<script src="/lib/wijmo/wijmo.grid.min.js"></script>
<script src="/lib/wijmo/wijmo.grid.multirow.min.js"></script>
</head>
<body>
<div class="toolbar">
<button id="prevBtn">前へ</button>
<span id="pageInfo">–</span>
<button id="nextBtn">次へ</button>
</div>
<div id="grid"></div>
<!-- 初期データをThymeleafでJSへ埋め込み -->
<script th:inline="javascript">
/*<![CDATA[*/
const items = /*[[${items}]]*/ []; // List<ItemDto> がJS配列として展開される
const PAGE_SIZE = /*[[${pageSize}]]*/ 20;
/*]]>*/
</script>
<!-- 最小JS(後述のJSと同等。別ファイル化してもOK) -->
<script>
(function () {
// 1) ページング用コレクション(全件いっぺんに受け取り、画面側のみで分割)
const view = new wijmo.collections.CollectionView(items, { pageSize: PAGE_SIZE });
// 2) MultiRowのレイアウト(2行に分けて最小限の表示)
const layoutDefinition = [
{ cells: [
{ binding: 'id', header: 'ID', width: 80 },
{ binding: 'code', header: 'コード', width: 140 },
{ binding: 'name', header: '名称', width: 240, colspan: 2 }
]},
{ cells: [
{ binding: 'category', header: 'カテゴリ', width: 160 },
{ binding: 'price', header: '価格', width: 120, format: 'n0', align: 'right' },
// 画像を載せるなら itemFormatter を使って <img> を差し込む(最小構成では割愛)
]}
];
// 3) MultiRow本体
const grid = new wijmo.grid.multirow.MultiRow('#grid', {
itemsSource: view,
layoutDefinition,
rowsPerItem: 2,
selectionMode: wijmo.grid.SelectionMode.Row,
allowSorting: false, // 余計な機能はオフ(必要ならtrue)
alternatingRowStep: 0 // 見た目の交互色を消したい場合(任意)
});
// 4) Pager(Prev/Nextだけ)
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const pageInfo = document.getElementById('pageInfo');
function syncPager() {
pageInfo.textContent = `${view.pageIndex + 1} / ${view.pageCount} ページ(全 ${view.totalItemCount} 件)`;
prevBtn.disabled = view.pageIndex <= 0;
nextBtn.disabled = view.pageIndex >= view.pageCount - 1;
}
prevBtn.addEventListener('click', () => { if (view.pageIndex > 0) view.moveToPage(view.pageIndex - 1); });
nextBtn.addEventListener('click', () => { if (view.pageIndex < view.pageCount - 1) view.moveToPage(view.pageIndex + 1); });
view.collectionChanged.addHandler(syncPager);
view.pageChanged.addHandler(syncPager);
syncPager();
})();
</script>
</body>
</html>
3) 画像列を足したい場合(任意の最小追加)
// grid 生成時の options に itemFormatter を追加(最低限)
itemFormatter: (panel, r, c, cell) => {
const Col = wijmo.grid.CellType;
if (panel.cellType !== Col.Cell) return;
const col = panel.columns[c];
if (col.binding === 'imageId') {
const it = panel.rows[r].dataItem;
// サムネをオンザフライで軽く(必要に応じてw/h調整)
cell.innerHTML = `<img src="/api/images/${it.imageId}?w=64&h=64" alt="" style="width:64px;height:64px;object-fit:cover;border-radius:4px">`;
}
}
画像が要らなければ、この項はスキップでOK。必要最低限を崩さないために分離しています。
【用語】
ThymeleafのJSインライン=th:inline="javascript"
を付けた <script>
内で /*[[${var}]]*/
形式にすると、サーバ側のオブジェクトが安全にJSリテラルとして埋め込まれる。
CollectionView.pageSize=クライアント側でデータをページに分割するWijmoの仕組み。サーバ呼び出しは発生しない。
MultiRow=1件を複数行にレイアウトするグリッド。rowsPerItem
と layoutDefinition
が肝。