自古以来,人类就梦想着对表单元素拥有更多控制权。好吧,我可能有点夸大了,但多年来,创建或自定义表单组件一直是前端网页开发的圣杯。
自定义元素(例如<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 中仍不受支持。