自定义元素不能直接用 required/pattern,因浏览器仅对原生表单控件执行约束验证;必须通过 formassociated: true、属性同步、方法代理和状态桥接,使自定义元素完全模拟原生 input 的验证行为。

自定义元素为什么不能直接用 required/pattern
因为浏览器只对原生表单控件(<input>、<select></select>、<textarea></textarea>)执行约束验证,<my-input></my-input>这类自定义元素压根不被 constraint validation API 识别。你写 <my-input required pattern="[0-9]{4}"></my-input>,required 和 pattern 只是普通 HTML 属性,不会触发任何校验逻辑。
更关键的是:my-input.reportValidity() 会直接报错 —— 它根本没这个方法;my-input.checkValidity() 同样不存在;my-input.validity 是 undefined。所有这些 API 都只存在于内部封装的原生 <input> 上。
必须把属性和方法都透传到内部 input
可行路径只有一条:用自定义元素封装一个原生 <input>,再通过 Shadow DOM + 属性反射 + 方法代理完成桥接。核心动作有三类:
- 属性同步:监听
attributeChangedCallback,把required、pattern、minlength等显式设到内部this.input上 - 方法代理:在自定义元素上手动实现
reportValidity()、checkValidity()、setCustomValidity(),内部调用this.input.xxx() - 状态桥接:暴露
validity、validationMessage、willValidate等 getter,返回this.input.validity等对应值
漏掉任意一项,表单提交时就无法参与校验流程,form.reportValidity() 会跳过它,form.checkValidity() 也读不到它的状态。
formAssociated 接口不是可选项,是必需项
光靠属性透传和方法代理还不够。如果想让 <my-input></my-input> 被父级 <form></form> 自动纳入验证范围(比如调用 form.reportValidity() 时它也能响应),必须启用 formAssociated: true 并实现 formAssociated 接口。
这意味着你要额外实现:
-
get form()getter:返回关联的HTMLFormElement -
name、type、value、checked等基础属性的映射(哪怕只是简单转发) -
willValidate和validity的完整桥接(不能只返回布尔值,得返回ValidityState对象)
否则,即使你手动调用了 myInput.reportValidity(),<form></form> 在 submit 时仍会忽略它 —— 它压根不认为这是个“可约束元素”。
异步校验必须绕开原生触发时机
自定义元素里做用户名唯一性校验?别指望 setCustomValidity() 在 fetch 回调里自动生效。它只是个快照,且浏览器只在 submit 或 reportValidity() 调用时读取一次。
正确做法是:在父表单的 submit 事件里 e.preventDefault(),先调 myInput.reportValidity() 过本地规则,再发请求;成功则清空错误并提交,失败则 myInput.setCustomValidity('已存在') + myInput.reportValidity()。
容易踩的坑:
- 没在每次请求前调
myInput.setCustomValidity(''),导致旧错误残留 - 没在
fetch前判断myInput.checkValidity() === true,结果后端校验失败但用户根本没看到前端提示 - 把异步逻辑塞进
invalid事件里 —— 这个事件只在浏览器判定失败后触发,时机太晚,无法干预提交流
复杂点在于:你既要让自定义元素“看起来像原生”,又要让它能承载业务逻辑;而浏览器只认原生语义,所有桥接都得自己补全,少一步,整个验证链就断了。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











