コンテンツにスキップ

CRUD リストページ

hc-shell のアプリフレーム の main リージョンに、リストページの標準的な道具立てを組み上げます: ページングされたデータグリッド、 選択行への一括操作ツールバーリモートダイアログを 開く「New item…」ボタン、そして Undo つきで削除できる行。

先に正直な注記をひとつ: 各リージョンはデモ API のそれぞれ別の レシピ名前空間と通信します。そのためプレビューは、1 つのエンド ポイントが 4 つの契約すべてを提供しているふりをせず、リージョンを 並置しています。実アプリでは通常、1 つの /products リソースが 同じグリッドに対して pager・bulk・undo の契約をまとめて実装します — 契約は合成できます。デモのデータセットが別々なだけです。

  • Catalog (paged) — 5,000 件のフェイク商品をページ送り。各ページャ リンクは自分のページの行だけを <tbody> にスワップし、ページャと ステータス行は同じレスポンスでアウトオブバンドに届きます。
  • New item… — remote-dialog 名前空間の定型編集ダイアログ (items/123/edit。実アプリなら同じフラグメント形で GET /items/new を提供します)を開きます。名前を空にして保存すると 422 のエラー状態 ダイアログが返り、有効な保存はダイアログを閉じてトーストを上げます。
  • Bulk actions — 行をチェックするとツールバーが現れます。選択を Archive / Delete してみてください。Anvil (101) を含めると部分 失敗の warning トーストが見られます(サーバ側で保護されています)。
  • Delete with undo — 行を削除(確認なし)し、猶予期間内にトーストの Undo を押すと行がその場に復元されます。
Acme Admin

Products

Catalog (paged)

IDNameUnit price

Loading…

Bulk actions

IDProductStatus

6 products

Delete with undo

ItemActions

実アプリ形の URL で書いた完全なページです — pager + bulk の契約を 実装する 1 つの /products リソースと、ダイアログ編集・undo-delete の ための /items。1 ページ目の行はサーバサイドでレンダリングし、 JavaScript が動く前からグリッドが埋まっているようにします。トーストの マークアップは不要です: installToast() が自分でリージョンを作ります。

