自古以来,人类就梦想着对表单元素拥有更多控制权。好吧,我可能有点夸大了,但多年来,创建或自定义表单组件一直是前端网页开发的圣杯。
自定义元素(例如<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
值。我们还可以看到我们的元素已添加到HTMLFormElement
的radInput
属性中。但是,您可能已经注意到的一个问题是,尽管我们的元素没有值,它仍然允许表单提交。接下来,让我们向元素添加一些验证。
添加约束验证
为了设置字段的验证,我们需要稍微修改一下元素以使用元素内部对象上的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 最终允许开发人员在自定义元素中释放大量潜力,并最终让我们能够自由控制用户体验。
你能总结一下这样做的益处吗?为什么要重新发明轮子,而这会导致很多额外的工作、bug 和安全问题?
这难道不像是很多现代 JavaScript 的炒作一样,对开发者来说很有趣,但对性能、稳定性、安全性、渐进增强和用户体验来说却很糟糕吗?
换句话说,你的第一句话是:“自古以来,专家们就警告过不要重建现有的浏览器元素。”
尽管如此,也许我遗漏了一些东西,并且这样做确实有一些优点(除了“它很酷”之外)?
嗨,Skythe。这是一个原生 API,它让你能够利用浏览器默认提供的功能。当然,它现在需要 polyfill,但希望很快就会有更多采用。
我看到的三个最大用例是
基本上,我们上面看到的只是使用
ElementInternals
的最小可行产品,如何继续使用取决于你。谢谢!在处理表单时,我总是尽可能地依赖浏览器的原生行为。
但有时这是不可能的。大多数情况下,我们会最终使用一个由自定义表单输入控制的 input type="hidden"。
但现在有了 ElementInternals,我们可以像原生表单输入一样直接处理错误和验证。
这太棒了。这里的主要好处是,现在有一个非 hacky 的解决方案来解决当你创建具有样式封装(ShadowDOM)的自定义输入时出现的问题,你很快就会意识到它没有注册到父
<form>
,因为它们之间存在一个 Shadow Boundary。我所知道的当前解决方案
– 生成一个 input type="hidden" 来委托。很明显,为什么这不好。
– 自定义输入通过
<slot>
包装原生输入,并在 LightDOM 中自行生成原生输入。但是 LightDOM 是组件用户领域,而不是组件作者领域。现在我们有了一个合适的方法:)。感谢你的文章!
表单关联的一个主要问题是自动填充表单关联的同级元素。这还没有得到支持,或者可能只是有 bug……?
在很多情况下,使用 name 属性会在给定的字段中触发自动完成。ElementInternals 中还有一个用于自动填充表单的 API,但它还没有 polyfill(仍在考虑实现它的最佳方法),并且部分功能在 Chrome 中仍不受支持。