← 首页 / HTML 表单:从控件到提交与校验 待审核 24 页

HTML 表单:从控件到提交与校验

一个表单背后是一整套规范:手指按下键 → 键盘布局翻译成字符 → 进入 <input> / <textarea><form> 收编一批控件、按算法构造 entry list 并选定编码提交 → 浏览器做约束校验 → 应用层还可接管做实时格式化。本话题沿 WHATWG「4.10 Forms」规范从底到顶走一遍,把那些只有当场实测才看得清的边界行为逐一拆开演示。

顺序是:先看键盘把按键变成字符的事件模型;再看 <form> 元素与提交(谁被收编、entry list 如何构造、三种 enctype 的报文长什么样、reset 与 dirty value flag);然后逐个过表单控件(input 全部 type state、select / datalist、output / progress / meter、fieldset、label);接着是几个原生控件的边界行为;再到约束校验文本选择 API、autocomplete;最后是用库接管的实时 masking

键盘:从按键到字符

表单的入口是键盘。先把「一次按键如何变成 <input> 里的一个字符」这条事件链走清楚:ASCII 控制区、key / code 的分工、事件顺序与 IME 组字、以及 parser 视角下「字符即数字」。

ASCII · 0–31

Ctrl+H 为什么是退格

ASCII 前 32 个位置全是发给电传打字机的指令:退格、走纸、摇回行首、响铃。Ctrl 键的本职是把字母按位与 0x1F 变成控制字符——所以 Ctrl+H = 72 & 0x1F = 8 = 退格、Ctrl+[ = Esc。点整张 ASCII 表看每个字符的身份,拖位掩码看字母变指令,顺带讲清 DEL 为什么是 127(纸带打满孔作废)与 CR/LF 为什么是两个字符(打字机的两个机械动作)。

key · code · keyCode

key vs code:按键的两种名字

按任意键,实测面板列出全部属性。key 说「输入了什么」(受布局/Shift 影响),code 说「按的哪颗键」(物理位置,Dvorak 下也不变)——游戏 WASD 该用 code,快捷键 Ctrl+Z 该用 key。再看三个废弃属性的混乱:keyCode 在 keydown/keypress 两个事件里语义不同,标点区 Chrome 与 Firefox 数值都不一样;左右 Shift、小键盘、长按 repeat 都能当场验证。

event order · IME

一次按键,触发哪些事件

实时时间线抓下 keydown → (keypress) → beforeinput → input → keyup 的完整顺序与各自分工(beforeinput 是取消编辑的最后机会)。切中文输入法后行为完全不同:按键先交给 IME 组字,keydown 全变 keyCode 229isComposing: true,上屏靠 compositionstart/update/end——顺带修掉一个常见缺陷:选字的 Enter 把表单提交了

charCodeAt · tokenizer

parser 视角:字符就是数字

Babel 的 lexer 不写 s[i] === '{',写 code === charCodes.leftCurlyBrace——npm 包 charcodes 就是那张 ASCII 常量表(backSpace = 8……共 104 个)。逐字符拆任意代码看 code 与常量名、现场 benchmark 数字比较 vs 字符串比较、看 charCodeAt 在 😀 代理对上读不全字符,最后附全表与它「为 JS lexer 而生」的取舍证据。

<form> 元素与提交

表单的核心机制:<form> 收编哪些控件(form ownership)、提交时如何把它们构造成 entry list 并选定编码、按钮类型与 Enter 隐式提交、以及 reset 背后的 dirty value flag 与默认值。

form · action / method · ownership

<form> 元素:收编了哪些控件,提交去哪里

<form> 的两件本职:收编一批控件(form ownership)与描述提交去向(action / method / target)。两个反直觉点当场点出:控件归属哪张表单不取决于视觉位置——写了 form="someId" 的控件可待在 <form> 之外却仍被它提交;以及 form.action / form.method 这些 IDL 属性读出来的值经过规范化(相对路径解析成绝对 URL、非法 method 回落 get)。配一个 live form inspector 实时显示 form.elements 与解析后的提交去向。

entry list · enctype

提交算法:entry list 与三种编码

提交时浏览器按 tree order 把 submittable 控件构造成 entry list(successful controls):无 namedisabled、未勾选的 checkbox / radio、非 submitter 的按钮都不进。改表单字段,当场对照三种 enctype 的报文——application/x-www-form-urlencoded(默认,空格变 +)、multipart/form-data(文件上传必用,boundary 分段)、text/plain(不转义,仅供调试)。再演示 dirname 如何额外附带一条文本方向 field.dir=ltr

button[type] · implicit submission

按钮的四种类型,与 Enter 何时触发提交

<button> 不写 type默认就是 submit——一个本想做交互的按钮会悄悄提交整张表单、刷新页面。逐个点一排按钮看它解析出的 .type 与连带触发的 submit / reset:submit / reset / button 三种 <button> 都有,image(图形化 submit,附带点击坐标)则是 <input> 独有。另一条隐蔽链路是在文本框按 Enter 的 implicit submission:有无 submit 按钮、有几个文本字段决定它提交 / 直接提交 / 静默无事,四个 mini-form 各演示一条规则,并点出 textarea 的 Enter 是换行IME 选字的 Enter 不该提交

reset · dirty value flag

重置表单:dirty value flag 与默认值

type=reset 或调 form.reset() 会派发可取消的 reset 事件,把各控件还原为默认值<input value> 属性 = defaultValue、checkbox 的 checked 属性 = defaultChecked)。招牌是 dirty value flag:控件未被碰过时改 value 属性会同步到显示值;一旦用户输入过或脚本写过 .value(dirty=true),再改 value 属性就不再生效——这正是「SSR 注水时改 value 属性有时生效有时不生效」的根因。并排显示「当前值 vs 默认值」实测。

