1. ホーム
  2. JavaScript

【JavaScript】FormData の使い方|フォームの入力値をまとめて取得・送信する方法

Share

フォームの入力値を JavaScript で扱うとき、入力欄ひとつずつに getElementById() を書いて value を読む……という書き方をしていると、項目が増えるほどコードが長くなっていきます。FormData を使えば、フォーム要素を渡すだけで入力値をまとめて取り出せて、そのまま fetch() の送信データにもできます。この記事では new FormData(form) での取り出し方、get / getAll / append などのメソッド、オブジェクトへの変換、ファイルを含む送信までを解説します。

FormData とは何か

FormData は、「名前」と「値」の組を並べて持つための入れ物です。フォームが送信されるときにブラウザーが内部で組み立てているデータと同じ形を、JavaScript から作ったり読んだりできるようにしたものだと考えてください。

ポイントは2つあります。ひとつは、new FormData(フォーム要素) と書くだけで、そのフォームの中にある入力欄の値がすべて自動で詰め込まれること。もうひとつは、作った FormDatafetch()body にそのまま渡せることです。値を集める手間と、送信できる形に変換する手間の両方が省けます。

また、名前と値の組は同じ名前を複数持てます。チェックボックスのように1つの名前で複数の値を選べる入力があるため、この性質は後述する getAll() と合わせて重要になります。

基本の使い方|フォームから値をまとめて取り出す

まずは典型的なフォームを用意します。値を取り出す対象になるのは、name 属性が付いた入力欄です。

index.html
<form id="signup">
  <input type="text" name="name" value="山田太郎">
  <input type="email" name="email" value="taro@example.com">
  <button type="submit">送信</button>
</form>

このフォームの submit イベントで FormData を作ると、入力値が名前付きで入った状態になります。event.preventDefault() は、ページが再読み込みされる既定の送信動作を止めるための記述です。

main.js
const form = document.getElementById('signup');

form.addEventListener('submit', (event) => {
  // 既定の送信(ページ遷移)を止める
  event.preventDefault();

  // フォーム要素を渡すだけで、入力値がまとめて入る
  const formData = new FormData(form);

  console.log(formData.get('name'));  // => '山田太郎'
  console.log(formData.get('email')); // => 'taro@example.com'
});

注意したいのは、console.log(formData) と書いても中身がそのまま並んで見えるわけではないことです。FormData のデータは通常のプロパティとして持たれていないため、中身を確認したいときは次のように反復して取り出します。

main.js
// entries() で「名前と値」の組を1つずつ取り出す
for (const [key, value] of formData.entries()) {
  console.log(key, value);
}

// FormData 自体が反復可能なので entries() は省略してもよい
for (const [key, value] of formData) {
  console.log(key, value);
}

下のデモで実際の動きを確認してみてください。送信ボタンを押すと、フォームの内容が FormData としてどう取り出されるかが表示されます。チェックボックスの選択を変えたり、テキストを書き換えたりすると結果が変わります。

<form id="signup">
  <p>
    <label for="name">お名前</label>
    <input type="text" id="name" name="name" value="山田太郎">
  </p>
  <p>
    <label for="email">メールアドレス</label>
    <input type="email" id="email" name="email" value="taro@example.com">
  </p>
  <p>
    <label for="plan">プラン</label>
    <select id="plan" name="plan">
      <option value="free">無料</option>
      <option value="pro" selected>プロ</option>
    </select>
  </p>
  <fieldset>
    <legend>興味のある分野(複数可)</legend>
    <label><input type="checkbox" name="interest" value="html" checked> HTML</label>
    <label><input type="checkbox" name="interest" value="css" checked> CSS</label>
    <label><input type="checkbox" name="interest" value="js"> JavaScript</label>
  </fieldset>
  <p>
    <!-- name がないので FormData には入らない -->
    <input type="text" placeholder="name 属性なし(入りません)">
  </p>
  <button type="submit">入力値を取り出す</button>
</form>

<h3>FormData の中身</h3>
<pre id="output">送信ボタンを押してください</pre>
body {
  font-family: sans-serif;
  margin: 16px;
  color: #333;
}

form {
  border: 1px solid #ddd;
  border-radius: 6px;
  padding: 12px 16px;
}

label {
  display: inline-block;
  margin-right: 8px;
  font-size: 14px;
}

input[type="text"],
input[type="email"],
select {
  padding: 6px 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 14px;
}

fieldset {
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 8px 12px;
}

legend {
  font-size: 13px;
  color: #666;
}

button {
  margin-top: 8px;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background: #007bff;
  color: #fff;
  cursor: pointer;
}

h3 {
  margin: 20px 0 8px;
  font-size: 14px;
  color: #007bff;
}

