1. ホーム
  2. HTML

【HTML】formaction・formmethod の使い方|送信ボタンごとにフォームの送信先を変える方法

Share

記事の編集画面のように、1つのフォームに「下書き保存」と「公開する」の2つのボタンを置きたいことがあります。押されたボタンによって送信先を変えたい——そんなとき、JavaScript で form.action を書き換える方法を思いつくかもしれませんが、実は HTML だけで解決できます。送信ボタンに付けられる formaction をはじめとした form* 系の属性が、そのためのしくみです。この記事では5つの属性の働きと、どのボタンが押されたかをサーバー側で見分ける方法までをまとめて解説します。

送信先はフォームではなくボタン側でも決められる

フォームの送信先は <form> の action 属性で指定します。これはフォーム全体の設定なので、中にいくつボタンがあっても送信先は1つです。ところが実際のアプリケーションでは、同じ入力欄を使いながら処理を分けたい場面がよくあります。入力内容をそのまま保存するだけの「下書き保存」と、必須項目のチェックを通して公開する「公開する」は、送信先の URL もやりたいことも違います。

HTML には、この差分をボタン側から指定するしくみが用意されています。送信ボタンに formaction を付けると、そのボタンで送信したときに限って <form> の action を上書きできます。同じように method や target、入力検証の有無まで、ボタンごとに差し替えられます。

これらの属性を指定できるのは、送信を行う3種類の要素だけです。<button type="submit">、<input type="submit">、そして画像を送信ボタンにする <input type="image"> です。リセットボタンやただのボタンに書いても効果はありません。

formaction の基本|そのボタンのときだけ送信先を変える

もっとも単純な例から見ていきましょう。フォーム全体の送信先は /articles/publish ですが、「下書き保存」ボタンだけは /articles/draft に送ります。

form.html
<form action="/articles/publish" method="post">
  <input type="text" name="title" value="はじめての投稿">

  <!-- このボタンで送信したときだけ /articles/draft へ -->
  <button type="submit" formaction="/articles/draft">下書き保存</button>

  <!-- 属性を付けなければ form の action がそのまま使われる -->
  <button type="submit">公開する</button>
</form>

ポイントは「上書きは押されたボタンにだけ適用される」ことです。formaction を書いても <form> の action 自体は変わらないので、別のボタンで送信すれば元の /articles/publish に送られます。JavaScript で action を書き換える方法では、一度書き換えた値がフォームに残ってしまうため、状況によっては戻す処理まで必要になります。HTML の属性ならその心配がありません。

ここで type="submit" をわざわざ書いていますが、<form> の中に置いた <button> は type を省略すると送信ボタンとして扱われます。既定値が submit だからです。省略しても formaction は効きますが、意図を明示する意味でも書いておくほうが安全でしょう。

ボタン側で上書きできる5つの属性

上書きできるのは action だけではありません。<form> の属性名の頭に form を付けた名前が、そのままボタン側の属性名になっています。対応は次のとおりです。

ボタンに書く属性上書きする form の属性働き
formactionaction送信先の URL を変える
formmethodmethod送信方法を変える(get / post / dialog)
formenctypeenctypeデータのエンコード方式を変える
formtargettarget送信結果を表示する場所を変える(_blank など)
formnovalidatenovalidateブラウザーの入力検証をスキップする(値を持たない真偽値属性)

いずれも「そのボタンで送信したときだけ有効」という点は共通です。必要なものだけを、必要なボタンにだけ書けば十分です。以下、formaction 以外の4つを順に見ていきます。

formmethod|get・post・dialog を切り替える

formmethod は送信方法の上書きです。指定できる値は get、post、dialog の3つです。get は入力内容を URL のクエリ文字列に付けて送る方法で、検索や絞り込みのように「結果の URL を共有したい」ときに向いています。post は本文に載せて送る方法で、データを作成・更新する処理はこちらです。

