Composable Life

Building with Go, Docker, and Terraform

【HTML Standard】1つのフォームで複数あるSubmitボタンの送信先を変える方法(formaction/formmethod)

概要

Webアプリケーションの管理画面で、一覧画面から複数のアイテムを選択して一括処理する機能を実装する場合があります。

例えば、以下の画面のように、選択した書籍に対して「一括で予約を有効にする」や「一括で書籍を削除」などの処理を実行する機能です。

従来はJavaScriptでフォームのactionを動的に変更していましたが、HTML Living Standardの formactionformmethod 属性を使うことで、HTMLのみで実装可能です。

実装方法

基本的な実装

<form>
  <input type="submit" value="予約を有効にする" formaction="/admin/book_reserve_acceptance" formmethod="POST"/>
  <input type="submit" value="削除" formaction="/admin/book_bulk_deletion" formmethod="POST"/>
  
  <table>
    <thead>
      <tr>
        <th></th>
        <th>書籍名</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><input type="checkbox" name="book_ids[]" value="1"/></td>
        <td>新刊1書籍名</td>
      </tr>
      <tr>
        <td><input type="checkbox" name="book_ids[]" value="2"/></td>
        <td>新刊2書籍名</td>
      </tr>
    </tbody>
  </table>
</form>

JavaScriptによる従来の実装との比較

従来の実装:

<form id="bookForm" method="POST">
  <button type="button" onclick="submitForm('/admin/book_reserve_acceptance')">予約を有効にする</button>
  <button type="button" onclick="submitForm('/admin/book_bulk_deletion')">削除</button>
</form>

<script>
function submitForm(action) {
  const form = document.getElementById('bookForm');
  form.action = action;
  form.submit();
}
</script>

formactionを使った実装では、JavaScriptが不要になります。

formaction, formmethodの仕様

<input type="submit"> および <button type="submit"> 要素で使用できる属性です。

  • formaction: form要素のaction属性を上書き
  • formmethod: form要素のmethod属性を上書き

MDNによると、これらの属性はHTML5で追加され、現在はすべてのモダンブラウザでサポートされています。

技術的な詳細

属性の優先順位

HTML仕様では、formaction/formmethodがform要素の属性より優先されます:

<form action="/default" method="GET">
  <!-- このボタンは /default にGETリクエストを送信 -->
  <button type="submit">デフォルト送信</button>
  
  <!-- このボタンは /custom にPOSTリクエストを送信 -->
  <button type="submit" formaction="/custom" formmethod="POST">カスタム送信</button>
</form>

サーバー側の実装例

Go言語(net/http)の場合:

// ルーティング
http.HandleFunc("/admin/book_reserve_acceptance", acceptReservationsHandler)
http.HandleFunc("/admin/book_bulk_deletion", bulkDeleteHandler)

// ハンドラー
func acceptReservationsHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    r.ParseForm()
    bookIDs := r.Form["book_ids[]"]
    
    // DBの更新処理(擬似コード)
    err := db.Model(&Book{}).
        Where("id IN ?", bookIDs).
        Update("reservation_status", "accepted").Error
    
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    http.Redirect(w, r, "/admin/books", http.StatusSeeOther)
}

動作確認方法

Chrome DevToolsのNetworkタブで確認すると:

  1. 「予約を有効にする」クリック時:POST /admin/book_reserve_acceptance
  2. 「削除」クリック時:POST /admin/book_bulk_deletion

それぞれ異なるエンドポイントにリクエストが送信されることが確認できます。

削除前の確認ダイアログ

削除処理では、誤操作を防ぐために確認ダイアログを表示することが推奨されます。

<form>
  <input type="submit" value="予約を有効にする" formaction="/admin/book_reserve_acceptance" formmethod="POST"/>
  <input type="submit" value="削除" formaction="/admin/book_bulk_deletion" formmethod="POST" id="deleteButton"/>
  
  <!-- テーブル部分は省略 -->
</form>

<script>
document.getElementById('deleteButton').addEventListener('click', function(e) {
    const checkboxes = document.querySelectorAll('input[name="book_ids[]"]:checked');
    
    if (checkboxes.length === 0) {
        alert('削除する書籍を選択してください。');
        e.preventDefault();
        return;
    }
    
    const confirmed = confirm(`${checkboxes.length}件の書籍を削除します。よろしいですか?`);
    if (!confirmed) {
        e.preventDefault();
    }
});
</script>

このコードにより:

  1. 何も選択されていない場合は警告を表示
  2. 選択件数を含む確認ダイアログを表示
  3. キャンセルされた場合はフォーム送信を中止

まとめ

formaction/formmethod属性を使用することで、複数の送信先を持つフォームをHTML標準の機能のみで実装できます。JavaScriptへの依存を減らし、よりシンプルなコードベースを維持できるため、管理画面などの業務システム開発では有用な選択肢となります。

参考