報價表上的客戶編號明明看得到,送到後端卻沒有這個欄位。把驗證規則改了幾次,仍然收到「編號必填」。這時先看表單怎麼組成送出的資料:欄位若設成 disabled,瀏覽器會把它排除;畫面顯示的值,不一定在請求裡。
若你的意思是「讓人看得到,不能直接修改,但仍要隨表單送出」,支援唯讀的文字欄位可以用 readonly。兩個屬性外觀可能相近,處理資料的方式卻不同。先確認請求內容,才知道該修畫面、送出程式,還是後端。
先用一張小表單看出差別
以下例子有四個文字欄位:一般欄位、停用欄位、唯讀欄位,以及沒有名稱的欄位。把它放在本機 HTML 檔,用瀏覽器開啟,再從開發者工具的 Console 執行後面的 JavaScript。這個例子只建立資料,不會傳到伺服器。
<form id="quote">
<input name="customer" value="Ada">
<input name="account" value="A17" disabled>
<input name="reference" value="R42" readonly>
<input value="visible-but-unnamed">
<input type="hidden" name="plan" value="basic">
</form>
const form = document.querySelector('#quote');
const data = new FormData(form);
console.log([...data.entries()]);
結果包含 customer= Ada、reference= R42 與 plan= basic,沒有 account,也沒有最後那個未命名文字欄位。停用欄位即使有值、有名稱,仍被略過;唯讀文字欄位則照常加入。沒有 name 的控制項沒有送出鍵名,也不能靠 id 補上。
這裡的空格只是文字展示方式,實際值分別是 Ada、R42、basic。不要用肉眼看輸入框是否有字來判斷資料是否送出,直接查看 FormData 的 entries,或在 Network 面板查看真正的請求,會更可靠。
disabled表示暫時不能使用,不只是不能修改
停用的原生表單控制項通常無法取得焦點,也不參與表單約束驗證。例如停用且帶有 required 的欄位,不會因為空值而擋住表單。這符合「目前不適用這個選項」的意思,卻不適合拿來鎖住仍需要送出的訂單編號。
停用也可能來自外層的 fieldset disabled。如果單看 input 沒有發現屬性,往上檢查父層;停用 fieldset 內的控制項通常一起受影響,其第一個 legend 子元素內的控制項有規格例外。不要只靠 CSS 灰色判斷,也不要把 disabled="false" 當成啟用:這是布林屬性,只要屬性存在,就代表停用。要恢復使用,移除屬性或把 DOM 的 disabled 屬性設成 false。
const account = form.elements.namedItem('account');
account.disabled = false;
console.log(new FormData(form).get('account'));
// A17
資料是在建立 FormData 的那一刻取樣。先建立 data,再啟用欄位,不會自動把新欄位補進既有物件;需要重新建立,或明確使用 append、set 修改資料。這在「按送出後鎖住整張表單」的程式特別容易出錯:若先停用所有欄位,才收集資料,內容可能突然變少。
readonly適合文字值,不能套到每個選項
唯讀文字欄位仍可取得焦點,值可以隨表單送出,但使用者不能透過一般輸入操作修改。它適合參考編號、從其他選項算出的文字結果,或需要讓人選取複製的值。唯讀控制項同樣不參與約束驗證,因此不能期待 readonly 加 required 幫你檢查資料完整。
readonly並非所有 input 類型都支援,也不適用於 select、checkbox 或 radio。把 readonly 加在方案下拉選單上,不能假設它因此被鎖住。需要顯示固定方案時,可以用普通文字展示,另帶有名稱的 hidden 欄位承載資料;或者保留正常控制項,依實際需求限制操作。設計時要清楚分開「顯示」與「提交」,不要拿一個無效屬性充當規則。
hidden雖然不在畫面上,仍是使用者端資料。它適合提交輔助值,並不能保護價格、折扣或權限。若畫面已有同名的其他欄位,再加 hidden 會產生多個同名值;後端採第一個、最後一個或陣列,取決於框架與解析方式。先定義一個欄位的來源,避免重複名稱讓結果更難查。
修好提交,仍要由後端確認可信的值
readonly只限制一般畫面操作。開發者工具可以改值,程式也可以自行建立請求;disabled欄位同樣可以被重新啟用。伺服器收到客戶編號後,仍要核對登入者能否使用該編號。總價應由伺服器依商品與折扣規則計算,不能因為欄位看起來鎖住就照單全收。
若編號本來就能從登入身分或訂單紀錄取得,可以考慮不讓前端提交這個值,由後端自行查出。表單只提交真正需要使用者選擇的內容,會少一個不一致來源。這是資料設計的選擇,並不要求每張表單都改成同一種寫法。
用實際送出路徑做最後確認
假設你的報價表正在載入客戶資料,期間停用整張表單是合理的:此時資料尚未準備好,使用者也不應送出。載入完成後,需要送出的控制項就應恢復正常,固定的客戶名稱可改為唯讀文字。進入送出中狀態時,先保存要傳的 FormData,再停用送出按鈕,通常比先停用全部欄位更容易掌握。若你確實需要鎖住整張表單,也要確認資料收集已完成,以及失敗後會恢復哪些欄位。
表單失敗後不要一律把所有 disabled 移除。有些欄位本來就因為目前方案不適用而停用,例如未選到企業方案時的公司統編。恢復時應回到原本的業務狀態,而非把所有選項都打開。同樣地,成功之後要清空的是使用者輸入內容,還是保留供下一次填寫的客戶編號,也應先決定,避免畫面和下一次送出的值不同步。
排查時可以準備一張簡短欄位清單,逐項記下畫面用途、name、來源、是否需要提交及伺服器怎麼確認。客戶名稱只是展示時,根本不需要提交;方案由使用者選擇時,需要提交但仍要核對可用方案;價格由後端決定時,應以後端計算為準。這樣就能針對每個欄位選擇文字、唯讀輸入、正常控制項或 hidden,而不是把整張表單全部改成同一個屬性。
本文的本機 Chromium 案例驗證了原生 FormData 的欄位收集,不代表每個框架都使用相同路徑。有些框架從自己的 state 組 JSON,即使 DOM input 停用,仍可能把 state 裡的值送出。遇到差異時,先找出送出的物件從哪裡來,再對照 Network 的 request payload,不要把瀏覽器表單規則套到所有自訂 API。
最後檢查一般狀態、停用狀態和送出中的狀態,各自有哪些鍵名。再測缺少值、同名欄位、按鈕送出與鍵盤 Enter 是否走同一段程式。用 new FormData(form) 不會自動加入某個送出按鈕的 name/value;若業務依按鈕判斷動作,需處理實際 submitter。看到欄位缺失時,這幾個入口比反覆調整後端必填訊息更值得先查。
參考資料
HTML Standard:readonly屬性
MDN:disabled
MDN:readonly
MDN:FormData建構子與submitter
MDN:使用FormData物件

評論0