Web 平台 API / HTML 表单:从控件到提交与校验 / fieldset 与 legend:一条属性禁用一组控件 待审核 12 / 24
fieldset · legend · disabled 传播

fieldset 与 legend:一条属性禁用一组控件

<fieldset> 把一组相关控件在语义与视觉上归拢成一束,<legend> 是这束控件的标题(渲染在边框上)。它最有分量的能力藏在一条布尔属性里:给 <fieldset> 写上 disabled,它的全部后代 form 控件会一起变成 disabled——不可编辑、不可聚焦、且不进表单提交。这是原生 HTML 里唯一能用一条属性把一组控件变成 disabled 并逐出 entry list 的手段(<optgroup disabled> 只作用于组内 option,inert 只去交互、不改 disabled 也不影响提交)。规范留了一个精心设计的例外:位于该 fieldset 第一个 <legend> 里的控件不受这层传播影响,仍可操作——因为标题区往往要放一个「整组开关」。

1 · disabled 的向下传播与第一个 legend 的例外

点右上角开关切换外层 <fieldset>disabled 属性。禁用后,框内所有控件即刻锁死,右侧 new FormData(form) 里那些 entry 全部消失;而 legend 里那个开关复选框(它有 name=mode、且因身处第一个 legend 而不被禁用)仍留在 FormData 中。注意内层虚线 fieldset 自己没写 disabled,但外层一旦 disabled,它的后代也被连带禁用——这就是「沿 DOM 向下」的含义。

图 1-1 · 切换外层 fieldset 的 disabled 属性,观察框内控件锁死、对应 entry 从 FormData 中消失,而第一个 legend 里的开关不受传播影响。

「被禁用 via ancestor fieldset」也反映在控件自身的 .disabled。内层那个 otp 输入框从没写过 disabled,但当外层 fieldset 被禁用时,读 otp.disabled 返回 true——规范定义的 disabled 状态是「自身有 disabled 属性,祖先 fieldset 处于 disabled 且自己不在其第一个 legend 内」。所以脚本无需关心是谁把它禁掉的,统一读 el.disabled 即可。

2 · fieldset 自身的 listed 身份

<fieldset> 属于 listed 元素(但不在 resettable 之列——那一类只有 input / output / select / textarea 与 form-associated custom element),所以它出现在 form.elements 里,也有 .type === "fieldset".name.disabled 等成员;还有一个专属的 .elements 属性——收录它后代里的 listed 控件。但它不是 submittable:fieldset 自身永远不进 entry list,FormData 里不会出现名为 account 的 entry。下面把这几个属性当场读出来,并可切换 disabled 观察它是否影响这条结论。

图 2-1 · fieldset 作为 listed 元素在 form.elements 中的表现,以及它专属的 elements 属性收录了哪些后代控件。
禁用 / 只读手段 可编辑 可聚焦 进表单提交 典型用途
单个控件 disabled 关掉某一个控件;它的值提交。
<fieldset disabled> 批量 否(全组) 否(全组) 否(全组) 一条属性禁用整组后代控件;第一个 legend 内的控件例外仍可用。
单个控件 readonly 值不可改但仍随表单提交、仍可聚焦 / 复制,但被逐出约束校验(实测 required 的空 readonly 框 willValidatefalsevalueMissingfalse);支持它的是 text / search / url / tel / email / password / date / month / week / time / datetime-local / number 与 <textarea>

3 · 同一份代码里的三件事

<form>
  <fieldset name="account" disabled>
    <legend>
      账户设置
      <!-- 第一个 legend 内的控件例外:不被禁用,仍提交 -->
      <label><input type="checkbox" name="mode" value="advanced" /> 高级模式</label>
    </legend>

    <input name="nickname" />      <!-- disabled → 不进 FormData -->
    <select name="plan">…</select> <!-- disabled → 不进 FormData -->

    <fieldset>
      <!-- 未写 disabled,但因外层 disabled 而被连带禁用 -->
      <input name="otp" />
    </fieldset>
  </fieldset>
</form>
fieldset.type               // "fieldset"
fieldset.disabled           // true
[...fieldset.elements]      // 后代 listed 控件集合
otp.disabled                // true(被祖先 fieldset 传播)
new FormData(form)          // 仅剩 mode=advanced;fieldset 自身不进 entry

disabled 不提交,readonly 提交——这是最常踩的混淆。被禁用的控件(无论自身写 disabled 还是被祖先 fieldset 传播)不是 successful control,不进 entry list,详见提交算法;而 readonly 的控件只是用户不能改值,提交时照样带上。要「展示但不可改、又要把值发回服务端」,用 readonly;要「这块暂时不参与、值也别发」,用 disabled

第一个 legend 的例外,以及 fieldset 不进 entry list。传播规则只豁免第一个 <legend> 内的控件——第二个及之后的 legend(若有)不享受豁免,且只有第一个 legend 会渲染成 caption。另外别指望从 FormData 里找到 fieldset:它是 listed 但不可提交,只有它内部的 input / select / textarea 等才会贡献 entry。