データグリッドのページネーション
hc-datagrid はページングされたデータのために作られています:
データウィンドウはサーバが所有し、htmx が 1 ページ分の行をスワップし、
installDatagrid() がスワップされた行を再初期化します。このレシピは
それを hc-pagination
ページャーに配線します。
5,000 件のフェイク商品をページ送りしてみてください — 各ページャー
リンクはそのページの行だけを <tbody> へスワップし(innerHTML)、
ページャーとステータス行は同じレスポンスで out-of-band に届きます。
エンドポイントはサーバレスポンス契約を実装した名前空間付きデモで、
api/recipes/datagrid-pager/ 配下にあります。(静的な docs サイトの
制約として、デモの tbody は 1 ページ目を load トリガーで取得します。
実サーバは下のとおり 1 ページ目をインラインで描画します。)
| ID | Name | Unit price |
|---|
Loading…
基本の使い方
Section titled “基本の使い方”1 ページ目はサーバサイドで、最初のページに描画します — JavaScript が
走る前からグリッドは満たされています。グリッドの <tbody> をスワップ
ターゲットにして、innerHTML(その内側の行)をスワップします:
各ページャーリンクは自分のページを同じ <tbody> へ読み込み、その
href="?page=N" は非 JS のパスであり続けます:
<div class="hc-datagrid"> <div class="hc-datagrid__scroll"> <table class="hc-datagrid__table"> <thead class="hc-datagrid__head"> <tr> <th class="hc-datagrid__headcell" data-frozen data-frozen-edge scope="col">ID</th> <th class="hc-datagrid__headcell" scope="col">Name</th> <th class="hc-datagrid__headcell" scope="col">Price</th> </tr> </thead> <tbody class="hc-datagrid__body" id="rows"> <!-- Page 1's rows, rendered by the server: --> <tr class="hc-datagrid__row"> <th class="hc-datagrid__cell" data-frozen data-frozen-edge scope="row">1</th> <td class="hc-datagrid__cell">Chai</td> <td class="hc-datagrid__cell">$18.00</td> </tr> <!-- … --> </tbody> </table> </div>
<nav class="hc-pagination" id="pager" aria-label="Pagination"> <a class="hc-pagination__item" aria-current="page" data-hx-get="/products?page=1" data-hx-target="#rows" data-hx-swap="innerHTML" href="?page=1">1</a> <a class="hc-pagination__item" data-hx-get="/products?page=2" data-hx-target="#rows" data-hx-swap="innerHTML" href="?page=2">2</a> </nav></div>なぜ outerHTML ではなく innerHTML か
Section titled “なぜ outerHTML ではなく innerHTML か”行を tbody の内側でスワップする(innerHTML)と、#rows 要素
そのものがスワップをまたいで安定します — その id と、要素が運ぶ
配線はページ読み込みのたびに生き残り、レスポンスは行だけで済みます。
<tbody> 全体の置き換え(outerHTML)は、ターゲットの属性を一言一句
再現する責任をすべてのレスポンスに負わせます: id="rows" を書き
忘れたフラグメント 1 つで、以後のページ送りが黙って壊れます。だから
#rows をターゲットに innerHTML でスワップします —
installDatagrid() はどちらのスワップ後でも ARIA roles、sticky
オフセットの計測、リサイズ済みの列幅を読み込まれたばかりの行へ
再適用します。
したがってサーバでレイアウトを再計算する必要はありません: 各行を
ヘッダーと同じ列構造で描画すれば(固定セルに data-frozen /
data-frozen-edge、リサイズ / 編集可能な列に data-col)、固定列の
--hc-datagrid-left オフセットとリサイズ済みの幅は自動で再適用され
ます。
サーバレスポンス契約
Section titled “サーバレスポンス契約”GET /products?page=N&size=100 はそのページの行だけを返します:
<tr class="hc-datagrid__row"> <th class="hc-datagrid__cell" data-frozen data-frozen-edge scope="row">101</th> <td class="hc-datagrid__cell">Chai</td> <td class="hc-datagrid__cell">$18.00</td></tr><!-- …one <tr> per row in the page… -->(この例は最小の 3 列グリッドです。グリッドに追加の列がある場合 —
ライブデモの行は固定の選択チェックボックスセルで始まります — すべての
<tr> がヘッダーと同じ順序で全列を持つ必要があります。)
ページャーとステータス行は同じレスポンスで out-of-band に更新し、 2 回目のリクエストなしで最新にします:
<nav class="hc-pagination" id="pager" data-hx-swap-oob="true" aria-label="Pagination"> …items, with aria-current="page" on the active page…</nav><p id="rows-status" data-hx-swap-oob="true" aria-live="polite">101–200 / 5,000</p>現在のページには aria-current="page" を付け、両端では Prev / Next を
aria-disabled="true" で無効化してください。
| リクエスト | レスポンス |
|---|---|
GET /products?page=2&size=100(htmx、HX-Request: true) | 200 + そのページの <tr> 行 + OOB のページャー / ステータスフラグメント |
GET /products?page=2(HX-Request なし — href フォールバック) | 200 + 2 ページ目の行をサーバレンダリングしたフルページ |
GET /products?page=2(失敗) | 非 2xx — スワップされず、現在の行とページャーが残ります |
htmx ≥ 2 は非 2xx レスポンスをスワップしないため、ページ読み込みが
失敗しても現在の行とページャーはそのまま残ります。失敗はエラー
レスポンスの HX-Trigger: {"hc:toast":{…}} ヘッダーで知らせてください
(トーストレシピを参照)。
プログレッシブエンハンスメント
Section titled “プログレッシブエンハンスメント”- 1 ページ目はサーバレンダリングなので、JavaScript がなくても グリッドは完全です — 初期行のためのクライアント取得はありません。
- すべてのページャーリンクは本物の
href="?page=N"を保ちます。 htmx なしでは、そのページへの通常のフルページナビゲーションになり ます。 - サーバは
HX-Requestヘッダーで分岐します: あれば行フラグメント- OOB のページャー / ステータスを返し、なければそのページの行を
<tbody>に入れたフルページを描画します。
- OOB のページャー / ステータスを返し、なければそのページの行を
アクセシビリティ
Section titled “アクセシビリティ”- ページャーは本物のリンクからなる
<nav aria-label="Pagination">です。現在ページはaria-current="page"で示し、端では Prev / Next をaria-disabled="true"で無効化してください。 - out-of-band のステータス行(
101–200 / 5,000)はaria-live="polite"を持つため、ページの変化はユーザーを遮らずに 読み上げられます。 - フォーカス。 行のスワップは直前のアクティブセルを取り除きます。 グリッドはタブ可能なセルを保ちますが、フォーカスは移動しません。 フローが必要とするなら、サーバから(out-of-band のフォーカス ターゲットなどで)フォーカスを復元してください。
- 選択はページ単位です。サーバが選択済み行を
aria-selected="true"つきで再描画しない限り、ページをまたいで 保たれません — ページ横断の選択はサーバ側で追跡してください。 - 複数行レコード。 この形は標準の 1
<tbody>行レイアウトが 対象です。複数行レコード では、代わりに包んでいる領域をスワップし、document のオブザーバーに 再初期化させてください。