← HTML 表单:从控件到提交与校验 / 约束校验:八个 validity flag 与校验 API 待审核 19 / 24
constraint validation · ValidityState

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

WHATWG「Constraint validation」给每个 form control 定义了一组约束(来自 required / pattern / min / max / step / minlength / maxlength / type),并把当前是否违反约束实时反映在控件的 el.validity(ValidityState)上——一组只读布尔标志位。提交前浏览器会自动跑一遍校验,脚本也能用 checkValidity() / reportValidity() / setCustomValidity() 主动驱动。本页把每个 flag 单独点亮一遍,并对照三个 API 的差异。约束的来源有哪些 type 可用,见 input 的 type states;整张表单用 novalidate 关闭校验,见 novalidate

1 · 逐字段:边输边看哪个 flag 被点亮

下面四个字段各带不同约束。每个右侧实时遍历 el.validity 的全部 key,把当前为 true 的 flag 高亮——valueMissing / typeMismatch / patternMismatch / rangeUnderflow / rangeOverflow / stepMismatch / tooShort / tooLong / badInput / customError,以及汇总位 valid。同时显示 validationMessage(浏览器给出的本地化文案)与命中的伪类。注意输入框用了 :invalid(红边,一上来空 required 就红)与 :user-invalid(红色光晕,只在交互后才出现)两种描边。

pattern 对整串锚定全匹配。pattern="\d{6}" 等价于浏览器把它包成 ^(?:\d{6})$test——必须整个值匹配,不是部分匹配,所以无需自己写 ^ / $(写了反而可能因 multiline 语义出问题)。空值不触发 patternMismatch(留给 required 管),要强制非空请配合 required

2 · setCustomValidity:把业务规则塞进同一套机制

内建约束管不到的业务规则(如「两次密码一致」「用户名已占用」),用 el.setCustomValidity(msg) 注入:传非空字符串 → 该控件 validity.customErrortrueel.validity.validfalse,且这条 msg 成为 el.validationMessage(也是 reportValidity() 气泡里显示的文案);传 "" 清除。下面对同一个框设 / 清自定义错误,看 customError 与文案的变化。

3 · checkValidity vs reportValidity:返回 bool,还是顺带弹气泡

两个 API 都对控件(或整张 form)跑一遍校验并返回 boolean;无效时都会向每个无效控件派发一个 cancelableinvalid 事件。区别只有一处:reportValidity()额外把焦点移到第一个无效控件并弹出原生气泡checkValidity() 是「静默」版,只返回结果、不打扰用户(适合脚本里先探一下)。下面对「逐字段」一节的同一张表单分别调用,invalid 事件计数会累加;点 reportValidity 还会看到原生气泡。

validity flag 什么时候为 true
valueMissing 控件带 required 且值为空(对 checkbox / radio 是未选中,对 select 是选了空 value 的项)。
typeMismatch 值不符合 type 的语法:type=email 不是合法邮箱、type=url 不是合法 URL。
patternMismatch 值非空且不满足 pattern(整串锚定全匹配)。
tooShort 值的长度小于 minlength。仅当用户「编辑过」才参与(脚本设值不算)。
tooLong 值的长度大于 maxlength。用户键入会被 UA 直接截断,故通常只在脚本设值时才点亮。
rangeUnderflow 数值型(number / range / date 等)的值小于 min
rangeOverflow 数值型的值大于 max
stepMismatch 数值不落在 step 的整数倍格点上(基准为 min 或 step base)。
badInput UA 无法把用户输入转换成值:如 type=number 框里残留非数字字符,此时 el.value 读出来是 ""
customError setCustomValidity() 设过非空字符串且未清除。
valid 汇总位:以上九个全为 false 时才为 true——即该控件通过全部约束。
跑校验 / 派发 invalid 返回值 移焦 + 弹原生气泡 改 customError
checkValidity() boolean
reportValidity() boolean
setCustomValidity(s) 否(只置位) void 是(非空→customError;""→清除)

4 · 同一份逻辑里的几件事

<!-- 约束写在 attribute 上 -->
<input name="zip" required pattern="\d{6}" />
<input name="qty" type="number" min="2" max="8" step="2" />

el.validity.valueMissing     // required 且空
el.validity.patternMismatch  // 值非空且不满足 pattern(整串全匹配)
el.validity.stepMismatch     // 不落在 step 格点上
el.validity.valid            // 汇总:以上全为 false 才 true

// 业务规则注入
el.setCustomValidity("用户名已被占用");  // → customError = true
el.setCustomValidity("");               // 清除
el.validationMessage;                   // 当前要显示的文案

// 静默校验 vs 弹气泡
const ok = form.checkValidity();   // 只返回 bool,派发 invalid 事件
form.reportValidity();             // 同上 + 移焦 + 弹原生气泡

// 这个控件会被校验吗?
el.willValidate;  // disabled / readonly / type=hidden / datalist 后代 → false

:invalid 一上来就红,:user-invalid 交互后才红。一个空的 required 字段从渲染那一刻就匹配 :invalid——若直接拿它描红边,用户还没动表单就满屏报错,体验很差。:user-invalid / :user-valid 是为此设计的:只有当用户已经交互过(编辑并失焦,或尝试提交)且仍无效时才匹配。生产里给字段上色应优先用 :user-invalid,把 :invalid 留给「需要持续标记」的少数场景。

关闭校验的两个开关。<form novalidate>整张表单提交时跳过约束校验(但 el.validity 和手动 checkValidity() 仍照常工作,本页表单正是 novalidate 才不会在按钮触发提交);单个 submit 按钮上的 formnovalidate局部覆盖——让「保存草稿」这种按钮绕过校验、而「正式提交」仍校验。详见 novalidate

willValidate:这个控件到底参不参与校验。并非所有控件都会被校验。当控件是 disabledreadonlytype=hidden<button> / output / 这类 non-listed,或位于 <datalist> 后代中时,el.willValidatefalse——它的约束被完全忽略,既不进自动校验、checkValidity() 也跳过它。判断「这个框现在会不会被校验」看 willValidate,而不是看它有没有写约束属性。