更强大的表单控件

借助新活动和自定义元素 API,您可以更轻松地参与表单。

Arthur Evans

许多开发者会构建自定义表单控件,其目的要么是为了提供未内置在浏览器中的控件,要么是为了自定义外观(使用内置表单控件无法实现的功能)。

不过,很难实现内置 HTML 表单控件的功能。设想一下当您将 <input> 元素添加到表单时,该元素会自动获取的某些功能:

  • 输入的内容会自动添加到表单的控件列表中。
  • 输入内容的值会自动随表单提交。
  • 输入会参与表单验证。您可以使用 :valid:invalid 伪类设置输入的样式。
  • 重置表单、重新加载表单或浏览器尝试自动填充表单条目时,系统会通知输入内容。

自定义表单控件通常只有少数这样的功能。开发者可以解决 JavaScript 中的一些限制问题,例如向表单添加隐藏的 <input> 以参与表单提交。但是,其他功能是无法通过单靠 JavaScript 实现的。

我们新增了两项 Web 功能,让您能够更轻松地构建自定义表单控件,并摆脱当前自定义控件的限制:

  • formdata 事件允许任意 JavaScript 对象参与表单提交,因此您无需使用隐藏的 <input> 即可添加表单数据。
  • 借助与表单关联的自定义元素 API,自定义元素的行为更像是内置表单控件。

这两项功能可用于创建效果更好的新控件类型。

基于事件的 API

formdata 事件是一种低级 API,可让任何 JavaScript 代码参与表单提交。该机制的运作方式如下:

  1. 您可以将 formdata 事件监听器添加到想要与之互动的表单。
  2. 当用户点击提交按钮时,表单会触发 formdata 事件,其中包含一个 FormData 对象,用来保存提交的所有数据。
  3. 每个 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);
});

不妨使用我们在 Glitch 上的示例来试试看。请务必在 Chrome 77 或更高版本上运行,以查看 API 的实际运行情况。

浏览器兼容性

浏览器支持

  • 5
  • 12
  • 4
  • 5

来源

与表单关联的自定义元素

您可以将基于事件的 API 用于任何类型的组件,但该 API 仅允许您与提交流程互动。

标准化的表单控件除了提交外,还会参与表单生命周期的许多部分。与表单关联的自定义元素旨在弥合自定义 widget 和内置控件之间的差距。与表单关联的自定义元素与标准化表单元素的许多功能相匹配:

  • 将与表单关联的自定义元素放在 <form> 中时,它会自动与表单关联,就像浏览器提供的控件一样。
  • 您可以使用 <label> 元素标记此元素。
  • 元素可以设置自动随表单提交的值。
  • 该元素可以设置一个标记,指示其是否具有有效输入。如果其中一个表单控件包含无效输入,则无法提交该表单。
  • 此元素可以为表单生命周期的各个部分(例如表单停用或重置为默认状态)提供回调。
  • 该元素支持表单控件的标准 CSS 伪类,例如 :disabled:invalid

好多功能啊!本文不会介绍所有这些元素,但会介绍将自定义元素与表单集成所需的基本信息。

定义与表单关联的自定义元素

如需将自定义元素转换为与表单关联的自定义元素,您需要执行几个额外的步骤:

  • 向自定义元素类添加静态 formAssociated 属性。此属性会告知浏览器将该元素视为表单控件。
  • 对元素调用 attachInternals() 方法,以获取表单控件的额外方法和属性,如 setFormValue()setValidity()
  • 添加表单控件支持的常见属性和方法,例如 namevaluevalidity

下面介绍了这些项在基本自定义元素定义中的作用:

// Form-associated custom elements must be autonomous custom elements--
// meaning 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() 方法可以采用以下三种值之一:

  • 字符串值。
  • 一个 File 对象。
  • 一个 FormData 对象。您可以使用 FormData 对象传递多个值(例如,信用卡输入控件可能会传递卡号、失效日期和验证码)。

如需设置简单值,请执行以下操作:

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);

输入验证

您的控件还可以通过对 internals 对象调用 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> 上发生了变化。disabled 参数表示元素的新停用状态。例如,当元素被停用时,元素可以禁用其 shadow 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 形参相同 - 可以是字符串、FileFormData 对象。

如果您无法仅根据值来恢复控件的状态,state 参数会非常有用。例如,假设您创建了一个具有多种模式(调色板或 RGB 色轮)的颜色选择器。可提交的 value 将是规范形式的选定颜色,例如 "#7fff00"。但是,要将控件恢复到特定状态,您还需要知道它处于哪种模式,因此state可能类似于 "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 currently 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;
}

可使用示例

以下示例将与表单关联的自定义元素的许多功能整合到一起。请务必在 Chrome 77 或更高版本上运行,以查看 API 的实际运行情况。

功能检测

您可以使用功能检测来确定 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 事件以及与表单关联的自定义元素提供了用于创建自定义表单控件的新工具。

formdata 事件没有提供任何新功能,但提供了一个界面,让您可以将表单数据添加到提交流程,而不必创建隐藏的 <input> 元素。

与表单关联的自定义元素 API 提供了一组新功能,使自定义表单控件的工作方式类似于内置表单控件。

主打图片由 Oudom Pravat 在 Un 创 作中。