<body>
<div class="hc-shell">
<header class="hc-shell__header">
<button class="hc-button hc-shell__toggle" data-variant="ghost"
data-hc-shell-toggle aria-label="Open navigation" type="button"></button>
<strong>Acme Admin</strong>
</header>
<nav class="hc-shell__sidebar" aria-label="Primary">
<a href="/dashboard">Dashboard</a>
<a href="/products" aria-current="page">Products</a>
<a href="/settings">Settings</a>
</nav>
<main class="hc-shell__main hc-stack" style="--hc-stack-gap: 1.5rem;">
<!-- ページツールバー: タイトル + リモートダイアログのトリガー。 -->
<div class="hc-cluster" style="justify-content: space-between; align-items: center;">
<h1>Products</h1>
<button class="hc-button" data-variant="primary" type="button"
data-hx-get="/items/new"
data-hx-target="#dialog-root"
data-hx-swap="innerHTML"
data-hx-disabled-elt="this">New item…</button>
</div>
<!-- 取得した <dialog class="hc-dialog"> フラグメントはここにスワップされ、
installRemoteDialog が showModal() します。 -->
<div id="dialog-root" data-hc-remote-dialog-root></div>
<!-- 1 つのフォームが選択ツールバー・グリッド・ステータス行を包みます:
チェックされた行の id はネイティブのフォームシリアライズで運ばれます。 -->
<form method="post" action="/products/bulk">
<div class="hc-toolbar" role="toolbar" aria-label="Bulk actions"
data-hc-datagrid-actions="#products-grid" hidden>
<span data-hc-datagrid-count></span>
<button class="hc-button" type="submit" name="action" value="archive"
data-hx-post="/products/bulk"
data-hx-target="#products-rows" data-hx-swap="innerHTML"
data-hx-disabled-elt="this">Archive</button>
<button class="hc-button" data-variant="error" type="submit"
name="action" value="delete"
data-hc-confirm="Delete the selected products? This cannot be undone."
data-hc-confirm-title="Delete products"
data-hx-trigger="hc:confirmed"
data-hx-post="/products/bulk"
data-hx-target="#products-rows" data-hx-swap="innerHTML"
data-hx-disabled-elt="this">Delete</button>
</div>
<div class="hc-datagrid" id="products-grid">
<div class="hc-datagrid__scroll">
<table class="hc-datagrid__table">
<thead class="hc-datagrid__head">
<tr>
<th class="hc-datagrid__headcell" data-frozen scope="col">
<!-- 全選択: name 属性なし — 決してシリアライズさせない。 -->
<input type="checkbox" class="hc-checkbox" aria-label="Select all" />
</th>
<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">Unit price</th>
</tr>
</thead>
<tbody class="hc-datagrid__body" id="products-rows">
<!-- 1 ページ目の行はサーバがレンダリングします。undo-delete 契約に
従い、各 Delete ボタンは自分の行をサーバが返すトゥームストーンに
スワップします:
<tr id="item-101" class="hc-datagrid__row">
<td class="hc-datagrid__cell" data-frozen>
<input type="checkbox" class="hc-checkbox" name="ids" value="101"
aria-label="Select Anvil" />
</td>
<th class="hc-datagrid__cell" data-frozen data-frozen-edge scope="row">101</th>
<td class="hc-datagrid__cell">Anvil</td>
<td class="hc-datagrid__cell">
$1,200
<button class="hc-button" data-size="sm" type="button"
data-hx-delete="/items/101"
data-hx-target="closest tr" data-hx-swap="outerHTML"
data-hx-disabled-elt="this">Delete</button>
</td>
</tr>
-->
</tbody>
</table>
</div>
</div>
<div class="hc-cluster" style="justify-content: space-between; inline-size: 100%;">
<p id="products-status" aria-live="polite">1–100 / 5,000</p>
<!-- サーバは行の各ページと一緒にページャをアウトオブバンドで
再レンダリングします。リンクは非 JS パスとして href="?page=N" を
持ちます。 -->
<nav class="hc-pagination" id="products-pager" aria-label="Pagination">
<a class="hc-pagination__item" aria-current="page" href="?page=1"
data-hx-get="/products?page=1" data-hx-target="#products-rows"
data-hx-swap="innerHTML">1</a>
<a class="hc-pagination__item" href="?page=2"
data-hx-get="/products?page=2" data-hx-target="#products-rows"
data-hx-swap="innerHTML">2</a>
<!-- … -->
</nav>
</div>
</form>
</main>
</div>
</body>
リージョンコンポーネント / ブロックレシピ契約
アプリフレームhc-shell—(純 CSS。installShell() はモバイルナビ担当)
「New item…」ダイアログhc-dialogremote-dialogremote-dialog/contract.md
行 + ページャhc-datagrid + hc-paginationdatagrid-pagerdatagrid-pager/contract.md
一括操作ツールバーhc-toolbardatagrid-bulk-actionsdatagrid-bulk-actions/contract.md
行削除 + Undohc-toastundo-deleteundo-delete/contract.md
  • エンドポイントを差し替える。 /products/items はプレース ホルダーです — 各リージョンはリンク先の契約さえ実装されていればよく、 1 つのリソース上で自由に合成できます。
  • 本物の CSRF。 bulk の POST、ダイアログの保存、DELETE はすべて 変更系です — <meta name="csrf-token"> + installCsrfHeader() と、 非 JS フォームには隠しフィールドを追加します — CSRF トークン を参照。
  • 本物の認証。 リソースはフレームワークのセッション認証で守って ください — サーバインテグレーションガイド を参照。
  • Undo か確認か。 行ごとの削除は undo-delete (ダイアログなし)、より稀な一括削除には confirm-action の ゲートを残します。1 つのアクションに両方を重ねてはいけません。