はじめに
この記事は 2023 年の MDN 翻訳 Advent Calendar 向けに作成したものです。
こんにちは。debiru です。ネタが尽きたのでアドベントカレンダーが空いてしまっています。
今日は適当に思いついた HTML Form 周りの挙動についてお話したいと思います。
input[type="text"]
で Enter キーを押すとサブミットされるのを防止する
<form action="https://params.lavoscore.org/" method="post">
<ul>
<li><input type="text" name="name" value="Alice"></li>
<li><button type="submit">submit</button></li>
</ul>
</form>
このような HTML フォームがあるとき、input[type="text"]
の編集中に Enter キーを押すと、フォームがサブミットされてしまいます。この挙動を防ぐにはどうしたらよいでしょうか。
input
要素の keydown
イベントを監視する
Enter キーが押されてもサブミットしないようにイベントハンドラで制御するという方法です。
(function() {
'use strict';
function delegateEvent(selector, type, listener, options) {
if (options == null) options = false;
document.addEventListener(type, evt => {
for (let elem = evt.target; elem && elem !== document; elem = elem.parentNode) {
if (elem.matches(selector)) return listener.call(elem, evt);
}
}, options);
}
delegateEvent('input', 'keydown', evt => {
if (evt.key === 'Enter') evt.preventDefault();
});
}());
この方法が一般的には用いられているかと思います。しかし別の方法も存在します。
disabled
なサブミットボタンを先頭に設置する
See the Pen Form prevent submit by Enter key by arcxor (@arcxor) on CodePen.
<form>
の子孫要素として最初に登場するサブミットボタンを disabled
にするだけです。
<button type="submit" disabled style="display: none;"></button>
form
にはデフォルトボタンという概念があり、form
要素内に最初に登場するサブミットボタンがデフォルトボタンとして扱われます。
- A form element's default button is the first submit button in tree order whose form owner is that form element.
- If the user agent supports letting the user submit a form implicitly (for example, on some platforms hitting the "enter" key while a text control is focused implicitly submits the form), then doing so for a form, whose default button has activation behavior and is not disabled, must cause the user agent to fire a click event at that default button.
拙訳:
- フォーム要素のデフォルトボタンは、そのフォーム要素がルートとなる構文木の順序において最初に登場する送信ボタンです。
- ユーザーエージェントが、ユーザーに暗黙的にフォームを送信させることをサポートしている場合(例えば、あるプラットフォームでは、テキストコントロールがフォーカスされている間に "enter" キーを押すと、暗黙的にフォームが送信されます)、デフォルトボタンがアクティブな振る舞いを持ち、無効化されていないフォームに対してこれを行うと、ユーザーエージェントはそのデフォルトボタンでクリックイベントを発生させなければなりません。
つまり、デフォルトボタンを disabled
にしておくことで、Enter キーが押された場合にフォームのサブミットが行われないようにするという方法です。
しかし、どうやら Safari ではこの方法は効果がないようです。
以下の関連記事もありますが、これ、仕様的にどうなっているのか、なぜ Safari では無視されるのかについてお分かりの方がいたらぜひコメント欄で教えてください。
関連記事
HTML Form の POST リクエストを JavaScript で再現する方法
<form action="https://params.lavoscore.org/" method="post">
<ul>
<li><input type="text" name="name" value="Alice"></li>
<li><input type="text" name="message" value="<xmp>"></li>
<li><button type="submit">submit</button></li>
</ul>
</form>
上記のような name
, message
をパラメータとして持つフォームを単純にサブミットすると、サーバーサイドでは次のようなパラメータが受け取れます。以下は PHP で処理した例です。
{
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"body": "name=Alice&message=%3Cxmp%3E",
"post": {
"name": "Alice",
"message": "<xmp>"
},
"get": [],
"files": []
}
JavaScript の処理で、この POST リクエストと同等のリクエストを送る方法は分かるでしょうか。今どきは XMLHttpRequest (XHR)
ではなく fetch
という新しい API が存在しているので fetch
を使いましょう。
fetch
の第 2 引数に { body: params }
を与えてみる
(function() {
'use strict';
async function doFetch() {
const url = 'https://params.lavoscore.org/';
const params = {
name: 'Alice',
message: '<xmp>',
};
const options = {
method: 'POST',
body: params,
};
const obj = await fetch(url, options).then(response => response.json());
console.log(obj);
}
document.addEventListener('DOMContentLoaded', doFetch);
}());
body
に params
オブジェクトを直接指定した例ですが、これはうまくいきません。実行してみると、以下のようなレスポンスが返ります。HTTP リクエストボディが [object Object]
という文字列になってしまっています。
{
"headers": {
"Content-Type": "text/plain;charset=UTF-8"
},
"body": "[object Object]",
"post": [],
"get": [],
"files": []
}
body
パラメータには、文字列またはサポートされている適切なデータ型を与える必要があります。
application/json
な値を与えてみる
(function() {
'use strict';
async function doFetch() {
const url = 'https://params.lavoscore.org/';
const params = {
name: 'Alice',
message: '<xmp>',
};
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
};
const obj = await fetch(url, options).then(response => response.json());
console.log(obj);
}
document.addEventListener('DOMContentLoaded', doFetch);
}());
options
に headers['Content-Type']
を追加して、body
の値を文字列(JSON)とすることで、JSON データを HTTP リクエストボディに与えることができます。サーバーサイドでは以下のように値を受け取ることができます。
{
"headers": {
"Content-Type": "application/json"
},
"body": "{\"name\":\"Alice\",\"message\":\"<xmp>\"}",
"post": [],
"get": [],
"files": []
}
しかしこれでは PHP の場合、$_POST
としては値を受け取ることができませんね。
Blob として JSON データを与える
fetch
の第 2 引数 options
として headers['Content-Type']
を明記したくない場合は、次のような方法もあります。
(function() {
'use strict';
async function doFetch() {
const url = 'https://params.lavoscore.org/';
const params = {
name: 'Alice',
message: '<xmp>',
};
const options = {
method: 'POST',
body: new Blob([JSON.stringify(params)], {type: 'application/json'}),
};
const obj = await fetch(url, options).then(response => response.json());
console.log(obj);
}
document.addEventListener('DOMContentLoaded', doFetch);
}());
multipart/form-data
な FormData
を使ってみる
JavaScript には FormData
インターフェイスが存在しています。
(function() {
'use strict';
function makeDataAsFormData(obj) {
const formData = new FormData();
Object.keys(obj).forEach(key => formData.append(key, obj[key]));
return formData;
}
async function doFetch(makeDataFunc) {
const url = 'https://params.lavoscore.org/';
const params = {
name: 'Alice',
message: '<xmp>',
};
const options = {
method: 'POST',
body: makeDataAsFormData(params),
};
const obj = await fetch(url, options).then(response => response.json());
console.log(obj);
}
document.addEventListener('DOMContentLoaded', doFetch);
}());
これを介すと、次のようなレスポンスが受け取れます。
{
"headers": {
"Content-Type": "multipart/form-data; boundary=---------------------------29960240541939911991501528013"
},
"body": "",
"post": {
"name": "Alice",
"message": "<xmp>"
},
"get": [],
"files": []
}
FormData
を body
として与えると、Content-Type
は multipart/form-data
として扱われます。この方法ならサーバーサイドで PHP の場合 $_POST
として値を処理することができます。
なお、この方法を使う場合、明示的に fetch
の第 2 引数として headers['Content-Type']
を指定してはいけません。指定した場合、boundary
が設定されず、サーバーサイドで値を適切に受け取ることができなくなってしまいます。
関連記事
application/x-www-form-urlencoded
な URLSearchParams
を使う
(function() {
'use strict';
async function doFetch(makeDataFunc) {
const url = 'https://params.lavoscore.org/';
const params = {
name: 'Alice',
message: '<xmp>',
};
const options = {
method: 'POST',
body: new URLSearchParams(params),
};
const obj = await fetch(url, options).then(response => response.json());
console.log(obj);
}
document.addEventListener('DOMContentLoaded', doFetch);
}());
URLSearchParams
を body
として与えると、Content-Type
は application/x-www-form-urlencoded
として扱われます。この方法ならサーバーサイドで PHP の場合 $_POST
として値を処理することができます。
これを介すと、次のようなレスポンスが受け取れます。
{
"headers": {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
},
"body": "name=Alice&message=%3Cxmp%3E",
"post": {
"name": "Alice",
"message": "<xmp>"
},
"get": [],
"files": []
}
このレスポンスは、JavaScript を介さずに HTML Form をサブミットしたときと同等のレスポンスになっています。つまり、JavaScript で application/x-www-form-urlencoded
な POST リクエストを送りたい場合は、この方法が適しています。
試してみる
See the Pen POST Request by JavaScript by arcxor (@arcxor) on CodePen.
さいごに
HTML Form 関連のお話でした。JavaScript で連想配列(オブジェクト)のパラメータを POST(または GET)として fetch
でリクエストするには、実は少し工夫が必要だったのです。自分で FormData
なり URLSearchParams
を用意して body
に値を与えなければなりません。
というわけで、既に 12 月 21 日ですが、この記事は 2023 年の MDN 翻訳 Advent Calendar の 20 日の記事ということで公開しておこうと思います。21 日の記事はどうするんでしょうねぇ。
おわり。