发布日期:2019 年 8 月 8 日
许多开发者会构建自定义表单控件,要么是为了提供浏览器中没有内置的控件,要么是为了自定义外观和感觉,使其超出内置表单控件的功能范围。
然而,要复制内置 HTML 表单控件的功能可能很困难。考虑一下将 <input> 元素添加到表单时会自动获得的一些特性:
- 输入内容会自动添加到表单的控件列表中。
- 输入框的值会随表单自动提交。
- 输入参与 表单验证。您可以使用
:valid和:invalid伪类来设置输入框的样式。 - 当表单重置、表单重新加载或浏览器尝试自动填充表单条目时,输入框会收到通知。
自定义表单控件通常很少具备这些功能。开发者可以通过一些方法来规避 JavaScript 的一些限制,例如向表单添加隐藏的 <input> 来参与表单提交。但有些功能仅靠 JavaScript 是无法实现的。
两项 Web 功能使构建自定义表单控件变得更加容易,并消除了自定义控件的限制:
formdata事件允许任意 JavaScript 对象参与表单提交,因此您可以添加表单数据而无需使用隐藏的<input>。- 表单关联的自定义元素 API 使自定义元素能够像内置表单控件一样运行。
这两个特性可以用来创建更好用的新型控件。
基于事件的 API
formdata 事件是一个底层 API,允许任何 JavaScript 代码参与表单提交。
- 向要交互的表单添加一个
formdata事件监听器。 - 当用户点击提交按钮时,表单会触发一个
formdata事件,其中包含一个FormData对象,该对象保存所有要提交的数据。 - 每个
formdata监听器都有机会在表单提交之前添加或修改数据。
以下示例展示了如何在 formdata 事件监听器中发送单个值:
const form = document.querySelector('form');
// FormData event is sent on <form> submission, before transmission.
// The event has a formData property
form.addEventListener('formdata', ({formData}) => {
// https://developer.mozilla.org/docs/Web/API/FormData
formData.append('my-input', myInputValue);
});
表单关联的自定义元素
你可以将基于事件的 API 与任何类型的组件一起使用,但它只允许你与提交过程进行交互。
标准化表单控件参与表单生命周期的许多环节。 表单关联的 自定义元素 旨在弥合自定义小部件和内置控件之间的差距。表单相关的自定义元素与标准化表单元素的许多功能相匹配:
- 当您将与表单关联的自定义元素放置在
<form>中时,它会自动与表单关联,就像浏览器提供的控件一样。 - 可以使用
<label>元素来标记该元素。 - 该元素可以设置一个值,该值将随表单自动提交。
- 该元素可以设置一个标志,指示其输入是否有效。如果表单中的某个控件输入了无效内容,则无法提交表单。
- 该元素可以为表单生命周期的各个部分提供回调,例如表单被禁用或重置为默认状态时。
- 该元素支持表单控件的标准 CSS 伪类,例如
:disabled和:invalid。
本文档并未涵盖所有内容,但介绍了将自定义元素与表单集成所需的基本知识。
定义一个与表单关联的自定义元素
将自定义元素转换为表单关联的自定义元素需要一些额外的步骤:
- 向自定义元素类添加静态的
formAssociated属性。这会告诉浏览器将该元素视为表单控件。 - 对元素调用
attachInternals()方法,以获取对表单控件(如setFormValue()和setValidity())的额外方法和属性的访问权限。 - 添加表单控件支持的通用属性和方法,如
name、value和validity。
以下是这些元素如何融入基本自定义元素定义中:
// Form-associated custom elements must be autonomous custom elements.
// They must extend HTMLElement, not one of its subclasses.
class MyCounter extends HTMLElement {
// Identify the element as a form-associated custom element
static formAssociated = true;
constructor() {
super();
// Get access to the internal form control APIs
this.internals_ = this.attachInternals();
// internal value for this control
this.value_ = 0;
}
// Form controls usually expose a "value" property
get value() { return this.value_; }
set value(v) { this.value_ = v; }
// The following properties and methods aren't strictly required,
// but browser-level form controls provide them. Providing them helps
// ensure consistency with browser-provided controls.
get form() { return this.internals_.form; }
get name() { return this.getAttribute('name'); }
get type() { return this.localName; }
get validity() {return this.internals_.validity; }
get validationMessage() {return this.internals_.validationMessage; }
get willValidate() {return this.internals_.willValidate; }
checkValidity() { return this.internals_.checkValidity(); }
reportValidity() {return this.internals_.reportValidity(); }
…
}
customElements.define('my-counter', MyCounter);
注册后,您可以在任何需要使用浏览器提供的表单控件的地方使用此元素:
<form>
<label>Number of bunnies: <my-counter></my-counter></label>
<button type="submit">Submit</button>
</form>
设置一个值
attachInternals() 方法返回一个 ElementInternals 对象,该对象提供对表单控件 API 的访问。其中最基本的是 setFormValue() 方法,它设置控件的当前值。
setFormValue() 方法可以采用以下三种类型的值之一:
设置值:
this.internals_.setFormValue(this.value_);
要设置多个值,可以这样做:
// Use the control's name as the base name for submitted data
const n = this.getAttribute('name');
const entries = new FormData();
entries.append(n + '-first-name', this.firstName_);
entries.append(n + '-last-name', this.lastName_);
this.internals_.setFormValue(entries);
输入验证
您的控件还可以通过对内部对象调用 setValidity() 方法来参与表单验证。
// Assume this is called whenever the internal value is updated
onUpdateValue() {
if (!this.matches(':disabled') && this.hasAttribute('required') &&
this.value_ < 0) {
this.internals_.setValidity({customError: true}, 'Value cannot be negative.');
}
else {
this.internals_.setValidity({});
}
this.internals.setFormValue(this.value_);
}
您可以像内置表单控件一样,使用 :valid 和 :invalid 伪类为与表单关联的自定义元素设置样式。
生命周期回调
与表单关联的自定义元素 API 包含一组额外的生命周期回调,用于与表单生命周期相关联。回调是可选的:只有当元素需要在生命周期的某个时间点执行某些操作时,才需要实现回调。
void formAssociatedCallback(form)
当浏览器将元素与表单元素关联,或将元素与表单元素取消关联时调用。
void formDisabledCallback(disabled)
以……命名disabled状态元素发生变化,可能是因为disabled该元素的属性被添加或删除;或者因为disabled状态已改变<fieldset>这是该元素的祖先元素。
例如,当该元素被禁用时,它可能会禁用其影子 DOM 中的元素。
void formResetCallback()
在表单重置后调用。元素应自行重置为某种默认状态。对于 <input> 元素,这通常涉及设置 value 属性以匹配标记中设置的 value 属性。对于复选框,这与设置 checked 属性以匹配 checked 属性有关。
void formStateRestoreCallback(state, mode)
在以下两种情况下会调用此方法:
- 当浏览器恢复元素的状态时,例如在导航之后或浏览器重新启动时。
mode实参为"restore"。 - 当浏览器的输入辅助功能(例如表单自动填充)设置值时。参数
mode为"autocomplete"。
第一个实参的类型取决于 setFormValue() 方法的调用方式。
恢复表单状态
在某些情况下(例如返回到某个网页或重启浏览器时),浏览器可能会尝试将表单恢复到用户离开时的状态。
对于与表单关联的自定义元素,恢复的状态来自您传递给 setFormValue() 方法的值。您可以调用该方法,并使用单个值形参(如前面的示例所示)或两个形参:
this.internals_.setFormValue(value, state);
value 表示控件的可提交值。可选的 state 参数是控件状态的 内部 表示,其中可以包含不会发送到服务器的数据。state 参数接受与 value 参数相同的类型:字符串、File 或 FormData 对象。
当仅凭值无法恢复控件的状态时,state 参数非常有用。例如,假设你创建了一个具有多种模式的颜色选择器:调色板或 RGB 色轮。可提交的值是规范形式的所选颜色,例如 "#7fff00"。如需将控件恢复到特定状态,您还需要知道它处于哪种模式,因此状态可能类似于 "palette/#7fff00"。
this.internals_.setFormValue(this.value_,
this.mode_ + '/' + this.value_);
你的代码需要根据存储的状态值来恢复其状态。
formStateRestoreCallback(state, mode) {
if (mode == 'restore') {
// expects a state parameter in the form 'controlMode/value'
[controlMode, value] = state.split('/');
this.mode_ = controlMode;
this.value_ = value;
}
// Chrome doesn't handle autofill for form-associated custom elements.
// In the autofill case, you might need to handle a raw value.
}
对于较简单的控件(例如数字输入),该值可能足以将控件恢复到之前的状态。如果您在调用 setFormValue() 时省略 state,则该值会传递给 formStateRestoreCallback()。
formStateRestoreCallback(state, mode) {
// Simple case, restore the saved value
this.value_ = state;
}
功能检测
您可以使用特征检测来确定 formdata 事件和表单关联的自定义元素是否可用。这两个功能都没有发布任何可用的 polyfill。在这两种情况下,您都可以退回到添加隐藏表单元素以将控件的值传播到表单。
表单相关自定义元素的许多更高级功能可能难以或不可能通过 polyfill 实现。
if ('FormDataEvent' in window) {
// formdata event is supported
}
if ('ElementInternals' in window &&
'setFormValue' in window.ElementInternals.prototype) {
// Form-associated custom elements are supported
}
formdata 事件为您提供了一个将表单数据添加到提交过程的接口,而无需创建隐藏的 <input> 元素。借助与表单关联的自定义元素 API,您可以为自定义表单控件提供一组新的功能,这些功能与内置表单控件类似。