タイトルの件、人から尋ねられました。備忘録として、少し整理したものを初学者向けに解説します。
結論
form.submit() によってフォームがサブミットされたからといって、submit ボタンが押されたことにはならないためです。<button type="submit" name="button-id" value="123"> を押された際に送信される 123 はあくまでもそのボタンを 押した ときに送られます。
事象
以下のようなフォームがあるときに button-id の値が送信されない、といった事象がありました。 favorite の値は送信されます。
<form id="test-form">
<input type="text" name="favorite" value="CPU">
<button type="submit" name="button-id" value="123">go</button>
</form>
<script>
document
.querySelector('#test-form')
.addEventListener('submit', function(event) {
event.preventDefault();
/* 処理 */
event.target.submit();
});
</script>
解説
イベント
本来 Web ページは表示して終わりです(画面表示後にユーザーが目で眺めていても、そのことをスマホやパソコンは検知しないでしょう)。しかし実際にはユーザーが操作を行ったり(e.g. ボタンをクリックする)、 Web ページにちょっとした変化が起こったり(e.g. 途中だった画面の読み込みが完了する)といった様々な出来事が起きます。プログラマがこの出来事と処理を対応づけるために、出来事を管理する方法が提供されています。
ここでいくつか用語を導入します。発生する出来事を イベント といい、出来事が起きることを「イベントが 発火 する」といいます。たとえば「ユーザーがボタンをクリックした」場合であれば「 click イベントが発火する」といえます。イベントの発火と処理を結びつける方法は2つあります。ここではそのひとつ、イベントの発火時に実行する処理をあらかじめ登録しておき、発火を待ち受ける方法を紹介します。これを イベントリスナ といいます。
document
.querySelector('#test-form')
.addEventListener('submit', function(event) {
event.preventDefault();
/* 処理 */
event.target.submit();
});
先ほどのコードの一部です。 querySelector で指定した form タグに addEventListener メソッドでイベントリスナを登録します。第一引数の submit により、「 submit イベントの発生を待ち受ける」ことを指定し、第二引数の関数により、イベント発火時の処理を記載します。関数の引数には Event オブジェクトが渡されます。
preventDefault
event.preventDefault();
上記ではイベント発火時に行うべき処理を記載しました。しかし、開発者による明示的な処理をなにも書かずとも submit イベントの発火時には画面遷移が行われます( link タグの click イベントが発火したときもそうですね)。開発者が何もせずとも、あらかじめブラウザ( User Agent )に用意された既定の処理が実行されます。
preventDefault メソッドにはこの既定の処理を止める働きがあります。今回発火した submit イベントであれば、ページ遷移処理が取り消されます。
submit メソッド
event.target.submit();
evernt.target によってイベントが発火した HTML 要素への参照を取得できます。今回であれば form 要素です。 form 要素は JavaScript 上で HTMLFormElement クラスとして扱われます。 HTMLFormElement オブジェクトの submit メソッドを呼び出すと、フォームを送信させます。
submit ボタンの挙動
<button type="submit" name="button-id" value="123">go</button>
この submit ボタンがクリックされると button-id=123 がフォームのパラメータとして送信されます。ただし、 ボタン(とその値)がフォームの送信データに含まれるのは、そのボタン自身が送信のきっかけとなった場合のみです 。 preventDefault 後に関数内で submit() メソッドを呼び出しても、 submit ボタンの情報は送信されません。
A button (and its value) is only included in the form submission if the button itself was used to initiate the form submission.
https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element
submit ボタンの情報が送信されない理由
ここまでの話を整理すると、以下のようになります。
- submit ボタンのクリックによって submit イベントが発火される
- preventDefault によって 1. によるフォームの送信が取り消される
- submit() メソッド呼び出しによって( 1. とは別に新しく)フォームが送信される
- submit ボタンの情報は
3.では送信されない
背景等
以上、前から読むと「そんなの当たりまえ」と思われるかもしれません。わたしもそう思います。しかしあくまでも記載したサンプルは大幅に簡略化したものです。実際のコードは疑わしい箇所が多く、一瞬分からなくなってしまいました。
サブミットボタンの挙動について知りたかった点が MDN に載っておらず、 html.spec.whatwg.org から発見できて良かったです。ただ submit() メソッドの挙動の詳細は調べきれていません。私の予想では submit() では submit イベントが発火しないが requestSubmit() ならばするため、無限ループとなる はずなのですが、実際には無限ループは発生しませんでした。 event.submitter.click() でも無限ループは発生しませんでした。この点を理解したいです…。
最終的に、私が尋ねられた件ではそもそも preventDefault() と submit() を使う必要がありませんでした。が、この記事の目的は挙動の理由を理解することであり、どのように実装すべきかを検討することではないため、割愛します。