Web 功能:
与表单关联的自定义元素
请勿回退到隐藏的 <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>,以便表单仍能提交,这是隐藏输入模式唯一合理的使用场景。