Form内にsubmitボタンが2つあったらどうする?

submitボタンのおさらい

皆さんはForm内にあるsubmitボタンをどのように制御していらっしゃるでしょうか。 ボタンのような形をしているので、button要素と同様clickイベントのリスナーを設定して、対応するイベントハンドラで動作させると思いがちですが、そもそも、submitボタンはForm内にあってForm内のinput要素、textarea要素、select要素のvalue属性を集めてFormのaction属性に設定されたエンドポイントに送ることがデフォルトの動作となっています。 したがって、イベントリスナーの設定は当然に予定されているものではないのです。 もちろん、Formの機能としてはデータをエンドポイントに送るだけではないので、それ以外の動作をさせるためにはイベントリスナーは必要ですが、その場合でもsubmitボタンにclickイベントのリスナーを設定するのではなく、FormにonSubmitイベントリスナーを設定すべきだと思います。 ChatGPTにも意見を聞いてみました。

formの送信に関するイベントリスナを設定する場合、onSubmitをform要素に設定することが一般的に推奨されます。以下は、その理由を示すいくつかのポイントです: 全体性: onSubmitはフォーム全体の送信に関するイベントです。ユーザーがsubmitボタンをクリックするだけでなく、例えばフォームのテキストフィールド内でEnterキーを押すことによってもフォームが送信されることがあります。onSubmitは、これらすべての送信方法をカバーします。 バリデーション: フォームのデータバリデーションは一般的に送信前に行われます。onSubmitを使用すると、送信が行われる前にバリデーションを行い、必要に応じて送信を中止することが簡単にできます(event.preventDefault()を使用)。 セマンティクス: formのonSubmitは、フォームが送信されるというセマンティクス(意味)を持っています。このため、その目的に合わせてこのイベントを使用するのは自然です。 柔軟性: 複数のsubmitボタンが1つのフォーム内に存在する場合、それぞれのボタンにonClickリスナーを追加するよりも、フォーム全体にonSubmitリスナーを1つ追加する方が管理が簡単です。

ボタンが2つあったら?

しかし、Form内にsubmitボタンが2つあったらどうするのでしょうか? 入力内容を確認するようなFormがあるとすると、OKかキャンセルを選択するために2つのsubmitボタンがあるというのが一般的です。 この場合どちらのボタンがクリックされたか峻別する必要があります。 FormのonSubmitリスナーは1つしか設定できません。 では、それぞれのボタンにイベントリスナーを設定するしかないのでしょうか?

そんなことはありません。

FormのonSubmitリスナーでコールバックされるイベントハンドラは引数を渡してくれます。 この引数はオブジェクトになっていて、クリックされたボタンの情報も持っているのです。 そのため、これを利用すれば、次のようにボタンを制御できます。 Reactコンポーネントであれば、次のようになります。

const handleSubmit = (e) => {//⓵
    e.preventDefault();//⓶
    const click_id = e.nativeEvent.submitter.id;//⓷
    if (click_id === send_id) {
            console.log('ok');
        } else if (click_id === cancel_id) {
            console.log('cancel')
        }
};
return(
    <form onSubmit={handleSubmit}>
        <input type="submit" id="btn_id_ok" value="ok">
        <input type="submit" id="btn_id_cancel" value="cancel">
    </form>
)

⓵の行のeが、その引数です。 まず、⓶でpreventDefaultメソッドを使ってonSubmitのデフォルトの挙動を止めます。 そして、③ではクリックされたボタンについているidを取り出すことができます。 これが取得できれば、ハンドラの中で条件分岐による制御が可能になります。 ここではnativeEvent.submitterというオブジェクトが利用できるということを覚えておきましょう

jQueryではオブジェクトが違う

先に示した例はReactコンポーネントをjsxの記法でjsファイルに記述しました。 では、これをjQueryで書くとどうなるでしょうか。

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<form>
  <input type="submit" id="btn_id_ok" value="ok">
  <input type="submit" id="btn_id_cancel" value="cancel">
</form>

<script>
  const send_id = "btn_id_ok";
  const cancel_id = "btn_id_cancel";

  $('form').on('submit', function (e) {
    e.preventDefault();
    const click_id = e.nativeEvent.submitter.id;//⓵
    if (click_id === send_id) {
      console.log('ok');
    } else if (click_id === cancel_id) {
      console.log('cancel');
    }
  });
</script>

これで同じ動きになるかと思いきや、そうなりません。なぜなら、イベントハンドラに渡ったeがReactの場合と違うからです。 結論から言うとe.nativeEvent.submitter.id;ではなくe.originalEvent.submitter.id;となります。 なぜ、こんな違いが生じるのか、ChatGPTに聞いてみました。

jQuery: jQueryは既存のJavaScriptイベントを包括する独自のイベントオブジェクトを作成します。このオブジェクトはネイティブのイベントオブジェクトを包含していて、それにアクセスするためにはe.originalEventプロパティを使用します。このようにして、jQueryクロスブラウザの互換性を提供しています。

React: Reactは仮想DOMという概念を導入しており、実際のDOMとは異なるレベルの抽象化を提供しています。Reactも独自のイベントシステムを持っており、これはSyntheticEventシステムと呼ばれます。しかし、Reactはネイティブのイベントオブジェクトにアクセスするための方法も提供しており、それがe.nativeEventプロパティを使用する方法です。

これを読んで思い出しました。 "The React Way"という言葉です。 この言葉の意味を短く要約すると、 jQueryはDOMを直接操作してレンダリングするのに対し、Reactは状態変数を介して仮想DOMを生成し、実態のDOMとの差分をレンダリングすることを原則としつつもDOMの直接操作の方法も残しているということです(この話題については、こちらのブログで解説してますので、興味のある方はご覧ください。)。 つまり、jQueryにとっては.オリジナルな手法、すなわちoriginalEventなのですが、Reactにとっては、もとからあったネイティブな手法、すなわちnativeEventというわけなんだと理解しました。 こんなところにも"The React Way"という考え方が反映されているということが非常に興味深いと思いませんか。

まとめ

それはさておき、今回の結論をまとめておきます。

  1. submitはそれ自体にイベントリスナを設定するのではなく、formのonSubmitイベントリスナでクリック時の処理をすることが推奨される。
  2. ReactコンポーネントでonSubmitイベントリスナを設定したとき、イベントハンドラに渡される引数からはnativeEvent.submitterというオブジェクトでクリックされたsubmitを峻別することができる。
  3. jQeryでは$('form').on('submit',function(e){・・・})としたときのイベントハンドラに渡される引数からはoriginalEvent.submitterというオブジェクトでクリックされたsubmitを峻別することができる。

とりあえず、この3つを覚えておくとFormの操作に迷うことがなくなりそうです。