pre {
  background: #f6f8fa;
  border: 1px solid #e1e4e8;
  border-radius: 4px;
  padding: 12px;
  font-size: 13px;
  white-space: pre-wrap;
  word-break: break-all;
}
const form = document.getElementById('signup');
const output = document.getElementById('output');

form.addEventListener('submit', (event) => {
  // ページ遷移を止めて JavaScript で値を扱う
  event.preventDefault();

  // フォーム要素を渡すだけで、入力値がまとめて入る
  const formData = new FormData(form);

  const lines = [];

  // 1件ずつ取り出す(同じ name が複数あると、その数だけ出てくる)
  for (const [key, value] of formData.entries()) {
    lines.push(key + ' = ' + value);
  }

  // 単一の値は get()、同じ name が複数あるものは getAll()
  lines.push('');
  lines.push("get('name')        => " + formData.get('name'));
  lines.push("get('interest')    => " + formData.get('interest'));
  lines.push("getAll('interest') => " + JSON.stringify(formData.getAll('interest')));
  lines.push("get('nothing')     => " + formData.get('nothing'));

  // まとめてオブジェクトにする(同じ name は最後の値だけ残る)
  lines.push('');
  lines.push('Object.fromEntries => ' + JSON.stringify(Object.fromEntries(formData)));

  output.textContent = lines.join('\n');
});
Preview

結果を見ると、チェックを入れた interest がその数だけ並んでいること、name 属性のない入力欄が結果に含まれていないこと、存在しない名前を get() で取ると null が返ることが確認できます。

FormData の主なメソッド

FormData には、中身を読み書きするためのメソッドが用意されています。よく使うものは次のとおりです。

メソッド説明
get(name)その名前の最初の値を返す。無ければ null
getAll(name)その名前の値をすべて配列で返す。無ければ空配列
append(name, value)値を追加する。同じ名前が既にあっても消さずに増やす
set(name, value)その名前の値をすべて消し、1つの値に置き換える
has(name)その名前の値があるかを true / false で返す
delete(name)その名前の値をすべて削除する
entries()[名前, 値] の組を順に返すイテレーターを返す
keys()名前だけを順に返すイテレーターを返す
values()値だけを順に返すイテレーターを返す
forEach(callback)すべての組に対してコールバックを実行する

間違えやすいのが append()set() の違いです。append()追加なので同じ名前が並んで増えていくのに対し、set()置き換えなので既存の値が消えて1つだけになります。下のデモでボタンを押しながら、中身がどう変わるかを見比べてみてください。

<div class="buttons">
  <button type="button" data-action="append">append('tag', 'css')</button>
  <button type="button" data-action="set">set('tag', 'php')</button>
  <button type="button" data-action="has">has('tag')</button>
  <button type="button" data-action="delete">delete('tag')</button>
  <button type="button" data-action="reset">最初に戻す</button>
</div>

<h3>いまの FormData</h3>
<pre id="output"></pre>

<h3>実行したこと</h3>
<pre id="log">ボタンを押してください</pre>
body {
  font-family: sans-serif;
  margin: 16px;
  color: #333;
}

