データ入力フォーム
あらゆる業務アプリが繰り返すページです: データはきれいに届かなければ ならず、失われてもいけない長いフォーム。このテンプレートは入力 スタック全体を合成します — ワイヤ上は生値のままのグループ化された 金額、IME 正規化、住所を埋めるマスク付き郵便番号、デバウンスされた ドラフト自動保存、未保存ガード、保存ボタンのリクエスト衛生、そして セッション切れと編集競合のための共有エラーダイアログホスト。
ライブテンプレート
Section titled “ライブテンプレート”何か編集してみてください: 未保存バッジが現れ(ダーティガード)、 2 秒手を止めるとフォームがドラフトされ(自動保存)、完全な郵便番号は 住所を埋め(ルックアップ + OOB)、Save はレコードを送信してガードを クリーンに戻します。金額は blur で再グループ化され、ワイヤ上は生値の ままです。
ページスケルトン
Section titled “ページスケルトン”<!-- method/action が非 JS のサブミットを生かします(post/redirect/get)。 disinherit が重要: htmx 属性は継承されるため、下の郵便番号 フィールドが発行する独自の lookup GET が disabled-elt を継承し、 自分の子孫にサブミットボタンが見つからず、lookup のたびに htmx が エラーをログしてしまいます。 hx-sync も disinherit が必須: 継承されると `this:abort` によって フォーム・郵便番号 lookup・autosave が 1 つの in-flight スロットを 共有し、2 秒ごとの下書き送信中に押した Save が無音で落ちます。 --><form id="report-form" data-hc-dirty-guard method="post" action="/reports/42" data-hx-post="/reports/42" data-hx-sync="this:abort" data-hx-disabled-elt="find button[type=submit]" data-hx-disinherit="hx-disabled-elt hx-sync" data-hx-target="#report-errors" data-hx-swap="innerHTML">
<!-- 422 のフィールドエラーフラグメントはここにスワップされ、 installFieldErrors が各項目を名指しされたフィールドへ分配します。 --> <div id="report-errors" aria-live="polite"></div>
<!-- Amount: grouped display, raw wire value --> <div class="hc-field"> <label class="hc-field__label" for="amount">Amount</label> <div class="hc-input-group"> <span class="hc-input-addon">¥</span> <input class="hc-input" id="amount" name="amount" type="text" inputmode="numeric" data-numeric data-hc-format="number" value="1,280,000"> </div> </div>
<!-- Codes: fullwidth leftovers self-correct --> <div class="hc-field"> <label class="hc-field__label" for="sku">SKU</label> <input class="hc-input" id="sku" name="sku" data-hc-normalize="ascii"> </div>
<!-- Postal → address (mask + lookup + OOB autofill) --> <div class="hc-field"> <label class="hc-field__label" for="postal">Postal code</label> <input class="hc-input" id="postal" name="postal" inputmode="numeric" placeholder="123-4567" pattern="\d{3}-\d{4}" data-hc-mask="postal-jp" aria-describedby="postal-result" data-hx-get="/address-by-postal" data-hx-trigger="change[target.value.length==8]" data-hx-include="this" data-hx-target="#postal-result"> <p class="hc-field__hint" id="postal-result" aria-live="polite"></p> </div> <div class="hc-field"> <label class="hc-field__label" for="pref">Prefecture</label> <input class="hc-input" id="pref" name="pref" autocomplete="address-level1"> </div> <!-- #city / #addr1 follow the same shape -->
<!-- Autosave: a request-owning div drafts the whole form --> <div data-hx-post="/reports/42/draft" data-hx-include="closest form" data-hx-trigger="input from:closest form changed delay:2s" data-hx-target="#draft-status" data-hx-swap="innerHTML"></div> <p class="hc-field__hint" id="draft-status" aria-live="polite"></p>
<button class="hc-button" data-variant="primary" type="submit">Save</button></form>
<!-- One shared host: 401 login dialogs and 409 conflict dialogs land here --><div id="error-dialog" data-hc-remote-dialog-root data-hc-session-expiry></div>
<script type="module"> document.body.addEventListener('htmx:beforeSwap', (event) => { if ([401, 409, 422].includes(event.detail.xhr.status)) { event.detail.shouldSwap = true; event.detail.isError = false; } });</script>| 領域 | コンポーネント / ビヘイビア | レシピ | 契約 |
|---|---|---|---|
| 金額フィールド | installFormat | — | クライアント専用(formdata 書き換え) |
| SKU フィールド | installNormalize | — | クライアント専用 |
| 郵便番号 → 住所 | installMask | postal-address | postal-address/contract.md |
| ドラフト自動保存 | —(純粋な htmx) | autosave | autosave/contract.md |
| 未保存ガード | installDirtyGuard | unsaved-changes | unsaved-changes/contract.md |
| 保存の衛生 | data-hx-sync + data-hx-disabled-elt | — | エラーとリカバリー |
| セッション切れ | installSessionExpiry | session-expiry | session-expiry/contract.md |
| 編集競合 | —(version フィールド) | edit-conflict | edit-conflict/contract.md |
| フィールドエラー | installFieldErrors | field-errors | field-errors/contract.md |
適応のしかた
Section titled “適応のしかた”- エンドポイントを改名する。
/reports/42と/address-by-postalはプレースホルダーです — 各領域はリンク先の契約だけを必要とします。 - レコードが並行編集されうるなら version フィールドを足す
(
<input type="hidden" name="version">) — edit-conflict の ダイアログは、既にページにあるエラーホストに乗ります。 - 本物の CSRF:
<meta name="csrf-token">+installCsrfHeader()— 再ログイン後のリプレイはローテートされたトークンを自動で拾います。 - 検証はサーバーに —
422+ field-errors フラグメント。スケルトンの許可が既にスワップを許しています。 - autosave 契約に従い、ドラフトに入れてはいけないフィールド
(
type="password")はサーバー側で落としてください。