本文を post で保存しつつ、プレビューだけは get で開きたい、というのが典型的な使い分けになります。

form.html
<form action="/articles/publish" method="post">
  <input type="text" name="title">

  <!-- 送信先とメソッドの両方を差し替える -->
  <button type="submit" formaction="/articles/preview" formmethod="get">
    プレビュー
  </button>

  <button type="submit">公開する</button>
</form>

3つめの dialog は少し毛色が違います。これは <dialog> 要素の中にあるフォームのための値で、データをどこにも送信せず、ダイアログを閉じるという動作になります。このとき閉じたダイアログの returnValue には、押されたボタンの value が入ります。確認ダイアログの「OK」と「キャンセル」を、どちらが押されたか判別しつつ閉じたい、といった場面で使えます。

dialog.html
<dialog id="confirm">
  <form method="dialog">
    <p>この記事を削除しますか?</p>
    <button value="cancel">キャンセル</button>

    <!-- ここだけメソッドを上書きすることもできる -->
    <button value="ok" formmethod="dialog">削除する</button>
  </form>
</dialog>

formenctype|送信データのエンコード方式を変える

formenctype は、フォームのデータをどんな形式に変換して送るかの上書きです。指定できるのは application/x-www-form-urlencoded(既定値)、multipart/form-data、text/plain の3つで、これは <form> の enctype と同じ顔ぶれです。ファイルを含むフォームでは multipart/form-data が必要になります。

なお、エンコード方式が意味を持つのは post で送るときだけです。get の場合は値が URL のクエリ文字列になるため、formenctype を書いても影響しません。使う機会は5つのなかで最も少ない属性ですが、同じフォームをファイル付き・ファイルなしの2経路に送り分けるようなときに役立ちます。

formtarget|プレビューだけ別タブで開く

formtarget は、送信結果をどこに表示するかの上書きです。値は <a> の target と同じ考え方で、同じタブに表示する _self(既定値)、新しいタブで開く _blank、フレームの親を指す _parent、最上位を指す _top が使えます。名前付きのブラウジングコンテキスト名を書くこともできます。

編集画面はそのままにしておきたいプレビューボタンは、まさにこの属性の出番です。formaction・formmethod と組み合わせると、編集中の内容をプレビュー用の URL へ get で送り、結果を別タブに表示する、という動きになります。

form.html
<button type="submit"
        formaction="/articles/preview"
        formmethod="get"
        formtarget="_blank">
  プレビュー(別タブ)
</button>

formnovalidate|下書き保存のときだけ検証を飛ばす

required や type="email" を指定した入力欄があると、ブラウザーは送信前に内容をチェックし、条件を満たしていなければ送信を止めてエラーを表示します。便利なしくみですが、「まだ書きかけだけど、いったん保存しておきたい」という下書き保存とは相性が悪いものです。必須項目が空のままでは保存すらできなくなってしまいます。

そこで使うのが formnovalidate です。値を持たない真偽値属性で、書いてあるだけで有効になります。このボタンで送信したときだけ検証がスキップされ、必須項目が空でもそのまま送信されます。

form.html
<form action="/articles/publish" method="post">
  <input type="text" name="title" required>

  <!-- 空でも保存できる -->
  <button type="submit" formaction="/articles/draft" formnovalidate>
    下書き保存
  </button>

  <!-- こちらは空だとブラウザーに止められる -->
  <button type="submit">公開する</button>
</form>

当然ながら、検証を飛ばせるのはブラウザー側のチェックだけです。受け取ったデータが妥当かどうかは、サーバー側で改めて確認する必要があります。formnovalidate は「入力を省いてもよい経路を用意する」ためのもので、検証そのものを不要にする属性ではありません。

どのボタンが押されたかをサーバー側で知る

送信先を分けるのではなく、同じ URL で処理を分岐させたいこともあります。このときに使えるのが、送信ボタンの name と value です。HTML のフォームには、送信に使われたボタンの name と value だけがデータに含まれるという決まりがあります。ボタンが3つあっても、押していない2つの値は送られません。