.buttons {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

button {
  padding: 6px 12px;
  border: 1px solid #007bff;
  border-radius: 4px;
  background: #fff;
  color: #007bff;
  font-family: inherit;
  cursor: pointer;
}

h3 {
  margin: 20px 0 8px;
  font-size: 14px;
  color: #007bff;
}

pre {
  background: #f6f8fa;
  border: 1px solid #e1e4e8;
  border-radius: 4px;
  padding: 12px;
  font-size: 13px;
  white-space: pre-wrap;
  word-break: break-all;
  min-height: 1.5em;
}
const output = document.getElementById('output');
const log = document.getElementById('log');

let formData;

function createFormData() {
  // フォームがなくても、空の FormData を作って自分で詰められる
  const data = new FormData();
  data.append('title', 'FormData の使い方');
  data.append('tag', 'javascript');
  return data;
}

function render() {
  const lines = [];
  for (const [key, value] of formData) {
    lines.push(key + ' = ' + value);
  }
  output.textContent = lines.length ? lines.join('\n') : '(空です)';
}

document.querySelectorAll('[data-action]').forEach((button) => {
  button.addEventListener('click', () => {
    switch (button.dataset.action) {
      case 'append':
        // 同じ name をもう1つ増やす(既存の値は残る)
        formData.append('tag', 'css');
        log.textContent = "append('tag', 'css') → tag が2つになります";
        break;
      case 'set':
        // 同じ name をすべて消して、1つの値に置き換える
        formData.set('tag', 'php');
        log.textContent = "set('tag', 'php') → tag はこの1つだけになります";
        break;
      case 'has':
        log.textContent = "has('tag') => " + formData.has('tag');
        break;
      case 'delete':
        // その name の値をすべて削除する
        formData.delete('tag');
        log.textContent = "delete('tag') → tag が全部消えます";
        break;
      case 'reset':
        formData = createFormData();
        log.textContent = '最初の状態に戻しました';
        break;
    }
    render();
  });
});

formData = createFormData();
render();
Preview

このデモのように、new FormData() は引数なしでも呼べます。フォームが存在しない場面でも、空の FormData を作って append() で値を詰めていけば、送信用のデータを自分で組み立てられます。

Object.fromEntries でオブジェクトに変換する

取り出した値をまとめて扱いたいときは、Object.fromEntries()FormData を渡すと、名前をキーにしたオブジェクトになります。FormData[名前, 値] の組を返す反復可能オブジェクトなので、そのまま渡せます。

main.js
const formData = new FormData(form);
const values = Object.fromEntries(formData);

console.log(values);
// => { name: '山田太郎', email: 'taro@example.com', plan: 'pro' }

// あとは普通のオブジェクトとして扱える
console.log(values.name); // => '山田太郎'

ただし、オブジェクトは1つのキーに1つの値しか持てません。そのため同じ名前が複数あると、最後の値だけが残って前の値は失われます。チェックボックスのように複数選べる項目がある場合は、その項目だけ getAll() で取り直してから組み立てます。

main.js
const formData = new FormData(form);

const values = {
  ...Object.fromEntries(formData),
  // 複数選択できる項目は配列として持ち直す
  interest: formData.getAll('interest'),
};

console.log(values);
// => { name: '山田太郎', plan: 'pro', interest: ['html', 'css'] }

fetch でそのまま送信する

FormData の便利さがいちばん効いてくるのが送信です。fetch()bodyFormData を渡すと、ブラウザーが multipart/form-data 形式に組み立てて送ってくれます。

main.js
form.addEventListener('submit', async (event) => {
  event.preventDefault();

  const formData = new FormData(form);

  const response = await fetch('/api/signup', {
    method: 'POST',
    body: formData, // Content-Type は指定しない
  });

  const result = await response.json();
  console.log(result);
});

Content-Type を自分で書いてはいけない理由

ここで headers'Content-Type': 'multipart/form-data' を書き足したくなりますが、書いてはいけませんmultipart/form-data は複数の値を区切り文字で区切って並べる形式で、その区切り文字(バウンダリー)は送信のたびにブラウザーが自動生成し、Content-Type ヘッダーの中に boundary=... として書き添えます。

実際に送信されるヘッダーの例
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABCDEF1234567890

自分でヘッダーを指定すると、この boundary のない Content-Type で上書きされてしまいます。受け取る側は値の区切り位置が分からなくなり、1つも取り出せません。bodyFormData を渡したときはヘッダーを指定しないのが正解で、そうすればブラウザーが正しい値を付けてくれます。

JSON で送りたいときは変換する

送信先の API が JSON を期待している場合は、FormData をそのまま渡さず、前述の Object.fromEntries() でオブジェクトにしてから JSON.stringify() します。このときは自分で Content-Type を指定します。

main.js
const formData = new FormData(form);

await fetch('/api/signup', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(Object.fromEntries(formData)),
});

ファイルを含まないフォームなら、URLSearchParams を経由して、通常のフォーム送信と同じ application/x-www-form-urlencoded 形式で送ることもできます。この場合も Content-Type はブラウザーが付けてくれます。

main.js
// 値がすべて文字列のとき(ファイルを含まないとき)に使える
const params = new URLSearchParams(new FormData(form));

await fetch('/api/signup', {
  method: 'POST',
  body: params,
});

ファイルを送信する

multipart/form-data はファイルを含められる形式なので、<input type="file"> があるフォームでも書き方は変わりません。new FormData(form) に選択されたファイルが入り、fetch()body に渡せばそのまま送信できます。

index.html
<form id="upload">
  <input type="text" name="title">
  <input type="file" name="avatar">
  <input type="file" name="photos" multiple>
  <button type="submit">アップロード</button>
</form>

取り出した値は文字列ではなく File オブジェクトになります。File にはファイル名やサイズ、種類が入っているので、送信前にチェックすることもできます。multiple を付けた入力は複数のファイルを持てるので、getAll() で受け取ります。

main.js
const formData = new FormData(document.getElementById('upload'));

// 1つだけの file 入力は get() でよい(File オブジェクトが返る)
const avatar = formData.get('avatar');
console.log(avatar.name); // => 'icon.png'
console.log(avatar.size); // => 12345(バイト数)
console.log(avatar.type); // => 'image/png'

