你写了一个 自定义元素。五颗可以点击的星星,一个顺眼的悬停状态,一个 value 属性。你把它放进注册表单里,紧挨着那些真正的输入框。它看起来和周围的一切一模一样。

然后你提交表单。FormData 回来了,带着邮箱、密码、复选框——唯独没有评分。不是空值,是压根不存在。对

来说,你这个自定义元素从来没出现过。

打开网易新闻 查看精彩图片

这不是 bug,是平台规则。

表单只认一份固定名单

一个 只会从它认得的表单控件上收集值。这份名单是写死在平台里的:、、</strong>、,以及另外少数几个。

自定义元素不在名单上。不管它渲染得多像那么回事,不管你把它的属性起成什么名字,都不在。它可以有 name 属性,可以有 value getter,表单依然会像路过一个

一样路过它——因为就表单提交这件事而言,它确实就是一个 div。

你从 那里白拿的其余表单契约,也是同样的下场:

  • 不会聚焦它
  • required 不会阻止它提交
  • :invalid 不会给它上样式
  • form.reset() 不会清空它

这些全都没有接上。平台从来没有为自定义元素留过一个可以主动接入的机制。

最直觉的做法:藏一个镜像

显而易见的解法是加一个隐藏的镜像输入框。一个 ,配一个 ,然后监听星星的变化,把值同步过去。

这能跑通,前提是你说的"跑通"是指 FormData 里终于有值了。代价是你要额外维护一个元素,它存在的唯一意义就是:在第一个元素是假的地方,它是真的。

评分能变的每一个地方——点星星、键盘快捷键、预设按钮——都得记得同步这个镜像,否则两边会悄无声息地漂移开。

required 看起来是下一个要打的勾

于是有人把 required 加到了那个隐藏输入框上。它什么也不做。

被明确排除在约束校验之外,规范里的说法是"被禁止参与约束校验"。所以一个 required 的隐藏字段永远被视为有效,句号。

真正上线的是手写的检查,挂在提交处理器上:如果镜像的值为空,就阻止默认行为。这就是 required,用应用层代码重新实现了一遍,没有 :invalid 样式,也没有原生的"请填写此项"提示——因为那个逻辑本可以原生存在的地方,从来就不在候选名单里。

form.reset() 清掉了隐藏框,却没清掉星星

所以这又是一个要手写的监听器。你缺失的每一个原生行为,都会被重新实现成一个独立的事件监听器,挂在独立的元素上,还得和用户真正看到的那个元素保持同步。

这就是自定义元素在表单里的处境:渲染上它赢了,契约上它是个局外人。你得到的每一分原生能力,都得自己再挣一遍。