Специальные элементы, связанные с формой

Не используйте скрытый элемент <input>, чтобы передать значение пользовательского элемента управления в форму. Используйте специальные элементы, связанные с формой. С помощью 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>, синхронизированный со значением элемента управления, чтобы форма по-прежнему отправлялась. Это единственный случай, когда шаблон скрытого ввода оправдан.