0

0

如何正确实现表单关联自定义元素(FACE)的验证逻辑

聖光之護

聖光之護

发布时间:2026-02-22 09:47:01

|

314人浏览过

|

来源于php中文网

原创

如何正确实现表单关联自定义元素(FACE)的验证逻辑

本文详解如何为表单关联自定义元素(Form-Associated Custom Element, FACE)实现符合标准的约束验证(Constraint Validation API),重点解决“invalid form control is not focusable”报错,确保 checkValidity()、reportValidity() 和表单提交流程正常工作。

本文详解如何为表单关联自定义元素(form-associated custom element, face)实现符合标准的约束验证(constraint validation api),重点解决“invalid form control is not focusable”报错,确保 `checkvalidity()`、`reportvalidity()` 和表单提交流程正常工作。

在构建可参与原生表单验证的自定义元素(即 FACE)时,仅设置 static formAssociated = true 和调用 this.attachInternals() 是远远不够的。一个常见且令人困惑的错误是:当表单尝试提交或调用 form.reportValidity() 时,控制台抛出 “An invalid form control with name='xxx' is not focusable” —— 这并非表示你的元素无效,而是浏览器无法定位到一个可聚焦的、承载验证状态的内部控件。

根本原因在于:internals.setValidity() 必须显式传入一个可聚焦的 DOM 元素作为第三个参数(即 validity anchor),否则浏览器在触发错误提示时不知道该将焦点移向何处,从而中断验证流程。

以下是一个修复后的、生产就绪的 MyName 自定义元素实现,已通过 Chrome/Firefox/Safari 验证:

// name.js
class MyName extends HTMLElement {
  static formAssociated = true;

  constructor() {
    super();
    this.internals = this.attachInternals();
    const shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: true });

    // 初始化 FormData(用于提交值)
    this._data = new FormData();
    this._data.set('firstname', '');
    this._data.set('lastname', '');

    // 渲染 Shadow DOM
    shadowRoot.innerHTML = `
      <style>
        :host { display: block; }
        input:invalid { border-color: #d32f2f; outline: 2px solid #f44336; }
        .error { color: #d32f2f; font-size: 0.875rem; margin-top: 4px; }
      </style>
      <div>
        <h2 part="title">Name Form</h2>
        <p><input type="text" name="firstname" placeholder="First name" minlength="2" required /></p><div class="aritcle_card flexRow">
                                                        <div class="artcardd flexRow">
                                                                <a class="aritcle_card_img" href="/ai/927" title="Calliper 文档对比神器"><img
                                                                                src="https://img.php.cn/upload/ai_manual/000/000/000/175679997868619.jpg" alt="Calliper 文档对比神器"  onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
                                                                <div class="aritcle_card_info flexColumn">
                                                                        <a href="/ai/927" title="Calliper 文档对比神器">Calliper 文档对比神器</a>
                                                                        <p>文档内容对比神器</p>
                                                                </div>
                                                                <a href="/ai/927" title="Calliper 文档对比神器" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
                                                        </div>
                                                </div>
        <p><input type="text" name="lastname" placeholder="Last name" minlength="2" required /></p>
        <p class="error"></p>
      </div>
    `;
  }

  connectedCallback() {
    if (this._initialized) return;
    this._initialized = true;

    const inputs = this.shadowRoot.querySelectorAll('input');
    const errorEl = this.shadowRoot.querySelector('.error');

    // 绑定输入事件,实时更新数据与验证状态
    inputs.forEach(input => {
      input.addEventListener('input', () => {
        this._data.set(input.name, input.value);
        this.internals.setFormValue(this._data);
        this._updateValidity();
      });

      // 同步初始值(如通过 value 属性设置)
      if (this.hasAttribute('value')) {
        try {
          const parsed = JSON.parse(this.getAttribute('value'));
          if (parsed.firstname && parsed.lastname) {
            input.value = parsed[input.name] || '';
            this._data.set(input.name, input.value);
          }
        } catch (e) { /* ignore */ }
      }
    });

    // 初始化验证状态
    this._updateValidity();
  }

  // ✅ 关键修复:setValidity 必须传入具体的 input 元素作为 anchor
  _updateValidity() {
    const inputs = this.shadowRoot.querySelectorAll('input');
    let isValid = true;
    let firstInvalidInput = null;

    for (const input of inputs) {
      if (!input.checkValidity()) {
        isValid = false;
        firstInvalidInput = input;
        break;
      }
    }

    if (isValid) {
      this.internals.setValidity({});
    } else {
      // ⚠️ 第三个参数必须是可聚焦的 <input> 元素!
      this.internals.setValidity(
        { customError: true },
        'Please fill in both names correctly.',
        firstInvalidInput // ← validity anchor
      );
    }
  }

  // ✅ 暴露标准属性与方法(供表单和开发者调用)
  get validity() {
    return this.internals.validity;
  }

  get willValidate() {
    return this.internals.willValidate;
  }

  checkValidity() {
    return this.internals.checkValidity();
  }

  reportValidity() {
    return this.internals.reportValidity();
  }

  // ✅ 值访问器(支持双向绑定)
  get value() {
    return this._data;
  }

  set value(v) {
    if (v instanceof FormData) {
      this._data = v;
      this.shadowRoot.querySelector('input[name="firstname"]').value = v.get('firstname') || '';
      this.shadowRoot.querySelector('input[name="lastname"]').value = v.get('lastname') || '';
      this.internals.setFormValue(v);
      this._updateValidity();
    }
  }

  // ✅ 表单生命周期回调
  formResetCallback() {
    this._data.set('firstname', '');
    this._data.set('lastname', '');
    this.shadowRoot.querySelector('input[name="firstname"]').value = '';
    this.shadowRoot.querySelector('input[name="lastname"]').value = '';
    this.internals.setFormValue(this._data);
    this._updateValidity();
  }

  formDisabledCallback(isDisabled) {
    this.shadowRoot.querySelectorAll('input').forEach(i => i.disabled = isDisabled);
  }
}

