データグリッドの無限スクロール
フィードや長いリストのための
datagrid-pager の
姉妹レシピです: tbody の最終行がセンチネル — ビューポートに
入ると次バッチを GET し、outerHTML で自分自身を新しい行 + 次の
センチネルに置き換え、末尾マーカーが引退させるまで続きます。
カーソルは最終行の id(after=)で、ページ番号ではありません:
追記型のリストはオフセットページングの下でずれますが、id はずれ
ません。新規 JavaScript ゼロ。
バージョニングポリシーの
対象です。
グリッドの中でスクロールしてください — ローディング行が コンテナの表示領域に入るたびに、自分を次の 5 行(全 15 行)と新しい センチネルに入れ替え、リストは「15 of 15」マーカーで閉じます。
このデモはコンテナスクロール変種です(intersect once root:…、
下のカーブアウトを参照): 15 行しかないデモを既定のウィンドウ
ビューポート基準 revealed で動かすと、背の高い画面では新しい
センチネルが常にすでに見えているため、スクロールする前に全件が
連鎖ロードされてしまいます。実際のページスクロールのフィード —
下のマークアップが示す形 — は revealed のままです。初期行も静的な
docs サイトゆえのカーブアウトです: デモは最初のバッチを load
トリガーで取得しますが、実サーバは 1 ページ目をインラインで描画
します。
| ID | Product | Price | Stock |
|---|
マークアップ
Section titled “マークアップ”まずグリッドのキャップを外します: hc-datagrid ルートに
--hc-datagrid-max-height: none を指定し、ページをスクローラに
します。既定のキャップ(70vh)はグリッドを自前のオーバーフロー
コンテナに変えます — そして revealed はウィンドウビューポート
基準のトリガーなので、グリッドの内側であふれた番兵は二度と発火
しません: フィードは最初のバッチでデッドロックします。自前の
スクロールバーを保ちたいグリッドは、代わりに下の intersect
変種を使ってください。
<div class="hc-datagrid" style="--hc-datagrid-max-height: none"> <div class="hc-datagrid__scroll"> <table class="hc-datagrid__table"> <thead class="hc-datagrid__head">…</thead> <tbody class="hc-datagrid__body"> <!-- Page 1 server-rendered — the grid is full without JS. --> <tr class="hc-datagrid__row"> <th class="hc-datagrid__cell" scope="row">item-1</th> <td class="hc-datagrid__cell">Compact Anvil</td> <td class="hc-datagrid__cell">$107</td> <td class="hc-datagrid__cell">12</td> </tr> <!-- …rows item-2 … item-5… -->
<!-- The sentinel: the last row, carrying the cursor. --> <tr class="hc-datagrid__row" data-hx-get="/items?after=item-5" data-hx-trigger="revealed" data-hx-swap="outerHTML"> <td class="hc-datagrid__cell" colspan="4" aria-live="polite"> <span class="hc-spinner" aria-hidden="true"></span> Loading… </td> </tr> </tbody> </table> </div></div>outerHTML スワップがトリック全体です: どの応答もセンチネルを更新
(バッチ + 次カーソル付きの新センチネル)するか、引退(バッチ +
末尾マーカー)させるかのどちらかなので、ローディング行は常に
ちょうど 1 つです。エンドポイントは行だけを返します。フルページ
しかレンダリングできないサーバーは data-hx-select="tbody > tr" を
足して行を切り出してください。
コンテナスクロールのグリッド
Section titled “コンテナスクロールのグリッド”revealed が測るのはウィンドウのビューポートです。自前の
スクロールバーを保つグリッド(既定の --hc-datagrid-max-height: 70vh
や任意の overflow-y: scroll ラッパー)は、代わりにコンテナを認識
するトリガーが必要です。契約の他の部分は何も変わりません:
<tr class="hc-datagrid__row" data-hx-get="/items?after=item-5" data-hx-trigger="intersect once root:#feed-scroll threshold:0.5" data-hx-swap="outerHTML"> …</tr>root: はスクロールコンテナのセレクタを取ります(通常は
.hc-datagrid__scroll — id を付けてください)。once は所属する
センチネルと一緒にオブザーバーを引退させ、threshold: は次のバッチを
読み込む前にどれだけ行が見えている必要があるかを決めます。
サーバーは更新したすべてのセンチネルに同じトリガーを反映する必要が
あります — カーソル URL に root を通すか、テンプレートに埋め込んで
ください(docs のデモはテンプレートに埋め込んでいます)。
これが避ける 2 つの失敗モード:
- デッドロック — グリッドの内側であふれた
revealedの センチネルはウィンドウビューポートに入らないので、フィードは最初の バッチで止まります。 - 連鎖ロード — 背の高いウィンドウでは短いリストの新しい
センチネルが最初から見えているため、すべてのバッチが即座に発火し、
「無限」リストが完成した状態で届きます。(上のライブデモが避けて
いるのはこれです。数百行の実フィードが素の
revealedで問題なく 動くのも同じ理由です。)
サーバー応答契約
Section titled “サーバー応答契約”GET /items?after=<cursor>:
| ケース | 応答(常に 200) |
|---|---|
| まだ行がある | 次の <tr> バッチ + 次カーソルを積んだ新しいセンチネル行 |
| リストの末尾 | バッチ(空もあり)にセンチネルなし、末尾行で閉じる: <td colspan aria-live="polite">40 of 40</td> |
| 古いカーソル | 最寄りの安定点からのバッチ — カーソルは再開可能で、4xx にはなりません。スクロールはエラーではありません |
バッチの行は初期行を鏡写しにします(同じクラス、id の
scope="row" ヘッダー)。スワップされた行はサーバーレンダリングの
1 ページ目と見分けがつきません。
プログレッシブエンハンスメント
Section titled “プログレッシブエンハンスメント”1 ページ目はサーバーレンダリングなので、リストは届いた時点で有用
です。JavaScript なしではセンチネルはただの行です。全件に到達可能で
ある必要があるなら、素朴な「もっと見る」リンク
(<a href="/items?after=…">)を用意してください — 同じ
エンドポイントが HX-Request 分岐でフラグメントとフルページの両方を
返します。
アクセシビリティ
Section titled “アクセシビリティ”- センチネル/末尾のセルは 1 つの
aria-live="polite"スロットです: 「Loading…」と最後の「40 of 40」が、フォーカスを奪わずに読み上げ られます。 - スピナーは
aria-hidden="true"— 意味を運ぶのは読み上げられる テキストです。 - 行は本物の
<table>の 1 つの<tbody>の中の本物の<tr>の ままです — 支援技術には積み重なるテーブルではなく、1 つの育つ テーブルが見えます。
- datagrid-pager — ページを指名(ジャンプ・共有・再開)する必要があるときの番号付き ページング。
- lazy-panel —
intersect onceのいとこ。オーバーフローコンテナの中で手を伸ばす べきトリガーです。 - データグリッド — 行のレンダリング先になるコンポーネントの CSS API。