コンテンツにスキップ

明細行

受注フォーム、見積書、請求書: ヘッダ+明細 N 行、それぞれに数量・ 単価・行合計、フッタに小計/税/合計。このレシピはそのテーブルを ハイパーメディア契約にしたものです — 1 エンドポイント、1 つの形: フォーム全体を POST し、フォーム全体が再描画されて返る。クライアントは 数値を一切計算しません。丸めは業務の真実であり、実装はサーバに ただ 1 つです。

別名: 明細テーブル、複数行フォーム。

数量や単価を変えて(フィールドを離れると change が発火)、行を追加・ 削除してみてください — どの操作も同じ「フォーム全体の POST」で、 合計は常にサーバ計算で返ってきます(10% の税はサーバ側で切り捨て)。 数量に 3..5 と入れると 422 の形が見えます: 生の値がそのまま エコーバックされ、入力がマークされ、合計はダッシュに。エンドポイントは api/recipes/line-items/ 配下の契約のデモ実装です。

Loading quote…

<form method="post" action="/quotes/42/recalc" id="quote" data-hc-line-items>
<table class="hc-table">
<thead>…品目 / 数量 / 単価 / 行合計 / (削除)…</thead>
<tbody>
<tr>
<td><input class="hc-input" name="item" value="Widget" aria-label="Item"></td>
<td><input class="hc-input" name="qty" value="3" inputmode="numeric" aria-label="Quantity"
data-hx-post="/quotes/42/recalc" data-hx-trigger="change"
data-hx-target="#quote" data-hx-swap="outerHTML"></td>
<td><input class="hc-input" name="price" value="1200" inputmode="numeric" aria-label="Unit price"
data-hx-post="/quotes/42/recalc" data-hx-trigger="change"
data-hx-target="#quote" data-hx-swap="outerHTML"></td>
<td data-cell="line-total">¥3,600</td>
<td><button class="hc-button" data-variant="ghost" type="submit" name="remove-row" value="1"
data-hx-post="/quotes/42/recalc"
data-hx-target="#quote" data-hx-swap="outerHTML">Remove</button></td>
</tr>
</tbody>
<tfoot>…小計 / 税 / 合計、すべてサーバレンダリング…</tfoot>
</table>
<button class="hc-button" type="submit" name="add" value="1"
data-hx-post="/quotes/42/recalc"
data-hx-target="#quote" data-hx-swap="outerHTML">Add row</button>
<!-- Save is a different endpoint — formaction is the no-JS path -->
<button class="hc-button" data-variant="primary" type="submit"
formaction="/quotes/42" data-hx-post="/quotes/42"
data-hx-target="#quote" data-hx-swap="outerHTML">Save quote</button>
</form>

それぞれがこの形である理由:

  • 行は同名フィールドの繰り返しで位置的に整列。 各行が itemqtyprice を 1 つずつ供出し、ツリー順直列化が 3 つ組の対応を 保ちます — 並べ替えリストデータグリッドのスナップショットページャーと 同じ仕様保証です。items[0].qty 式の添字管理がないので、行の増減で 番号を振り直すものが何もありません。
  • どの操作も同じリクエスト。 qty/pricechange で発火。 Add と Remove は name/value が動詞の submit ボタン。エンドポイントは 押されたボタンで分岐し、JS オフでもネイティブ submit が同じことを します。
  • フォーム全体を outerHTML でスワップ。 レスポンスが唯一の 計算機です — 行合計・小計・税・合計はすべて描画済みで届きます。 それ以外のスワップは hc validate がエラーにします。
  • data-hc-line-items は契約マーカーで、ビヘイビアは何もアタッチ しません。

数値はフィールド単位で検証し、レスポンスは同じ再描画フォームです: 不正な生の値をそのままエコーバック(黙って矯正しない — ユーザーは サーバが見たものを見るべき)、入力に aria-invalid="true"、メッセージは 行の中、そしてどこかの行が不正な間、合計は**「—」** — 古い数字や 部分的な数字は決して出しません。ステータスは 422 (ミューテーションフォームの 1 行 beforeSwap 許可を併用)。

フォーカス — 唯一の実トレードオフ

Section titled “フォーカス — 唯一の実トレードオフ”

change 後のフォーム全体スワップは、ユーザーがいま離れた入力を 再描画します。キーボードでテーブルを渡っていると、スワップ時点で フォーカスを失います。契約の基本姿勢: 受け入れるchange は blur で発火し、たいていの入力画面では「真実は一つ」がフォーカス保持に 勝ちます。文書化された絞り込み変種は、同じレスポンスのまま派生セル だけをスワップします(data-hx-select-oob="#totals" + 行合計セルの id)— フォーカスは生き残り、代償はすべての派生セルへの id 付与。 キーボード中心の画面なら データグリッドの編集エラーの グリッドも検討を。

リクエストレスポンス
POST /quotes/42/recalc(行 ± add / remove-row=N)200 + フォーム全体の再描画
POST /quotes/42/recalc(不正な数値)422 + 同じフォーム、生の値エコー、合計は「—」
POST /quotes/42(保存 — ボタンの formaction)recalc と同じ検証。成功はミューテーションフォームの流儀で

プログレッシブエンハンスメント

Section titled “プログレッシブエンハンスメント”

すべての操作コントロールは name/value 付きのネイティブ submit です。 htmx なしではフォームが action へ POST し、サーバが同じ再描画 フォームのフルページを返します。保存ボタンの formaction が JS オフ でも 2 つのエンドポイントを正直に保ちます。

  • 入力には aria-label を(列ヘッダは列のラベルであって、コントロールの ラベルではありません)。
  • エラーメッセージは行の中、対象入力の隣に。入力には aria-invalid
  • 行に同一性があるなら削除ボタンに行名を(「Widget を削除」)。
  • ドラフト自動保存と、 ダーティガードは未保存ガードと 合成できます。
  • 行の並べ替え<tbody>並べ替えリストを適用する だけ — 行を動かせば入力も動き、位置的契約が新しい順序をそのまま 拾います。
  • 同時編集: 1 つの見積を 2 人で編集するなら、 編集競合の hidden version をこの同じフォームに同乗させます。