コンテンツにスキップ

データ入力フォーム

あらゆる業務アプリが繰り返すページです: データはきれいに届かなければ ならず、失われてもいけない長いフォーム。このテンプレートは入力 スタック全体を合成します — ワイヤ上は生値のままのグループ化された 金額、IME 正規化、住所を埋めるマスク付き郵便番号、デバウンスされた ドラフト自動保存、未保存ガード、保存ボタンのリクエスト衛生、そして セッション切れと編集競合のための共有エラーダイアログホスト。

何か編集してみてください: 未保存バッジが現れ(ダーティガード)、 2 秒手を止めるとフォームがドラフトされ(自動保存)、完全な郵便番号は 住所を埋め(ルックアップ + OOB)、Save はレコードを送信してガードを クリーンに戻します。金額は blur で再グループ化され、ワイヤ上は生値の ままです。

¥

Fullwidth digits normalize on blur; the wire value stays raw.

Try 123-4567 or 600-8216.

Pause two seconds after typing — the draft saves itself.

Unsaved changes

<!-- 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クライアント専用
郵便番号 → 住所installMaskpostal-addresspostal-address/contract.md
ドラフト自動保存—(純粋な htmx)autosaveautosave/contract.md
未保存ガードinstallDirtyGuardunsaved-changesunsaved-changes/contract.md
保存の衛生data-hx-sync + data-hx-disabled-eltエラーとリカバリー
セッション切れinstallSessionExpirysession-expirysession-expiry/contract.md
編集競合—(version フィールド)edit-conflictedit-conflict/contract.md
フィールドエラーinstallFieldErrorsfield-errorsfield-errors/contract.md
  • エンドポイントを改名する。 /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")はサーバー側で落としてください。