FormData とは何か
FormData は、フォームの入力値を「送信用のペア(name → value)」としてまとめるための標準APIです。ファイルも含めて簡単に扱え、fetch などの非同期送信でそのまま使えます。ここが重要です:FormData は“現在の入力状態”を収集し、checkbox 未チェックはキーなし、multiple な select は getAll で複数値を取得する――この挙動を理解しておくとデータの取り扱いが正確になります。
基本の使い方(収集・参照・送信)
現在のフォーム値を収集する
<form id="f">
<input name="email" value="a@example.com">
<input name="agree" type="checkbox" value="yes">
<select name="tag" multiple>
<option value="js" selected>JavaScript</option>
<option value="ui" selected>UI/UX</option>
</select>
<button>送信</button>
</form>
<script>
const fd = new FormData(document.getElementById("f"));
console.log(fd.get("email")); // "a@example.com"
console.log(fd.has("agree")); // checkbox 未チェックなら false(キーなし)
console.log(fd.getAll("tag")); // ["js", "ui"](multiple は getAll)
</script>
HTMLここが重要です:get は単一値、getAll は複数値。チェックの有無や未選択の判定は has/get の戻りで行い、空文字と“キーなし”を混同しないのがコツです。
fetch でそのまま送る(multipart/form-data)
<script>
async function submitForm(form) {
const fd = new FormData(form);
const res = await fetch(form.action || "/api/submit", {
method: form.method || "POST",
body: fd, // Content-Type はブラウザが自動設定(boundary 含む)
});
if (res.ok) alert("送信に成功しました");
else alert("送信に失敗しました");
}
document.getElementById("f").addEventListener("submit", (e) => {
e.preventDefault();
submitForm(e.target);
});
</script>
HTMLここが重要です:body に FormData を渡すと、ブラウザが適切な multipart/form-data ヘッダーを自動で付けます。自分で Content-Type を設定しないのが正解です。
ファイル送信(File / Blob の扱い)
file input から送る
<form id="avatarForm">
<input name="avatar" type="file" accept="image/*">
<button>アップロード</button>
</form>
<script>
avatarForm.addEventListener("submit", async (e) => {
e.preventDefault();
const fd = new FormData(avatarForm);
const res = await fetch("/upload", { method: "POST", body: fd });
alert(res.ok ? "アップロード成功" : "アップロード失敗");
});
</script>
HTMLここが重要です:files[0] を自分で触らずとも、FormData は file input の選択を自動で収集します。複数ファイルなら input に multiple を付ければ複数送信されます。
手動で Blob/File を追加する
<script>
const fd = new FormData();
const blob = new Blob(["hello"], { type: "text/plain" });
fd.append("note", blob, "note.txt"); // filename を付けるとファイル扱い
</script>
HTMLここが重要です:append は値だけでなく Blob/File も追加できます。filename を指定するとサーバー側でファイル名として受け取れます。
値の追加・更新・削除(append / set / delete)
追加(同じキーを複数持たせる)
<script>
const fd = new FormData();
fd.append("tag", "js");
fd.append("tag", "ui");
console.log(fd.getAll("tag")); // ["js", "ui"]
</script>
HTMLここが重要です:append は“同名キーを増やす”。複数選択や複数ファイルの表現に向いています。
上書き(常に一つにしたい)
<script>
const fd = new FormData();
fd.set("page", "1"); // 既存があれば置換、無ければ追加
fd.set("page", "2"); // "2" に上書き(get は "2")
</script>
HTMLここが重要です:set は“そのキーを一つだけ”に保ちたい場合の最短ルートです。
削除(キーごと消す)
<script>
fd.delete("tag"); // "tag" のエントリをすべて削除
</script>
HTMLここが重要です:delete はキー全削除。再構成前に一度消してから append/set する設計が安全です。
JSON が必要なAPIへの対応(変換・設計の注意点)
FormData をオブジェクトへ変換して JSON 送信
<script>
const fd = new FormData(document.getElementById("f"));
const obj = Object.fromEntries(fd.entries()); // キーの先頭値だけを取り込む
const res = await fetch("/api/json", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(obj),
});
</script>
HTMLここが重要です:Object.fromEntries は“各キーの先頭の値のみ”変換します。multiple の値や同名キーが複数ある場合は、必要に応じて getAll で配列化してから組み立てます。
checkbox 未チェックの扱い(キーが無い問題)
<script>
const fd = new FormData(f);
const agreed = fd.has("agree"); // true/false を明示のブールに変換
const payload = { ...Object.fromEntries(fd), agree: agreed };
</script>
HTMLここが重要です:未チェックはキーがありません。サーバーがブールを期待するなら、has() で明示的に true/false を付与する設計が堅牢です。
バリデーションと送信の流れ(checkValidity と submit 制御)
送信前に組み込み検証を通す
<script>
const form = document.getElementById("f");
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (!form.checkValidity()) {
form.reportValidity(); // バブル表示で案内
return;
}
const fd = new FormData(form);
const res = await fetch(form.action, { method: form.method, body: fd });
if (res.ok) form.reset();
});
</script>
HTMLここが重要です:checkValidity → reportValidity の流れで“正しい値だけ送る”。失敗時は送信を止め、成功時に reset で初期化するだけで UX が安定します。
実践例(依存セレクトと一括送信、進捗と二重送信防止)
依存セレクトの値を含めて送る
<form id="place">
<select name="pref" required>
<option value="">都道府県</option>
<option value="tokyo">東京都</option>
</select>
<select name="ward" required></select>
<button>送信</button>
</form>
<script>
const WARDS = { tokyo: [{ text:"江東区", value:"koto" }, { text:"渋谷区", value:"shibuya" }] };
const pref = place.pref, ward = place.ward;
function fillWard(list) {
ward.length = 0;
for (const i of list) ward.add(new Option(i.text, i.value));
}
pref.addEventListener("change", () => fillWard(WARDS[pref.value] || []));
fillWard([]);
place.addEventListener("submit", async (e) => {
e.preventDefault();
if (!place.checkValidity()) return place.reportValidity();
const fd = new FormData(place);
const res = await fetch("/api/place", { method:"POST", body: fd });
alert(res.ok ? "送信成功" : "送信失敗");
});
</script>
HTMLここが重要です:選択肢を動的に構築しても、FormData は“その時点の選択”を正しく収集します。送信前に必須チェックを通しておけば、未選択のまま送る事故を防げます。
進行中表示と二重送信防止
<script>
place.addEventListener("submit", async (e) => {
e.preventDefault();
if (!place.checkValidity()) return place.reportValidity();
const btn = e.submitter; btn.disabled = true; btn.textContent = "送信中…";
try {
const fd = new FormData(place);
const res = await fetch("/api/place", { method:"POST", body: fd });
alert(res.ok ? "完了" : "失敗");
} finally {
btn.disabled = false; btn.textContent = "送信";
}
});
</script>
HTMLここが重要です:送信中はボタン無効化と文言変更で状況を明示。連打による二重送信を防ぎ、体験を落ち着かせます。
よくある落とし穴と回避策
checkbox 未チェックは“キーがない”ため、単純な Object.fromEntries だけだと抜け落ちます。必要なら has() でブール化して補完します。multiple な select を get で読むと先頭しか取れないので、必ず getAll を使います。FormData を fetch に渡すときは Content-Type を自分で設定しないこと。ブラウザが boundary を含めて正しく付与します。ファイルを JSON に含めることはできないため、ファイル送信は FormData のままにするか、別のアップロードAPIを用意します。送信前の検証を省くと、空や不正値で API を叩いてしまい、サーバー側の負荷とユーザーのやり直しが増えます。checkValidity/reportValidity の流れを必ず挟んでください。
まとめ
FormData は「フォームの現在値を送信用のマップへ直列化する」ための最短手段です。get と getAll の違い、checkbox 未チェックの“キーなし”、ファイルをそのまま送れる点を押さえれば、fetch への非同期送信がシンプルに書けます。追加・更新・削除は append/set/delete、JSON が必要なら Object.fromEntries と getAll で丁寧に組み立てる。送信前は checkValidity/reportValidity で正しい値だけ送る――この基本を徹底すれば、初心者でも短いコードで堅実なフォーム送信が実装できます。
