1478 字
7 分鐘

FormData 欄位不見了?檢查 disabled、name 與 submitter

按下儲存後,程式先把整份表單 disabled,避免連點,再呼叫 new FormData(form);結果送到後端的欄位比畫面少。FormData 不是把所有可見 input 都收進去:先確認控制項的 name、停用狀態與選取狀態,並在鎖定欄位之前建立資料快照。

有「儲存草稿」與「發布」兩顆按鈕的表單,還要把實際觸發送出的 event.submitter 傳給建構子,才能保留那顆按鈕的名稱和值。資料本身有沒有收齊,應先在請求送出前確認,再往後端 parser 排錯。

從收集條件找缺少的欄位#

WHATWG 的表單 entry list 演算法定義哪些控制項會被收集。幾個容易誤判的情況如下:

表單狀態FormData 的結果要檢查什麼
只有 id,沒有 name不會得到預期名稱的欄位id 用於 DOM 識別,資料 key 要用 name
input disabled不收集該控制項是否在建立快照前鎖定介面
支援 readonly 的 input仍可收集readonly 限制編輯,不等同 disabled
checkbox 未勾選不會自動送出 false後端需明確定義缺少欄位的語意
多個相同 name可以保留多筆值使用 getAll,不要只讀 get
submit button依指定的 submitter 收集確認當次點按的是哪個按鈕

disabled 的 <fieldset> 也會影響其子控制項,首個 <legend> 的後代有規格例外;排錯時要檢查祖先,不只看 input 自己有沒有 disabled attribute。相反地,用 CSS 隱藏不等於停用,type="hidden" 也可以提交資料。畫面是否看得見並不是可靠的收集條件。

readonly 不是每種控制項都支援,不能把它當成 select 或 checkbox 的通用替代方法。若欄位必須禁止操作但仍要提交,請先設計資料來源:在鎖定前建立快照,或由可信的應用狀態加入該值。伺服器仍須驗證欄位,不應因為前端 readonly 就信任它。

可重現的表單:先看快照,再看是哪顆按鈕#

以下存成 formdata-check.html,使用目標瀏覽器開啟。它不會發出網路請求;按「儲存」、「發布」和最後一個檢查按鈕,比較輸出即可。

<!doctype html>
<html lang="zh-Hant">
<meta charset="utf-8">
<title>FormData 欄位檢查</title>
<form id="profile">
<label>名稱 <input name="displayName" value="Zero"></label>
<label>停用 <input name="disabledField" value="missing" disabled></label>
<label>唯讀 <input name="readOnlyField" value="included" readonly></label>
<label>只有 id <input id="unnamed" value="missing"></label>
<label><input type="checkbox" name="newsletter" value="yes">訂閱</label>
<label><input type="checkbox" name="topics" value="css" checked>CSS</label>
<label><input type="checkbox" name="topics" value="js" checked>JavaScript</label>
<button type="submit" name="intent" value="save">儲存</button>
<button type="submit" name="intent" value="publish">發布</button>
</form>
<button type="button" id="inspect">省略 submitter 的快照</button>
<pre id="result"></pre>
<script>
const form = document.getElementById('profile');
const result = document.getElementById('result');
function show(data) {
result.textContent = JSON.stringify({
entries: [...data],
topics: data.getAll('topics'),
flattenedTopics: Object.fromEntries(data).topics,
}, null, 2);
}
form.addEventListener('submit', event => {
event.preventDefault();
show(new FormData(form, event.submitter));
});
document.getElementById('inspect').addEventListener('click', () => {
show(new FormData(form));
});
</script>
</html>

依規格,未改動範例時,disabledField、沒有 name 的 input 和未勾選的 newsletter 不會出現;readOnlyField 會被收集。點按儲存應有 intent=save,發布應有 intent=publish,省略 submitter 的快照則沒有這兩顆按鈕的 intent。

這些是供你對照的預期值。本次內容維護已檢閱範例與檢查 JavaScript 語法,沒有取得瀏覽器實測結果,也沒有測試舊瀏覽器對建構子第二個參數的相容性。正式專案請在自己的支援矩陣驗收;MDN 的 FormData 建構子頁面有第二個參數與例外條件的說明。

多值欄位別直接轉成普通物件#

上例 topics 有 css、js 兩筆;data.getAll('topics') 保留兩者,Object.fromEntries(data) 則讓後面的同名 key 覆蓋前面。因此把整份 FormData 轉成 JSON 時,看到少了一個選項,問題可能出在轉換,而非表單收集。

要送 JSON,依欄位 schema 明確建構:

const data = new FormData(form, submitter);
const payload = {
displayName: data.get('displayName'),
topics: data.getAll('topics'),
newsletter: data.has('newsletter'),
intent: data.get('intent'),
};

這段是 schema 轉換示意,form、submitter 取自你的 submit handler;只有當所有選項都已載入、欄位確實可操作時,缺少 newsletter 才能解讀為未勾選。若控制項被停用或根本沒有渲染,缺少 key 也可能代表「不修改」;PATCH API 尤其應先把語意定義好。檔案則應另外處理,不能假設把 File 放進 JSON 就會上傳檔案內容。

先建立快照,再停用送出按鈕#

下面是接到產品 API 的整合示意,需要替換 /api/profile 及回應處理。它只停用 submit button,並且保留原本就停用的狀態,避免結束後把不能操作的按鈕一律打開:

let sending = false;
form.addEventListener('submit', async event => {
event.preventDefault();
if (sending) return;
const data = new FormData(form, event.submitter);
const buttons = [...form.querySelectorAll('button[type="submit"]')];
const states = buttons.map(button => button.disabled);
sending = true;
buttons.forEach(button => { button.disabled = true; });
try {
const response = await fetch('/api/profile', {
method: 'POST',
body: data,
});
if (!response.ok) throw new Error('HTTP ' + response.status);
// 依專案回應格式處理成功狀態。
} catch (error) {
console.error(error);
// 正式產品應在表單顯示可理解的錯誤與重試方式。
} finally {
buttons.forEach((button, index) => {
button.disabled = states[index];
});
sending = false;
}
});

範例限定在表單內、明確帶 type="submit" 的 button;外部關聯按鈕、input[type="submit"]、元件卸載與伺服器冪等性需要依產品補上。建立 FormData 只取得當下快照,後續 input 改值不會自動更新那份資料;若產品允許送出期間繼續編輯,要讓成功提示清楚指向哪次快照。

使用 body: data 傳送 FormData 時,MDN 明確提醒不要手動設定 multipart Content-Type,讓瀏覽器產生與本文相符的 boundary。若仍少欄位,依序比較建立快照時的 entries、Network 中的 Form Data,再比較後端解析結果。這樣能定位資料在哪一層消失。

若後端返回的本文又無法解析成 JSON,可接著看 fetch JSON 的 Unexpected token 排錯;先確認送出資料正確,再處理回應格式,兩邊不要共用一個模糊的「表單壞了」錯誤。

參考資料:

WHATWG HTML:Constructing the entry list

MDN:FormData()

MDN:Using FormData Objects

FormData 欄位不見了?檢查 disabled、name 與 submitter
https://laplusda.com/posts/javascript-formdata-missing-fields/
作者
Zero
發佈於
2026-10-07
許可協議
CC BY-NC-SA 4.0