// multiple の file 入力は getAll() で配列として受け取る
const photos = formData.getAll('photos');
photos.forEach((file) => {
  console.log(file.name);
});

すでに用意した FileBlob を後から足すこともできます。その場合は append() の第3引数でファイル名を指定できます。

main.js
const blob = new Blob(['メモの内容'], { type: 'text/plain' });

const formData = new FormData();
// 第3引数でファイル名を指定できる
formData.append('memo', blob, 'memo.txt');

PHP で受け取る場合、テキストの値は $_POST、ファイルは $_FILES に入ります。フォームを普通に送信したときとまったく同じ形式なので、サーバー側の処理を書き換える必要はありません。

期待した値が入っていないときに確認すること

FormData で「値が取れない」「一部しか入っていない」というときは、原因がだいたい決まっています。順に確認していきましょう。

name 属性のない入力は含まれない

FormData は「名前と値の組」を持つ入れ物なので、名前のない入力欄は最初から対象外です。id は付けたけれど name を書き忘れた、というのがいちばん多いパターンです。id は JavaScript や <label> から要素を指すためのもので、送信されるデータの名前になるのは name のほうです。

index.html
<!-- NG: name がないので FormData に入らない -->
<input type="text" id="email">

<!-- OK -->
<input type="text" id="email" name="email">

また、フォームの外に置いた入力欄も対象になりません。レイアウトの都合で <form> の外に出したい場合は、入力欄に form="フォームのid" 属性を付けると、そのフォームに属する扱いになります。

チェックボックスは get() では足りない

同じ name のチェックボックスを複数置いている場合、get() で返るのは最初の1つだけです。すべて欲しいときは getAll() を使います。「チェックを3つ入れたのに1つしか取れない」というときは、ここを疑ってください。

main.js
// name="interest" のチェックボックスを3つチェックした場合
console.log(formData.get('interest'));    // => 'html'(最初の1つだけ)
console.log(formData.getAll('interest')); // => ['html', 'css', 'js']

逆に、チェックが1つも入っていないときはその名前自体が存在しませんget()nullgetAll() は空配列を返します。「未選択なら空文字列が入っている」わけではないので、判定は getAll('interest').lengthhas('interest') で行います。

なお、チェックボックスやラジオボタンに value を書いていない場合、チェックされたときの値は 'on' になります。どの項目が選ばれたのかを区別できなくなるので、value は必ず指定しておきましょう。

disabled の入力は送信対象にならない

disabled が付いた入力欄は、通常のフォーム送信でも送られません。FormData も同じ規則に従うため、値が入力されていても結果には含まれません<fieldset disabled> のようにまとめて無効化している場合は、その中の入力欄がすべて対象外になります。

「編集はさせたくないが値は送りたい」という場合は、disabled ではなく readonly を使います。readonly は編集を禁止するだけで、送信対象からは外れません。ただし readonly はテキスト入力向けの属性なので、選択系の項目では <input type="hidden"> で値を別に持たせる方法をとります。

ファイル未選択でもキーは存在する

<input type="file"> は少し特殊で、ファイルを選んでいなくてもエントリー自体は作られます。このときの値は、ファイル名が空でサイズが 0 の File オブジェクトです。そのため has('avatar')true を返してしまい、選択の有無を判定できません。

main.js
const avatar = formData.get('avatar');

// NG: 未選択でも true になる
if (formData.has('avatar')) { /* ... */ }

// OK: ファイルの大きさで判定する
if (avatar && avatar.size > 0) {
  console.log('ファイルが選ばれています');
}

あとから書き換えても入力欄には反映されない

new FormData(form) で作られるのは、その時点の値をコピーした別のデータです。フォームと連動しているわけではないので、作ったあとにユーザーが入力欄を書き換えても FormData の中身は変わりませんし、set() で値を変えても画面の入力欄は変わりません。最新の値が必要なら、送信のたびに作り直してください。

まとめ

FormData は「名前と値の組」を並べて持つ入れ物で、new FormData(フォーム要素) と書くだけでフォームの入力値をまとめて取り出せます。単一の値は get()、チェックボックスのように同じ名前が複数あるものは getAll() で受け取り、append() は追加、set() は置き換えという違いを押さえておけば、たいていの場面に対応できます。オブジェクトとして扱いたいときは Object.fromEntries() が便利ですが、同じ名前が複数あると最後の値だけが残る点に注意してください。

送信するときは fetch()body に渡すだけで、ファイルを含むデータも multipart/form-data として送れます。このとき Content-Type を自分で指定すると区切り文字(boundary)が失われて受け取れなくなるので、ヘッダーは指定しないのが鉄則です。値が取れないときは、name 属性の有無、disabled になっていないか、get() ではなく getAll() が必要ではないかを順に確認してみてください。

参考ページ