與表單相關聯的自訂元素

請勿改用隱藏的 <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) 中,停用內部控制項 (或從分頁順序中移除並忽略輸入),而不是在主機上設定 disabled。在回呼中設定主機屬性可避免在重新啟用祖系 <fieldset> 時重新啟用,且主機已自動符合 :disabled。
  • 請遵循平台慣例來調度事件,並優先使用內建的 input 和 change 事件,而非自訂事件。當使用者動作 (輸入、拖曳滑桿) 直接導致值變更時,系統會觸發 input;當使用者提交變更 (放開滑桿、模糊欄位) 時,系統會針對每串變更觸發一次 change。請分別使用 InputEvent 和 Event 建構。

備用策略

請先使用 static formAssociated 支援功能偵測功能,再依賴該功能:'attachInternals' in HTMLElement.prototype && 'setFormValue' in ElementInternals.prototype。如果無法使用,請改用與控制項值同步的原生 <input>,並在視覺上隱藏,這樣表單仍可提交。這是唯一適合使用隱藏輸入模式的情況。