form.html
<form action="/articles/save.php" method="post">
  <input type="text" name="title">

  <!-- 押したほうの name=value だけが送られる -->
  <button type="submit" name="action" value="draft">下書き保存</button>
  <button type="submit" name="action" value="publish">公開する</button>
</form>

「下書き保存」を押せば action=draft が、「公開する」を押せば action=publish が送られます。同じ name を使い、value で区別するのがこのパターンの定石です。受け取る側では、その値を見て処理を振り分けます。

save.php
<?php
// 押されたボタンの name / value を受け取る
$action = $_POST['action'] ?? '';
$title  = $_POST['title'] ?? '';

if ( $action === 'publish' ) {
    // 公開処理
    echo '記事を公開しました:' . htmlspecialchars( $title, ENT_QUOTES, 'UTF-8' );
} elseif ( $action === 'draft' ) {
    // 下書き保存処理
    echo '下書きを保存しました。';
} else {
    // どのボタンとも一致しない(想定外の送信)
    http_response_code( 400 );
}

formaction と組み合わせる場合、送信先の URL 自体が分かれているので name / value は必須ではありません。ただ、両方を付けておくと受け取り側で「たしかにそのボタンから来た」と確認でき、後からルーティングを変えるときにも安全です。どちらか一方に寄せる必要はなく、併用してかまいません。

なお <input type="submit"> で同じことをしようとすると厄介です。<input> は value がボタンの表示ラベルを兼ねているため、送られる値が「公開する」という表示文字列そのものになってしまいます。表示を変えたらサーバー側の判定も壊れる、という状態です。表示ラベルと送信値を分けられる <button> を使いましょう。

書き方表示ラベル送信される値
<button name="action" value="publish">公開する</button>要素の中身(公開する)value の値(publish)
<input type="submit" name="action" value="公開する">value の値(公開する)同じく「公開する」

3つのボタンを持つフォームを動かしてみる

ここまでの内容をまとめて、「下書き保存」「公開する」「プレビュー」の3つのボタンを持つフォームを作ってみます。下のデモは実際のサーバーには送信せず、submit イベントを preventDefault() で止めて、押されたボタンに設定された送信先・メソッド・送信データを画面に表示します。ボタンごとに結果がどう変わるか、押し比べてみてください。

<form action="/articles/save" method="post">
  <label for="title">タイトル</label>
  <input type="text" id="title" name="title" value="はじめての投稿">

  <div class="buttons">
    <!-- 送信先を /articles/draft に差し替え。name と value も送られる -->
    <button type="submit" name="action" value="draft" formaction="/articles/draft">下書き保存</button>

    <!-- 属性なし。form の action / method がそのまま使われる -->
    <button type="submit" name="action" value="publish">公開する</button>

    <!-- 送信先とメソッドの両方を上書き。別タブで開く指定も付ける -->
    <button type="submit" formaction="/articles/preview" formmethod="get" formtarget="_blank">プレビュー</button>
  </div>
</form>

<pre id="log">ボタンを押すと、送信内容がここに表示されます。</pre>
body {
  font-family: sans-serif;
  line-height: 1.7;
  margin: 16px;
}

label {
  display: block;
  font-size: 14px;
  margin-bottom: 4px;
}

