readonly 改成 disabled 后字段不见了:编辑、验证与提交要分开看
同一份表单,把输入框从readonly改成disabled以后,后台突然收不到字段。两者都能让普通用户暂时无法编辑,却并不表达同一种提交合同。
readonly针对支持它的文本类控件限制用户编辑,字段通常仍参与表单数据;disabled则让相应控件不参与提交,还会改变焦点与验证行为。仅凭界面看起来都是灰色,无法判断服务端会收到什么。
下面用一个故意不符合邮箱格式的只读字段,把“能否编辑”“是否参与验证”“能否采集字段”同时显示出来。它不是推荐把非法邮箱作为业务默认值,而是让验证边界可见。
按两个独立开关,观察三份结果
保存form-state.html,在现代浏览器打开。采集按钮直接构造FormData,提交按钮走原生表单约束,两条入口刻意保留区别。
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<title>只读与禁用的提交合同</title>
<style>
body { max-width: 45rem; margin: 2rem auto; padding: 1rem; font: 16px sans-serif; }
label { display: block; margin: 12px 0; }
input { font: inherit; }
input:read-only { background: #edf3f9; }
pre { white-space: pre-wrap; }
</style>
<form id="form">
<label>邮箱字段 <input id="email" name="email" type="email" value="not-an-email" readonly></label>
<label>附加说明 <input id="note" name="note" value="本次演示" disabled></label>
<button type="submit">原生提交</button>
</form>
<label><input id="readonly" type="checkbox" checked>邮箱只读</label>
<label><input id="disabled" type="checkbox" checked>说明禁用</label>
<button id="collect" type="button">直接采集 FormData</button>
<pre id="state" role="status"></pre>
<pre id="data"></pre>
<pre id="submitted"></pre>
<script>
const form = document.querySelector('#form');
const email = document.querySelector('#email');
const note = document.querySelector('#note');
function render() {
document.querySelector('#state').textContent = [
`email: readOnly=${email.readOnly}, willValidate=${email.willValidate}, checkValidity=${email.checkValidity()}`,
`note: disabled=${note.disabled}, willValidate=${note.willValidate}`
].join('\n');
document.querySelector('#data').textContent = JSON.stringify([...new FormData(form).entries()]);
}
document.querySelector('#readonly').addEventListener('change', event => {
email.readOnly = event.target.checked;
render();
});
document.querySelector('#disabled').addEventListener('change', event => {
note.disabled = event.target.checked;
render();
});
form.addEventListener('input', render);
document.querySelector('#collect').addEventListener('click', render);
form.addEventListener('submit', event => {
event.preventDefault();
document.querySelector('#submitted').textContent = '进入 submit:' + JSON.stringify([...new FormData(form)]);
});
render();
</script>
</html>
初始采集有email、没有note。只读邮箱不参与相应约束验证,即使本例值不符合邮箱格式,checkValidity也不会据此阻止提交。取消只读后,同一值需要接受邮箱格式约束,原生提交将被挡住。
直接点采集,仍然可以得到那份格式不符的email文本。FormData负责按表单数据规则收集,不会先替调用方完成整份表单验证。这是许多自定义fetch提交绕过原生约束的起点。
取消说明禁用后,note才进入采集结果。开关只影响一个条件,字段名和值都没改,因此可以排除后端参数映射导致的丢失。
只读不是普遍适用的“所有控件锁定”
readonly适用于支持它的输入类型与textarea,不能当成select、复选框或按钮的通用冻结能力。给不支持的元素加同名属性,不能期待浏览器自动阻止所有交互。
如果整份区域只是预览,应先决定是否仍要允许选择、复制与焦点访问,再选具体实现。把每个控件都设disabled,虽然方便,却同时改变字段提交;把区域设为不可交互,又是另一种范围合同。
只读控件通常仍可获得焦点并复制内容,禁用控件通常不会进入普通键盘焦点顺序。业务若希望用户能复制一个系统生成编号,readonly可能更接近需求;如果控件当前完全不可用,disabled则能表达对应状态。视觉样式还应让用户看懂原因,而不是只变浅一个颜色。
脚本能改值,后台仍要重新验证
readonly限制用户编辑,不阻止页面脚本写value。disabled也不阻止脚本调整控件或直接构造网络请求。两者都不能承担服务端授权或数据真实性判断。
如果字段是当前登录账号、价格或权限标识,后台应从可信上下文读取或验证,不应因为页面把它锁住就直接相信提交值。把禁用字段复制到hidden输入,可以让字段继续提交,却不会提高它的可信程度。
清空禁用状态还可能让旧值重新出现。字段在禁用期间是否应该保留,用户切换业务分支以后是否需要重置,应由表单状态模型决定;disabled本身不会替你清除原值,也不负责清掉接口对象里的同名属性。
本页直接从DOM构造FormData,因而可以观察原生排除规则。若前端从另一份JavaScript状态对象生成JSON,disabled字段可能照样在对象里,这条提交路径不会自动执行浏览器的表单采集合同。排查丢字段或多字段时,先确认实际数据从哪里来。
原生提交与自定义提交应采用一致判断
表单submit处理器只会在相应原生约束通过后进入,例子故意把结果写到第三块文本中。取消只读且保留非法邮箱,再点击原生提交,这块文本不应新增一次成功记录。
如果改成普通button直接fetch,需要自己在适当时点调用reportValidity或使用对应表单提交流程,并且后台再次验证。程序成功执行new FormData不是验证完成的证据,更不是保存成功的证据。
checkValidity返回布尔状态,reportValidity还涉及面向用户的验证提示,二者不能在交互上完全替代。本文在报告区使用前者,避免每次观察状态都弹出原生提示;真正提交时再让浏览器呈现相应校验反馈。
属性还是布尔属性:在HTML里写readonly="false"仍然表示只读,disabled="false"仍然表示禁用。动态控制应使用对应布尔属性或明确增删属性,不能把后端字符串false直接拼回HTML就认为已经关闭。
样式中的:read-only也不能简单理解成“写了readonly属性”。它按元素是否可由用户编辑等规则匹配,范围可能比属性选择器更广。若产品只想突出显式只读字段,应考虑使用[readonly]表达那项状态,再分别处理禁用样式;不要从背景颜色反推字段一定带有哪项布尔属性。先确认状态再看样式,避免把交互规则与颜色混成同一个判断。
最后恢复只读邮箱、启用说明,观察采集结果;再把邮箱改为合法值并取消只读,验证原生提交能够进入处理器。让同一字段分别走编辑、验证、采集三条路径,比仅仅测试“鼠标能不能点进去”更能保住表单合同。