フォームに関連付けられたカスタム要素

カスタム コントロールの値をフォームに表示するために、非表示の <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 は、ユーザーがコミット(スライダーのリリース、フィールドのぼかし)したときに、変更のストリームごとに 1 回起動します。それぞれ InputEvent と Event で作成します。

代替戦略

static formAssociated のサポートに依存する前に、'attachInternals' in HTMLElement.prototype && 'setFormValue' in ElementInternals.prototype で機能検出を行います。利用できない場合は、コントロールの値に同期された、視覚的に非表示のネイティブ <input> にフォールバックして、フォームが送信されるようにします。これは、非表示入力パターンが正当化される唯一のケースです。