customElements.define('my-name', MyName);

使用示例(HTML + 表单集成)

<form id="myForm">
  <my-name name="person"></my-name>
  <button type="submit">Submit</button>
</form>

<pre class="brush:php;toolbar:false;" id="formdata">
<script> document.getElementById('myForm').addEventListener('submit', e => { e.preventDefault(); const el = document.querySelector('my-name'); if (el.checkValidity()) { console.log('✅ Valid!'); const data = Object.fromEntries(el.value.entries()); document.getElementById('formdata').textContent = JSON.stringify(data, null, 2); } else { console.log('❌ Invalid — browser will show native popover'); el.reportValidity(); // 触发聚焦与提示 } }); </script>

关键注意事项与最佳实践

  • setValidity(anchor) 的 anchor 参数不可省略:它必须是 Shadow DOM 中一个真实的、可聚焦的 元素(不能是 this 或 shadowRoot)。这是解决 “not focusable” 错误的唯一可靠方式。
  • 避免重复调用 setFormValue() 在非必要时机:每次调用都会触发表单状态变更,建议只在值真正变化后调用。
  • formResetCallback 和 formDisabledCallback 是必需的:它们确保自定义元素能响应
    和 form.disabled = true 等原生行为。
  • 不要覆盖 reportValidity() 方法体:你当前代码中重写了 reportValidity() 却引用了未定义的 _internals,应直接委托给 this.internals.reportValidity()。
  • 样式兼容性:使用 :host 和 input:invalid 可以无缝继承原生验证样式;若需自定义错误提示,建议结合 validity.customError + setCustomValidity('') 清除状态。

? 提示:Material Web 的 constraint-validation.ts 是极佳参考——它将验证逻辑抽象为 mixin,并强制子类实现 getValidityAnchor(): Element | null,确保每个 FACE 都明确声明其 validity anchor。

通过以上实现,你的自定义元素将完全融入 HTML 表单生态:支持 required、minlength、pattern 等原生约束,响应 checkValidity()、reportValidity()、form.reportValidity(),并在提交失败时正确聚焦并显示原生错误气泡,彻底告别 “not focusable” 报错。

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
chrome什么意思
chrome什么意思

chrome是浏览器的意思,由Google开发的网络浏览器,它在2008年首次发布,并迅速成为全球最受欢迎的浏览器之一。本专题为大家提供chrome相关的文章、下载、课程内容,供大家免费下载体验。

973

2023.08.11

chrome无法加载插件怎么办
chrome无法加载插件怎么办

chrome无法加载插件可以通过检查插件是否已正确安装、禁用和启用插件、清除插件缓存、更新浏览器和插件、检查网络连接和尝试在隐身模式下加载插件方法解决。更多关于chrome相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

799

2023.11.06

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

246

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

826

2024.03.01

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

246

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

826

2024.03.01

DOM是什么意思
DOM是什么意思

dom的英文全称是documentobjectmodel,表示文件对象模型,是w3c组织推荐的处理可扩展置标语言的标准编程接口;dom是html文档的内存中对象表示,它提供了使用javascript与网页交互的方式。想了解更多的相关内容,可以阅读本专题下面的文章。

3843

2024.08.14

点击input框没有光标怎么办
点击input框没有光标怎么办

点击input框没有光标的解决办法:1、确认输入框焦点;2、清除浏览器缓存;3、更新浏览器;4、使用JavaScript;5、检查硬件设备;6、检查输入框属性;7、调试JavaScript代码;8、检查页面其他元素;9、考虑浏览器兼容性。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

194

2023.11.24

pixiv网页版官网登录与阅读指南_pixiv官网直达入口与在线访问方法
pixiv网页版官网登录与阅读指南_pixiv官网直达入口与在线访问方法

本专题系统整理pixiv网页版官网入口及登录访问方式,涵盖官网登录页面直达路径、在线阅读入口及快速进入方法说明,帮助用户高效找到pixiv官方网站,实现便捷、安全的网页端浏览与账号登录体验。

928

2026.02.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号