表单控件全家

规范里的各个表单控件逐个过一遍:<input> 的 22 种 type state 与值净化、<select> / <datalist>、显示型的 <output> / <progress> / <meter><fieldset> 的 disabled 传播,以及 <label> 与可标记元素。

input · 22 type states

一个 <input>,22 种身份:type state 与值净化

<input>type 是 enumerated attribute:非法 / 缺失值默认是 text。切换右侧 live <input> 的 type(覆盖全部 22 个 state),看 value sanitization 当场生效——number 抹掉非法字符、color 净化为小写 #rrggbb、range 把值 clamp 贴 step、url / email 去首尾空白。再按四类 value 模式(value / default-on / filename / default)对照各 type 支持哪些属性与 IDL(valueAsNumber / stepUp / .files)。

select · optgroup · option · datalist

下拉与建议:select / optgroup / optiondatalist

<select> 的单选取 .value、多选(multiple / size)要读 .selectedOptions<option> 默认 value = 文本内容、label 可覆盖显示、selected = defaultSelected;<optgroup> 分组且不能嵌套<input list> + <datalist> 给出自动补全建议,但不约束输入(列表外的值仍被接受)。并排实测单选 / 多选的读值差异,与 datalist 的「只建议不限制」。

output · progress · meter

三个显示型元素:output / progress / meter

<output for> 是一个 live region(默认 role=status),显示计算结果且变化时辅助技术会播报,但不进表单提交;<progress value max> 表示任务进度,省略 value 即 indeterminate(.position = -1);<meter value min max low high optimum>标量量规(磁盘 / 评分),颜色由 value 落在 low / high 划出的段、以及 optimum 在哪段共同决定。拖滑块看 meter 在绿 / 黄 / 红之间切换,顺带点破 progress 与 meter 最常见的互相误用。

fieldset · legend · disabled 传播

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

<fieldset disabled> 是唯一能用一条属性批量禁用一组控件的原生手段:全部后代控件变 disabled、不可编辑且不进表单提交——唯一例外是第一个 <legend> 内的控件仍可用。切换 disabled,实时看 fieldset.elements 里每个控件的状态与 FormData 里 entry 的消失;再演示嵌套 fieldset 的向下传播,以及它与 readonly(只读但仍提交)的关键区别。

label · labelable

<label>:点文字也能操作控件

两种关联方式:显式 <label for="id"> 指向控件、或隐式把控件包进 <label>。点 label 会把 click 与 focus 转交给关联控件——对 checkbox / radio 等于放大了命中区,点文字即可勾选。可标记的(labelable)元素有名单:button、input(type=hidden 除外)、meter、output、progress、select、textarea。实测 control.labels / label.control,并演示给 hidden input 配 label 为何无效。

原生控件的边界行为

几个只有当场实测才看得清的细节:<input type=range> 的 track / thumb 伪元素、<textarea> 的 RCDATA 解析、maxlength 按 code unit 计数、inputmode 的软键盘、field-sizing 自动长高。

input[type=range] · track / thumb

<input type=range> 上妆——track / thumb 伪元素

