使用 ElementInternals 创建自定义表单控件

Avatar of Caleb Williams
Caleb Williams

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200美元免费额度!

自古以来,人类就梦想着对表单元素拥有更多控制权。好吧,我可能有点夸大了,但多年来,创建或自定义表单组件一直是前端网页开发的圣杯。

自定义元素(例如<my-custom-element>)中一个鲜为人知但功能强大的特性,已悄然从 Chrome 77 版本开始引入,并正在逐步进入其他浏览器。ElementInternals 标准是一组非常令人兴奋的功能,但名称却非常不起眼。内部功能添加的功能包括参与表单的能力以及围绕辅助功能控件的 API。

在本文中,我们将探讨如何创建自定义表单控件、集成约束验证、介绍内部辅助功能的基础知识,并了解如何将这些功能结合起来以创建高度可移植的宏表单控件。

让我们从创建一个非常简单的自定义元素开始,该元素与我们的设计系统相匹配。我们的元素将将所有样式保存在 Shadow DOM 中并确保一些基本的辅助功能。我们将在代码示例中使用来自 Google 的 Polymer 团队的出色LitElement,尽管您绝对不需要它,但它确实为编写自定义元素提供了很好的抽象。

在此 Pen 中,我们创建了一个<rad-input>,它具有一些基本设计。我们还在表单中添加了第二个输入,这是一个普通的 HTML 输入,并添加了一个默认值(因此您可以只需按提交即可查看其工作原理)。

当我们单击提交按钮时,会发生一些事情。首先,调用提交事件的preventDefault方法,在本例中,是为了确保页面不会重新加载。在此之后,我们创建一个FormData对象,该对象使我们能够访问有关表单的信息,我们使用这些信息来构造 JSON 字符串并将其附加到<output>元素。但是,请注意,添加到输出中的唯一值来自具有name="basic"的元素。

这是因为我们的元素还不知道如何与表单交互,因此让我们使用ElementInternals实例设置我们的<rad-input>以帮助它名副其实。首先,我们需要在元素的构造函数中调用方法的attachInternals方法,我们还将在页面中导入ElementInternals polyfill以与尚不支持该规范的浏览器一起使用。

attachInternals方法返回一个新的元素内部实例,其中包含一些我们可以在方法中使用的新 API。为了让我们的元素能够利用这些 API,我们需要添加一个返回true的静态formAssociated getter。

class RadInput extends LitElement {
  static get formAssociated() {
    return true;
  }

  constructor() {
    super();
    this.internals = this.attachInternals();
  }
}

让我们看一下元素的internals属性中的一些 API

  • setFormValue(value: string|FormData|File, state?: any): void — 此方法将在其父表单(如果存在)上设置元素的值。如果值为null,则元素将不参与表单提交过程。
  • form — 如果存在,则为元素的父表单的引用。
  • setValidity(flags: Partial<ValidityState>, message?: string, anchor?: HTMLElement): void — setValidity方法将帮助控制表单中元素的有效性状态。如果表单无效,则必须存在验证消息。
  • willValidate — 如果在提交表单时将评估元素,则为true
  • validity — 一个有效性对象,其 API 和语义与HTMLInputElement.prototype.validity附加的 API 和语义匹配。
  • validationMessage — 如果使用setValidity将控件设置为无效,则此为描述错误的消息。
  • checkValidity — 如果元素有效,则返回true,否则返回false并在元素上触发invalid事件。
  • reportValidity — 与checkValidity相同,如果事件未取消,则会向用户报告问题。
  • labels — 使用label[for]属性标记此元素的元素列表。
  • 许多其他用于在元素上设置 aria 信息的控件。

设置自定义元素的值

让我们修改我们的<rad-input>以利用其中一些 API

在这里,我们修改了元素的_onInput方法以包含对this.internals.setFormValue的调用。这告诉表单我们的元素希望在其给定名称下(在我们的 HTML 中作为属性设置)向表单注册一个值。我们还添加了一个firstUpdated方法(在不使用LitElement时与connectedCallback大致相同),该方法在元素完成渲染时将元素的值设置为空字符串。这是为了确保我们的元素始终具有表单的值(虽然没有必要,但您可以通过传入null值将元素从表单中排除)。

现在,当我们向输入添加值并提交表单时,我们将在<output>元素中看到我们有一个radInput值。我们还可以看到我们的元素已添加到HTMLFormElementradInput属性中。但是,您可能已经注意到的一个问题是,尽管我们的元素没有值,它仍然允许表单提交。接下来,让我们向元素添加一些验证。

添加约束验证

为了设置字段的验证,我们需要稍微修改一下元素以使用元素内部对象上的setValidity方法。此方法将接收三个参数(如果元素无效,则仅需要第二个参数,第三个参数始终是可选的)。第一个参数是部分ValidityState对象。如果任何标志设置为true,则控件将被标记为无效。如果内置的有效性键之一不满足您的需求,则有一个万能的customError键应该可以工作。最后,如果控件有效,我们将传入一个对象文字({})以重置控件的有效性。

第二个参数是控件的有效性消息。如果控件无效,则此参数是必需的,如果控件有效,则不允许使用此参数。第三个参数是可选的验证目标,如果表单提交无效或调用reportValidity,它将控制用户的焦点。

我们将向我们的<rad-input>引入一个新方法,该方法将为我们处理此逻辑

_manageRequired() {
  const { value } = this;
  const input = this.shadowRoot.querySelector('input');
  if (value === '' && this.required) {
    this.internals.setValidity({
      valueMissing: true
    }, 'This field is required', input);
  } else {
    this.internals.setValidity({});
  }
}

此函数获取控件的值和输入。如果值等于空字符串并且元素被标记为必需,我们将调用internals.setValidity并切换控件的有效性。现在,我们只需要在firstUpdated_onInput方法中调用此方法,我们就可以向元素添加一些基本验证了。

在我们的<rad-input>中输入值之前单击提交按钮,现在将在支持ElementInternals规范的浏览器中显示错误消息。不幸的是,由于没有可靠的方法在不支持的浏览器中触发内置的验证弹出窗口,polyfill 仍然不支持显示验证错误

我们还通过使用internals对象向示例添加了一些基本的辅助功能信息。我们在元素中添加了一个附加属性_required,它将充当this.required的代理,并作为required的 getter/setter。

get required() {
  return this._required;
}

set required(isRequired) {
  this._required = isRequired;
  this.internals.ariaRequired = isRequired;
}

通过将required属性传递给internals.ariaRequired,我们通知屏幕阅读器我们的元素当前正在期望一个值。在 polyfill 中,这是通过添加aria-required属性来完成的;但是,在支持的浏览器中,不会将属性添加到元素中,因为该属性是元素固有的。

创建微型表单

现在我们已经拥有了一个符合我们设计系统的有效输入,我们可能希望开始将元素组合成可以在多个应用程序中重用的模式。ElementInternals 最引人注目的功能之一是setFormValue方法不仅可以接收字符串和文件数据,还可以接收FormData对象。因此,假设我们想要创建一个可能在多个组织中使用的通用地址表单,我们可以使用我们新创建的元素轻松地做到这一点。

在此示例中,我们在元素的 Shadow Root 中创建了一个表单,我们在其中组合了四个<rad-input>元素以创建地址表单。这次,我们选择传递整个表单的值,而不是使用字符串调用setFormValue。因此,我们的元素将其子表单中每个单独元素的值传递到外部表单。


向此表单添加约束验证将是一个相当简单的过程,提供其他样式、行为和内容插槽也是如此。使用这些较新的 API 最终允许开发人员在自定义元素中释放大量潜力,并最终让我们能够自由控制用户体验。