コンテンツにスキップ

ファイルアップロード

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 を放っておくと、完了直前にバーが 巻き戻ってしまうからです。
    ケースレスポンス
    成功200 — 新しいアイテムのフラグメント(#filesafterbegin に着地)に加えて、data-hx-swap-oob フラグメントとしてのまっさらなフォーム(file input はマークアップからリセットできません — サーバがフォームを再送し、htmx が再初期化します)+ HX-Trigger トースト
    バリデーション失敗422 + HX-Retarget: #upload-errors + HX-Reswap: innerHTMLfield-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 の契約を参照)。

    JavaScript なしでも、同じフォームがネイティブに正しいマルチパート リクエストを送信します。進捗とインラインエラーはエンハンスメントです。

    素のフィールドを hc-dropzone に 差し替えます — 他には何も変わりません。ドロップは同じネイティブ input に 代入して普通の change を発火するため、シリアライズ、進捗、OOB リセット、422 パスは同一です。サーバが再送するまっさらなフォームには、 単にまっさらなドロップゾーンのマークアップが入ります。

    ここでの主張 — 100 に到達するライブ進捗、out-of-band のフォーム リセット、リターゲットされた 422 — は、実マルチパートのブラウザテスト (test-browser/file-upload.spec.mjs)で固定されています。

    • 進捗バーはネイティブの <progress> で、progressbar ロールを 保ちます。aria-label を与えてください(レシピのマークアップは aria-label="Upload progress" を同梱)。表示されるのはリクエスト 中だけです。
    • ファイル入力は本物の <input type="file"> と本物の <label for> の ペアです — ネイティブのピッカー、キーボード挙動、アクセシブルな 名前がそのまま手に入ります。ドロップゾーン変種も同じ入力を保ち ます。
    • バリデーションエラーは field-errors の フラグメントとして届き、標準の aria-invalid / aria-describedby の配線と、最初の無効なコントロールへのフォーカスがついてきます。
    • 成功はトースト(role="status")が読み上げます。送信ボタンの リクエスト中の disabled はネイティブ属性で、支援技術が報告 します。