range slider 能用 CSS 改样子、却不能用 JS 操作内部:它由 track(滑轨)+ thumb(滑块)组成,两者藏在 shadow DOM 里,只能靠厂商伪元素(::-webkit-slider-thumb / ::-moz-range-thumb)选中,querySelector 不到。从最省事的 accent-color 一行换色,到 appearance: none 完全重绘,实测三个固定坑:厂商伪元素不能合并进一个选择器列表(否则整条规则被丢)、进度填充各家不同(Firefox 原生 ::-moz-range-progress vs WebKit 的 gradient + JS 变量)、input 连发 / change 收尾的事件分工。最后叠两个 input 拼一个原生没有的双向区间(min–max)slider。

textarea · RCDATA

<textarea> 的内容不是 HTML——RCDATA 解析

同一段 a<b>x</b> &amp;,放进 <div> 会被解析成真实元素,放进 <textarea> 却只当纯文本——因为后者处于 HTML 解析器的 RCDATA state:标签不开启、只有 </textarea> 收尾,但 character reference 照常解码。这正是「用 textarea 解码 HTML 实体」这一经典技巧的由来。再当场对照两者的 outerHTML 序列化差异。

maxlength · code unit

maxlength 数的是 UTF-16 code unit,不是字符

maxlength="6" 不等于「最多 6 个字」。它按 UTF-16 code unit(即 str.length)计数:一个 emoji 😀 占 2、国旗 🇨🇳 占 4、带 ZWJ 的家庭 emoji 占更多——于是 6 的上限可能只够打 1 个 emoji。逐字素拆开,并排对照 str.length(code unit)/ [...str].length(code point)/ Intl.Segmenter(字素)三种计数为何各不相同。

inputmode · enterkeyhint

inputmode / enterkeyhint——软键盘长什么样

移动端弹出的虚拟键盘布局,由 inputmode 决定,而非 type。验证码、卡号该用 type="text" inputmode="numeric" 而不是 type="number":后者带来抹除前导零、可输 e、滚轮误改值、maxlength 失效等一连串副作用。逐一对照 numeric / decimal / tel / email / url / search / none 的键盘,与 enterkeyhint 的回车键文案。软键盘需在手机上查看

field-sizing: content

field-sizing: content——textarea 一行 CSS 自动长高

过去让 <textarea> 随内容增高要靠 JS 监听 input 改高度。如今一行 field-sizing: content 即可,且对 <input> / <select> 同样生效。并排放 fixed(默认,内容溢出滚动)与 content(随内容伸缩),边打字边看高度变化,再叠加 min-height / max-height 设上下界。该属性早期提案名为 form-sizing: normal,落地时更名为 field-sizing

约束校验与文本选择

提交前的守门:约束校验把 required / pattern / min 等映射成 ValidityState 与校验 API;autocomplete 告诉浏览器字段语义;文本选择 API 读写光标与选区。

constraint validation · ValidityState

约束校验:八个 validity flag 与校验 API

required / pattern(锚定全匹配)/ min / max / step / minlength / type=email 等约束,会映射成 ValidityState 上的 valueMissing / typeMismatch / patternMismatch / rangeOverflow / stepMismatch / customError 等标志位。边输边看哪个 flag 点亮;实测 checkValidity / reportValidity / setCustomValidity / willValidate;并对照 :invalid(一上来就红)与 :user-invalid(交互后才红)的体验差异,以及 novalidate / formnovalidate 如何跳过校验。

autocomplete · autofill

autocomplete:告诉浏览器这个字段是什么

autocomplete 的值要么是 on / off,要么是一串顺序固定的 token:section-*shipping / billing → 联系方式 → 字段名(必需)→ 可选 webauthn。一个 token 解析器把任意值拆成各分量并判断是否合法。重点厘清三个正交维度:field type(type,数据与原生 UI)、autofill field name(autocomplete,语义)、input modality(inputmode,软键盘)——验证码框 type="text" inputmode="numeric" autocomplete="one-time-code" 三者各司其职。

text selection API

文本选择 API:读写光标与选区

selectionStart / selectionEnd / selectionDirection 读出光标与选区(偏移按 UTF-16 code unit 计);setSelectionRange / select() / setRangeText(四种 selectMode)写选区。这套 API 只对 text-ish 控件(text / search / tel / url / password / textarea)有效——对 type=number / emailselectionStart 返回 null,调用会抛 InvalidStateError。是做 textarea 工具栏 / 代码编辑器的基础,当场拖选 + 包裹加粗演示。

实时格式化与 masking

在用户键入时一边规范化一边守门 —— createSmartInput 把 IME 冲突、光标漂移、undo 撕裂等坑统一接管,调用方只描述一个格式化规则。

smart-input · 引擎剖析

Smart Input 引擎:能力与实现

