5
1

HTML Form で Enter キーによるサブミットを防止する方法、JavaScript で POST する方法

Last updated at Posted at 2023-12-21

はじめに

この記事は 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="&lt;xmp&gt;"></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);
}());

bodyparams オブジェクトを直接指定した例ですが、これはうまくいきません。実行してみると、以下のようなレスポンスが返ります。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);
}());

optionsheaders['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-dataFormData を使ってみる

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": []
}

FormDatabody として与えると、Content-Typemultipart/form-data として扱われます。この方法ならサーバーサイドで PHP の場合 $_POST として値を処理することができます。

なお、この方法を使う場合、明示的に fetch の第 2 引数として headers['Content-Type'] を指定してはいけません。指定した場合、boundary が設定されず、サーバーサイドで値を適切に受け取ることができなくなってしまいます。

関連記事

application/x-www-form-urlencodedURLSearchParams を使う

(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);
}());

URLSearchParamsbody として与えると、Content-Typeapplication/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 日の記事はどうするんでしょうねぇ。

おわり。

5
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1