input[type="text"] {
  width: 100%;
  padding: 8px;
  box-sizing: border-box;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.buttons {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

button {
  padding: 8px 14px;
  border: 1px solid #007bff;
  border-radius: 4px;
  background: #fff;
  color: #007bff;
  cursor: pointer;
}

#log {
  margin-top: 16px;
  padding: 12px;
  background: #f5f5f5;
  border-radius: 4px;
  font-size: 13px;
  white-space: pre-wrap;
}
const form = document.querySelector('form');
const log = document.getElementById('log');

form.addEventListener('submit', (event) => {
  // このデモは実際には送信せず、送信内容を表示するだけにする
  event.preventDefault();

  // event.submitter が「実際に押されたボタン」
  const button = event.submitter;

  // form* 属性が無いボタンは、form 側の値がそのまま使われる
  const action = button.getAttribute('formaction') || form.getAttribute('action');
  const method = button.getAttribute('formmethod') || form.getAttribute('method');
  const target = button.getAttribute('formtarget') || '(指定なし)';

  // FormData の第2引数に submitter を渡すと、そのボタンの name / value も入る
  const data = new FormData(form, button);

  log.textContent =
    '送信先(action): ' + action + '\n' +
    'メソッド(method): ' + method + '\n' +
    'ターゲット(target): ' + target + '\n' +
    '送信されるデータ: ' + new URLSearchParams(data).toString();
});
Preview

JavaScript 側で使っている event.submitter は、その送信を引き起こしたボタンを返すプロパティです。ボタンが複数あるフォームで「どれが押されたか」を知りたいときは、これを見るのが確実です。あわせて new FormData(form, button) のように第2引数に submitter を渡すと、そのボタンの name / value を含んだ送信データを取り出せます。

属性を書いていないボタンでは、フォーム側の action と method がそのまま使われている点にも注目してください。上書きはあくまで「書いたボタンだけ」に効きます。

formnovalidate の効き目を確かめる

formnovalidate の働きは、実際に試すのがいちばん分かりやすいでしょう。次のデモのタイトル欄には required が付いています。空のまま「公開する」を押すとブラウザーに止められて入力を促されますが、「下書き保存」は formnovalidate が付いているのでそのまま送信されます。

<form action="/articles/publish" method="post">
  <label for="title">タイトル(必須)</label>
  <input type="text" id="title" name="title" required placeholder="空のまま押してみてください">

  <div class="buttons">
    <!-- 下書き保存だけは入力検証をスキップする -->
    <button type="submit" name="action" value="draft" formaction="/articles/draft" formnovalidate>下書き保存</button>

    <!-- 公開は検証あり。空だとブラウザーに止められる -->
    <button type="submit" name="action" value="publish">公開する</button>
  </div>
</form>

<pre id="log">タイトルを空にしたまま、2つのボタンを押し比べてみてください。</pre>
body {
  font-family: sans-serif;
  line-height: 1.7;
  margin: 16px;
}

label {
  display: block;
  font-size: 14px;
  margin-bottom: 4px;
}

input[type="text"] {
  width: 100%;
  padding: 8px;
  box-sizing: border-box;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.buttons {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

button {
  padding: 8px 14px;
  border: 1px solid #007bff;
  border-radius: 4px;
  background: #fff;
  color: #007bff;
  cursor: pointer;
}

#log {
  margin-top: 16px;
  padding: 12px;
  background: #f5f5f5;
  border-radius: 4px;
  font-size: 13px;
  white-space: pre-wrap;
}
const form = document.querySelector('form');
const log = document.getElementById('log');

form.addEventListener('submit', (event) => {
  // このデモは実際には送信せず、送信内容を表示するだけにする
  event.preventDefault();

  const button = event.submitter;
  const action = button.getAttribute('formaction') || form.getAttribute('action');
  const data = new FormData(form, button);

  // 検証をスキップしたボタンかどうかも表示する
  const validate = button.hasAttribute('formnovalidate') ? 'スキップ' : 'あり';

  log.textContent =
    '送信されました\n' +
    '送信先(action): ' + action + '\n' +
    '入力検証: ' + validate + '\n' +
    '送信されるデータ: ' + new URLSearchParams(data).toString();
});
Preview

止められたときは submit イベント自体が発生しないため、ログの表示も更新されません。逆にいえば、検証で止まったのか JavaScript 側の処理に問題があるのかを切り分ける手がかりにもなります。

formaction が効かないときに見るところ

属性を書いたのに送信先が変わらない、というときは、たいてい次のどれかに当てはまります。まず formaction というつづり(間にハイフンや大文字は入りません)を確認したうえで、以下を順に見てください。

ボタンが送信ボタンになっていない

もっとも多いのがこれです。type="button" や type="reset" のボタンは送信を行わないため、formaction を書いても何も起こりません。JavaScript で処理するつもりで type="button" にしたまま、あとから formaction を足した、というパターンでよく起きます。

逆に、フォームの中で type を省略した <button> は送信ボタンになります。「クリックで JavaScript の処理をするだけのつもりが、ページが再読み込みされてしまう」というのは、この既定値が原因です。送信させたくないボタンには必ず type="button" を書いてください。

ボタンがフォームの外にある

レイアウトの都合でボタンを <form> の外に出すことがあります。この場合、そのボタンはどのフォームにも属していないので、押しても送信されません。外に置きたいときは、form 属性にフォームの id を指定して結び付けます。

form.html
<form id="editor" action="/articles/publish" method="post">
  <input type="text" name="title">
</form>

<!-- フォームの外でも form 属性で結び付ければ送信できる -->
<footer>
  <button type="submit" form="editor" formaction="/articles/draft">
    下書き保存
  </button>
</footer>

JavaScript から送信している

form.submit() を呼んで送信している場合、押されたボタンという概念が存在しません。そのため formaction をはじめとする上書きは一切適用されず、ボタンの name / value も送られません。form.requestSubmit(button) のように送信ボタンを引数に渡せば、そのボタンをクリックしたのと同じ扱いになり、上書きも入力検証も働きます。

ボタンの値がサーバーに届かないとき

送信先は正しく変わったのに、どのボタンが押されたか判別できない——そんなときは、値が送られる条件を満たしているか確認します。

name 属性を付け忘れている

value だけを書いても送信データには入りません。フォームの部品は name があって初めて送信対象になるからで、これは入力欄でもボタンでも同じです。<button type="submit" value="publish"> のように name を落としていないか見直してください。

押していないボタンの値を期待している

送られるのは、あくまで送信に使われた1つのボタンだけです。3つのボタンすべてに name="action" を付けても、届くのは押された1つ分の値だけで、残りは存在しないものとして扱われます。したがって、受け取り側で $_POST['action'] が必ずあるものとして書くと、キーボード操作や特殊な経路で送信されたときにエラーになることがあります。先ほどの PHP の例のように、値が無い場合の分岐を用意しておくと安全です。

input type=”image” を使っている

画像を送信ボタンにする <input type="image"> でも formaction は使えますが、送信されるデータの形が違います。クリックされた座標が 名前.x と 名前.y という2つの値として送られ、value はそのままの形では送られません。ボタンの見た目を変えたいだけなら、CSS でスタイルを当てた <button> を使うほうが扱いやすいでしょう。

まとめ

送信ボタンに付ける formaction は、そのボタンで送信したときだけ <form> の action を上書きする属性です。同じ考え方で formmethod・formenctype・formtarget・formnovalidate が用意されており、送信方法やエンコード方式、表示先、入力検証の有無までボタンごとに変えられます。指定できるのは <button type="submit">・<input type="submit">・<input type="image"> の3つだけです。

同じ URL のまま処理を分けたいなら、<button name="action" value="publish"> のように名前と値を付けます。送信データに入るのは押されたボタンの分だけなので、サーバー側ではその値を見て分岐できます。表示ラベルと送信値を分けられる点でも、<input type="submit"> より <button> が扱いやすい選択です。

うまく動かないときは、そのボタンが本当に送信ボタンか(type="button" になっていないか)、フォームの中にあるか(外なら form 属性を付けているか)を確認してください。JavaScript で form.action を書き換える前に、まず HTML だけで足りないかを検討する——それがこの属性群のいちばんの使いどころです。

参考ページ