先把这台引擎拆开再看 demo。原生 <input> 做 masking 绕不开的五个坑(IME 冲突、光标漂移、原生 undo 撕碎格式、粘贴富文本、拦截时机)如何被统一接管;两层 API(五个 helper + createSmartInput)与只需描述一个 Formatter 的契约;核心 refresh → normalize → strict 守门 → apply → commit 管线(单一可信源 (text, caret)、唯一写入点)。逐一拆开 normalize 光标重映射、isExtendable 前缀守门、逻辑层 history(为何不用原生)、IME 的 keyCode 229 兜底、字符偏移 caret、async 竞态、renderHtml 的 textContent 约束。附一个接真实 phoneFormatter 的 live inspector。

smart-input · formatters

Smart Input 格式化合集

同一套 createSmartInput 的五个场景:千分位 number(动态 min / max)、HH:MM:SS strict 守门、6 位 OTP(镜像格子 + 智能 paste)、多 tag chip 输入,以及 IME-safe 的 Enter 提交(合成期间放行,不误发)。history / IME / DOM 适配都由库接管,调用方只描述格式化规则。

smart-input · rule DSL

规则输入框 · masking 与实时格式化

领域专属动作规则 DSL 的渐进式输入:基础 masking(beforeinput 过滤 + 整体校验)、contenteditable 实时格式化(htmlRenderer 富渲染 + 次数上标)、strict isExtendable 前缀守门(不可能扩展到合法的按键直接吞)。可切换 rule.ts / parser.ts 两种 parser 视角看右侧 inspector。

相关链接

WHATWG HTML Standard · 4.10 Forms

  • 4.10 Forms · 总章 WHATWG 本话题的规范本体:<form> / 各控件 / form control infrastructure / 约束校验 / 提交算法 / reset 的权威定义,本系列各页与它逐节对应。
  • Form submission algorithm · entry list WHATWG 对应「提交算法」页:successful controls 的筛选、entry list 构造、三种 enctype(urlencoded / multipart / text-plain)的序列化。
  • Implicit submission WHATWG 对应「按钮类型与 Enter 提交」页:default button 的定义,与「无 submit 按钮时恰好一个 field that blocks implicit submission 才直接提交」的精确规则。
  • The dirname attribute WHATWG 对应「提交算法」页:dirname 如何在 entry list 里额外追加一条文本方向(field.dir=ltr)。
  • Constraint validation · ValidityState WHATWG 对应「约束校验」页:各 validity flag 的定义、willValidate、setCustomValidity 与 :valid / :invalid 的关系。
  • APIs for the text control selections WHATWG 对应「文本选择 API」页:selectionStart / End / Direction、setSelectionRange、setRangeText(四种 selectMode)的定义与适用范围。
  • RCDATA state WHATWG 对应「textarea / RCDATA」页:<textarea> / <title> 的内容只在遇到对应结束标签时退出,期间 < 不开启新标签,但 & 仍进入 character reference 解码。

MDN · 控件与属性参考

  • <input> · 全部 type state MDN 对应「input 的 22 种 type state」页:每个 type 的值净化、支持的属性与 IDL 一览。
  • <select> / <option> / <optgroup> / <datalist> MDN 对应「select / datalist」页:多选取值、option 缺省值规则、datalist 不约束输入。
  • <meter> 与 <progress> MDN 对应「output / progress / meter」页:meter 的 low / high / optimum 颜色语义,与 progress 的 indeterminate 区别。
  • autocomplete token 一览 MDN 对应「autocomplete」页:全部 autofill field name token、section-* / shipping / billing / webauthn 的顺序与含义。
  • field-sizing MDN 对应「field-sizing」页:fixed(默认)与 content 两个取值,对 <input> / <textarea> / <select> 的作用。
  • KeyboardEvent.key / .code MDN 对应「key vs code」页:两个现代属性的取值表与分工。

caniuse · 浏览器支持

延伸

  • UI Events KeyboardEvent code Values W3C code 的完整命名表 —— 每颗物理键叫什么,按 QWERTY 键帽定名。
  • charcodes npm 「parser 视角」页的主角:Babel 团队的 ASCII 常量表。
  • Styling and scripting sliders quirksmode 对应「range slider」页:跨浏览器 track / thumb 伪元素与事件兜底,「厂商伪元素不能合并写」这一结论至今成立。
  • regex × Unicode 字符模型 同站 charCodeAt 读不全 emoji、maxlength 按 code unit 计数、文本选择偏移的共同根源:代理对与 Unicode 字符模型。
  • Dialog / Popover 原生浮层 同站 <form method="dialog"> 提交关闭对话框并写入 returnValue —— 「form 元素与提交」页提到的特殊提交路径。