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