フォームの入力値を JavaScript で扱うとき、入力欄ひとつずつに getElementById() を書いて value を読む……という書き方をしていると、項目が増えるほどコードが長くなっていきます。FormData を使えば、フォーム要素を渡すだけで入力値をまとめて取り出せて、そのまま fetch() の送信データにもできます。この記事では new FormData(form) での取り出し方、get / getAll / append などのメソッド、オブジェクトへの変換、ファイルを含む送信までを解説します。
目次
FormData とは何か
FormData は、「名前」と「値」の組を並べて持つための入れ物です。フォームが送信されるときにブラウザーが内部で組み立てているデータと同じ形を、JavaScript から作ったり読んだりできるようにしたものだと考えてください。
ポイントは2つあります。ひとつは、new FormData(フォーム要素) と書くだけで、そのフォームの中にある入力欄の値がすべて自動で詰め込まれること。もうひとつは、作った FormData を fetch() の body にそのまま渡せることです。値を集める手間と、送信できる形に変換する手間の両方が省けます。
また、名前と値の組は同じ名前を複数持てます。チェックボックスのように1つの名前で複数の値を選べる入力があるため、この性質は後述する getAll() と合わせて重要になります。
基本の使い方|フォームから値をまとめて取り出す
まずは典型的なフォームを用意します。値を取り出す対象になるのは、name 属性が付いた入力欄です。
<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() は、ページが再読み込みされる既定の送信動作を止めるための記述です。
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 のデータは通常のプロパティとして持たれていないため、中身を確認したいときは次のように反復して取り出します。
// 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');
});
結果を見ると、チェックを入れた 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();
このデモのように、new FormData() は引数なしでも呼べます。フォームが存在しない場面でも、空の FormData を作って append() で値を詰めていけば、送信用のデータを自分で組み立てられます。
Object.fromEntries でオブジェクトに変換する
取り出した値をまとめて扱いたいときは、Object.fromEntries() に FormData を渡すと、名前をキーにしたオブジェクトになります。FormData が [名前, 値] の組を返す反復可能オブジェクトなので、そのまま渡せます。
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() で取り直してから組み立てます。
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() の body に FormData を渡すと、ブラウザーが multipart/form-data 形式に組み立てて送ってくれます。
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つも取り出せません。body に FormData を渡したときはヘッダーを指定しないのが正解で、そうすればブラウザーが正しい値を付けてくれます。
JSON で送りたいときは変換する
送信先の API が JSON を期待している場合は、FormData をそのまま渡さず、前述の Object.fromEntries() でオブジェクトにしてから JSON.stringify() します。このときは自分で Content-Type を指定します。
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 はブラウザーが付けてくれます。
// 値がすべて文字列のとき(ファイルを含まないとき)に使える
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 に渡せばそのまま送信できます。
<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() で受け取ります。
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);
});
すでに用意した File や Blob を後から足すこともできます。その場合は append() の第3引数でファイル名を指定できます。
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 のほうです。
<!-- 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つしか取れない」というときは、ここを疑ってください。
// name="interest" のチェックボックスを3つチェックした場合
console.log(formData.get('interest')); // => 'html'(最初の1つだけ)
console.log(formData.getAll('interest')); // => ['html', 'css', 'js']
逆に、チェックが1つも入っていないときはその名前自体が存在しません。get() は null、getAll() は空配列を返します。「未選択なら空文字列が入っている」わけではないので、判定は getAll('interest').length や has('interest') で行います。
なお、チェックボックスやラジオボタンに value を書いていない場合、チェックされたときの値は 'on' になります。どの項目が選ばれたのかを区別できなくなるので、value は必ず指定しておきましょう。
disabled の入力は送信対象にならない
disabled が付いた入力欄は、通常のフォーム送信でも送られません。FormData も同じ規則に従うため、値が入力されていても結果には含まれません。<fieldset disabled> のようにまとめて無効化している場合は、その中の入力欄がすべて対象外になります。
「編集はさせたくないが値は送りたい」という場合は、disabled ではなく readonly を使います。readonly は編集を禁止するだけで、送信対象からは外れません。ただし readonly はテキスト入力向けの属性なので、選択系の項目では <input type="hidden"> で値を別に持たせる方法をとります。
ファイル未選択でもキーは存在する
<input type="file"> は少し特殊で、ファイルを選んでいなくてもエントリー自体は作られます。このときの値は、ファイル名が空でサイズが 0 の File オブジェクトです。そのため has('avatar') は true を返してしまい、選択の有無を判定できません。
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() が必要ではないかを順に確認してみてください。