与表单关联的自定义元素

请勿回退到隐藏的 <input>,以便在表单中显示自定义控件的值。使用与表单关联的自定义元素 (FACE):通过 formAssociated 和 ElementInternals,该元素可以提交自己的值、参与验证并原生反映表单状态。

class RatingInput extends HTMLElement {
  // MANDATORY: opt in. Without this, attachInternals() form APIs are unavailable.
  static formAssociated = true;

  #internals;
  #value = '';

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

  connectedCallback() {
    // Validate initial state once attributes (such as `required`) are present.
    this.#validate();
  }

  get value() { return this.#value; }
  set value(v) {
    this.#value = v;
    // Push the value into the owning <form> for submission.
    this.#internals.setFormValue(v);
    this.#validate();
  }

  #validate() {
    if (this.hasAttribute('required') && !this.#value) {
      // Mark invalid and supply the native validation message + anchor.
      this.#internals.setValidity(
        { valueMissing: true },
        'Please choose a rating.',
        this.shadowRoot.querySelector('[tabindex]'),
      );
    } else {
      this.#internals.setValidity({}); // clear
    }
  }

  // Optional FACE lifecycle hooks:
  formResetCallback() { this.value = ''; }
  formDisabledCallback(disabled) {
    // Disable internal controls so they leave the tab order and ignore clicks.
    // Do NOT toggle `disabled` on `this`, or a `<fieldset disabled>` toggle will
    // leave the host permanently stuck with its own `disabled` attribute.
    for (const el of this.shadowRoot.querySelectorAll('button')) {
      el.disabled = disabled;
    }
  }
  formStateRestoreCallback(state) { this.value = state; }
}
customElements.define('rating-input', RatingInput);

要点:

  • static formAssociated = true 是必需的;它会解锁与表单相关的 ElementInternals 方法和 formXxxCallback 生命周期。
  • setFormValue(value) 是提交的内容,无需隐藏输入。为多值控件传递 FormData。
  • setValidity(flags, message, anchor) 与原生约束验证集成:表单在无效时不会提交,:invalid/:valid 会应用,并且锚点元素会在报告时获得焦点。调用 setValidity({}) 即可清除。在不同浏览器中,感知互动的 :user-valid/:user-invalid 伪类与表单关联元素的连接方式并不一致。在使用这些伪类之前,请先验证其支持情况,而不要假设它们会像跟踪原生控件那样跟踪用户互动。
  • 实现 formResetCallback、formDisabledCallback 和 formStateRestoreCallback,以便元素在重置、fieldset[disabled] 和后退/前进自动填充恢复时表现得像原生控件一样。在 formDisabledCallback(disabled) 中,停用内部控件(或将其从 Tab 键顺序中移除并忽略输入),而不是在宿主上设置 disabled。在回调中设置宿主属性会阻止在祖先 <fieldset> 重新启用时重新启用,并且宿主已自动匹配 :disabled。
  • 遵循平台惯例来分派事件,并优先使用内置的 input 和 change 事件,而不是自定义事件。当值因用户操作(输入、拖动滑块)而直接发生变化时,会触发 input;当用户提交(释放滑块、使字段失去焦点)时,每次更改流会触发一次 change。分别使用 InputEvent 和 Event 构建它们。

后备策略

在使用 static formAssociated 之前,请先检测其是否受支持:'attachInternals' in HTMLElement.prototype && 'setFormValue' in ElementInternals.prototype。如果该功能不可用,则回退到与控件值同步的视觉隐藏原生 <input>,以便表单仍能提交,这是隐藏输入模式唯一合理的使用场景。