ファイルアップロード
file-upload は公認のマルチパートパターンです: ファイルを選んで
アップロードするとライブ進捗バーが表示され、新しいファイルの
フラグメントがリスト先頭に追加され(afterbegin スワップ)、フォームが
リセットされ、トーストが出ます。トランスポートは htmx の所有です(data-hx-encoding +
htmx:xhr:progress はネイティブ機能)。唯一の糊は、ネイティブの
<progress> を駆動する
installUploadProgress() です。
マークアップのバージョニングポリシーの
下で安定しています。
必要なのは installUploadProgress()、installToast()、
installFieldErrors()(いずれも自動初期化の
./behaviors
バンドルに同梱)です。
小さな PDF か PNG を選んでアップロードしてください: 進捗バーは
htmx:xhr:progress(installUploadProgress() ブリッジ)が駆動し、
レスポンスは新しいアイテムをリスト先頭に追記しつつ、まっさらなフォームを
out-of-band でスワップします — 公認の file input リセットです。許可外の
拡張子(たとえば .txt)を試すと、リターゲットされた 422
フィールドエラー経路が見られます。2 つ目のフォームは
ドロップゾーン変種です — ファイルをドロップ
(またはクリック)すると、同じ契約が専用のエラーコンテナと
out-of-band リセットに対して再現されます。アップロードは保存されません: デモは
アイテムをリクエストから導出して忘れるため、リロードするとリストは
リセットされます。
Dropzone variant — same endpoint, same contract
<form id="upload-form" method="post" action="/files" enctype="multipart/form-data" data-hx-post="/files" data-hx-encoding="multipart/form-data" data-hx-target="#files" data-hx-swap="afterbegin" data-hx-indicator="find progress" data-hx-disabled-elt="find button[type=submit]"> <div id="upload-errors"></div> <div class="hc-field"> <label class="hc-field__label" for="doc">Document</label> <input class="hc-input" id="doc" name="doc" type="file" required accept=".pdf,.png"> </div> <progress class="hc-progress htmx-indicator" data-hc-upload-progress value="0" max="100" aria-label="Upload progress"></progress> <button class="hc-button" data-variant="primary" type="submit">Upload</button></form>
<ul id="files">…server-rendered current files…</ul>2 つの機構がきれいに分かれています:
- 表示 / 非表示は htmx ネイティブ:
data-hx-indicator="find progress"+htmx-indicatorクラスにより、バーはリクエストの実行中だけ表示され ます。 - 値はブリッジ:
installUploadProgress()がhtmx:xhr:progressを 0–100 に対応付けます — リクエスト内で単調に。htmx はこのイベントを アップロードとレスポンスダウンロードの両フェーズで発火するため、 ダウンロード側の小さいtotalを放っておくと、完了直前にバーが 巻き戻ってしまうからです。
サーバレスポンス契約
Section titled “サーバレスポンス契約”| ケース | レスポンス |
|---|---|
| 成功 | 200 — 新しいアイテムのフラグメント(#files の afterbegin に着地)に加えて、data-hx-swap-oob フラグメントとしてのまっさらなフォーム(file input はマークアップからリセットできません — サーバがフォームを再送し、htmx が再初期化します)+ HX-Trigger トースト |
| バリデーション失敗 | 422 + HX-Retarget: #upload-errors + HX-Reswap: innerHTML に field-errors フラグメント — 主経路は属性宣言のまま、例外だけヘッダーで誘導 |
プロキシの 413 | アプリがリクエストを見る前に届くことがあり、htmx は何もスワップしません。契約には任意の htmx:responseError トーストスニペットがあります — また、親切な 422 が勝つよう、アプリ側の上限はプロキシの上限より低く保ってください |
| JS なし | 303 の post/redirect/get(mutating-form と同じ HX-Request 分岐) |
バリデーターはサーバです — クライアントの accept / サイズのヒントは
UX にすぎません。HX-Trigger ヘッダーの非 ASCII は \uXXXX に
エスケープしてください(ヘッダー値は latin-1 —
undo-delete の契約を参照)。
デグラデーションと後続
Section titled “デグラデーションと後続”JavaScript なしでも、同じフォームがネイティブに正しいマルチパート リクエストを送信します。進捗とインラインエラーはエンハンスメントです。
ドロップゾーン変種
Section titled “ドロップゾーン変種”素のフィールドを
hc-dropzone に
差し替えます — 他には何も変わりません。ドロップは同じネイティブ input に
代入して普通の change を発火するため、シリアライズ、進捗、OOB
リセット、422 パスは同一です。サーバが再送するまっさらなフォームには、
単にまっさらなドロップゾーンのマークアップが入ります。
ここでの主張 — 100 に到達するライブ進捗、out-of-band のフォーム
リセット、リターゲットされた 422 — は、実マルチパートのブラウザテスト
(test-browser/file-upload.spec.mjs)で固定されています。
アクセシビリティ
Section titled “アクセシビリティ”- 進捗バーはネイティブの
<progress>で、progressbarロールを 保ちます。aria-labelを与えてください(レシピのマークアップはaria-label="Upload progress"を同梱)。表示されるのはリクエスト 中だけです。 - ファイル入力は本物の
<input type="file">と本物の<label for>の ペアです — ネイティブのピッカー、キーボード挙動、アクセシブルな 名前がそのまま手に入ります。ドロップゾーン変種も同じ入力を保ち ます。 - バリデーションエラーは
field-errors の
フラグメントとして届き、標準の
aria-invalid/aria-describedbyの配線と、最初の無効なコントロールへのフォーカスがついてきます。 - 成功はトースト(
role="status")が読み上げます。送信ボタンの リクエスト中のdisabledはネイティブ属性で、支援技術が報告 します。
- プログレス — ブリッジが駆動するネイティブ要素。
- フィールドエラーレシピ —
422フラグメント。 - ミューテーションフォームレシピ —
HX-Request分岐とbeforeSwapの許可。 - トーストレシピ — 成功のフィードバック。