随着2023年3月的Safari 16.4发布,Web组件在使用koude1 API与<form>
元素互动的能力中达到了一个里程碑。在此之前,无法通过表格发现位于Shadow DOM中的输入元素,这意味着它们不会在表单提交中验证,并且其数据将不包含在FormData
对象中。 ElementInterals
API允许我们创建与形式相关的自定义元素(简短面对)。现在,我们的自定义元素可以像本地输入元素一样行事,并利用form constraint validation的形式API。
我想探索API的功能,以便我们可以在工作中的组件库中实现它,但是当我开始工作时,我无法总是看到一条明确的途径,所以我决定写这篇文章任何遇到相同问题的人的文章。我还将在示例中使用TypeScript来帮助定义API。
入门
如果您已经设置了组件,或者您不按照自己的方式进行关注,请随时跳到以下表单的“自定义元素”。有代号可帮助您跳过。如果没有,here is a CodePen您可以开始。
设置组件
为了演示ElementInternals
API,我们的组件设置将非常简单,最初只有两个属性-value
和required
。此示例将使用“香草Web组件”,因此,如果您使用库或框架来构建组件,则组件设置可能会有所不同。设置组件时,请务必遵循这些最佳实践。好消息是,无论您使用哪种工具,实施ElementInternals
API似乎都相当一致。
添加输入
我们要做的第一件事是使用组件的connectedCallback
lifecycle钩子在我们的组件中添加阴影根并在其中插入输入和标签。
customElements.define('my-input', class extends HTMLElement {
connectedCallback() {
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<label>
My Input
<input type="text" />
</label>`;
}
});
让我们提到我们的输入元素。我们可以通过在我们的班级根部创建属性来做到这一点(我选择以$
的前缀为前缀,以区分引用HTML元素与其他属性的属性)。
private $input: HTMLInputElement;
在我们的connectedCallback
方法中,我们可以查询输入元素的阴影dom内容。
connectedCallback() {
...
this.$input = shadowRoot.querySelector('input');
}
添加属性
让我们添加value
和required
属性,以便我们可以将它们设置在我们的自定义元素标签上,然后将它们传递到我们的内部元素(<my-input value=”abc” required></my-input
)。我们将通过添加observedAttributes
属性并返回属性名称的数组来做到这一点。
static get observedAttributes() {
return ["required", "value"];
}
现在,当使用attributeChangedCallback
生命周期钩更改属性时,我们需要更新输入属性,但是我们将存在一个计时问题。 attributeChangedCallback
方法将在我们的内部输入有机会渲染和查询选择器之前运行,并将其分配给我们的$input
变量。为了解决这个问题,我们将创建一个组件变量来捕获属性和值,并且在准备就绪时将更新内部输入。
首先,让我们在我们的组件中添加一个名为_attrs
的私有属性,并将值设置为空对象。
private _attrs = {};
在我们的attributeChangeCallback
方法中,让我们分配任何属性更改为name
是更改的属性,而next
是新值。
attributeChangedCallback(name, prev, next) {
this._attrs[name] = next;
}
现在,让我们创建一个私人方法,该方法将使用我们的_attrs
值来更新我们的输入元素。
private setProps() {
// prevent any errors in case the input isn't set
if (!this.$input) {
return;
}
// loop over the properties and apply them to the input
for (let prop in this._attrs) {
switch (prop) {
case "value":
this.$input.value = this._attrs[prop];
break;
case "required":
const required = this._attrs[prop];
if (
required === "true" ||
required === true ||
required === ""
) {
this.$input.setAttribute("required", "");
}
if (
required === "false" ||
required === false ||
required === undefined
) {
this.$input.removeAttribute("required");
}
break;
}
}
// reset the attributes to prevent unwanted changes later
this._attrs = {};
}
我们现在可以将其添加到connectedCallback
方法中,以更新我们的输入元素,并在渲染之前发生任何更改。
connectedCallback() {
...
this.$input = shadowRoot.querySelector('input');
this.setProps();
}
我们还可以将其添加到attributeChangedCallback
方法中,以便将调用connectedCallback
方法调用后发生的任何属性更改都应用于输入元素。
attributeChangedCallback(name, prev, next) {
this._attrs[name] = next;
this.setProps();
}
现在,我们应该能够将属性添加到我们的元素标签中,并使它们传递到渲染的内部输入元素。
将自定义元素与表格相关联
关联您的自定义元素非常简单,可以以2个步骤完成:
-
将
formAssociated
静态属性设置为true
-
通过在组件的构造函数中调用
this._internals = this.attachInternals();
来公开ElementInternals
api。
static formAssociated = true;
private _internals: ElementInternals;
constructor() {
super();
this._internals = this.attachInternals();
}
通过这两个更改,我们的自定义元素现在可以看到父级表单!作为快速测试,尝试将console.log(this._internals.form);
添加到connectedCallback
方法中,您应该在控制台中登录父。
使用标签
通过使其成为形式相关的自定义元素,浏览器现在将其视为输入元素,这意味着我们可以将标签移出组件。
connectedCallback() {
...
shadowRoot.innerHTML = `<input type="text" />`;
}
我们可以将自定义输入元素标记为标准输入元素。让我们使用<label>
并使用自定义元素上的id
和标签上的for
属性引用。
<form id="my-form">
<label for="input">My Input</label>
<my-input id="input"></my-input>
</form>
注意: Safari和NVDA中有一个可访问性错误,其中标签与屏幕读取器的形式相关的自定义元素没有正确的关联。他们在铬浏览器(Chrome,Edger,Brave等)中使用配音透明,Mac上的Firefox,MS叙述者和窗户上的下颚。作为解决方法,您可以继续在元素中加入标签。
让我们还更新我们的影子根配置以委派焦点。这将允许在像本机输入元素一样单击标签时将输入集中在集中。
const shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: true });
如果
delegatesFocus
是true
,当单击阴影DOM的不可合并部分时,或者.focus()
被称为主机元素,则给出了第一个焦点部分,并给出了Shadow Host的任何可用的:focus
â:focus
样式。
暴露验证
现在表单与我们的自定义元素关联,我们可以通过公开一些核心验证行为开始,我们期望使用checkValidity
,reportValidity
,validity
和validationMessage
的输入元素。
public checkValidity(): boolean {
return this._internals.checkValidity();
}
public reportValidity(): void {
return this._internals.reportValidity();
}
public get validity(): ValidityState {
return this._internals.validity;
}
public get validationMessage(): string {
return this._internals.validationMessage;
}
控制验证
ElementIntenrals
api使我们可以访问setValidity
方法,我们可以使用该方法与我们的元素的有效性状态进行通信。
setValidity(flags: ValidityStateFlags, message?: string, anchor?: HTMLElement)
您可以看到,message
和anchor
属性是可选的。如果要重置验证,则可以将空对象({}
)作为标志参数传递。
标志
flags
接口几乎与调用input.validity
时获得的ValidityState
对象几乎相同,但是每个属性都是可选的,可以设置(其中一个输入的validity
是只读的属性)。这是界面的示例,也是某些何时将其用本机HTML输入元素设置的示例。
interface ValidityStateFlags {
/** `true` if the element is required, but has no value */
valueMissing?: boolean;
/** `true` if the value is not in the required syntax (when the "type" is "email" or "URL") */
typeMismatch?: boolean;
/** `true` if the value does not match the specified pattern */
patternMismatch?: boolean;
/** `true` if the value exceeds the specified `maxlength` */
tooLong?: boolean;
/** `true` if the value fails to meet the specified `minlength` */
tooShort?: boolean;
/** `true` if the value is less than the minimum specified by the `min` attribute */
rangeUnderflow?: boolean;
/** `true` if the value is greater than the maximum specified by the `max` attribute */
rangeOverflow?: boolean;
/** `true` if the value does not fit the rules determined by the `step` attribute (that is, it's not evenly divisible by the step value) */
stepMismatch?: boolean;
/** `true` if the user has provided input that the browser is unable to convert */
badInput?: boolean;
/** `true` if the element's custom validity message has been set to a non-empty string by calling the element's `setCustomValidity()` method */
customError?: boolean;
}
信息
message
参数是我们可以在验证这些验证参数之一时向元素提供自定义错误消息的方式。
锚
anchor
参数是我们要将错误消息与。
添加验证
现在我们可以控制组件中的验证方式,让我们添加功能以使我们的输入required
。
初始化验证
现在让我们初始化验证。因为我们的输入ValidityState
与ValidityStateFlags
的接口基本相同,所以我们可以使用输入的初始状态来设置ElementInterals
状态。在我们在connectedCallback
方法中的输入选择器之后,让我们根据输入调用setValidity
方法。
connectedCallback() {
...
this.$input = shadowRoot.querySelector('input');
this._internals.setValidity(this.$input.validity,
this.$input.validationMessage, this.$input);
}
在这里,我们使用内部输入元素的ValidityState
,但是您也可以通过ValidityStateFlags
的子集和自定义错误消息。
this._internals.setValidity(
{
valueMissing: true
},
'Please fill out this required field',
this.$input
);
测试验证
所有内容都应连接起来,所以让我们对其进行测试。让我们更新HTML以将提交按钮和一个required
属性添加到我们的自定义元素。
<form id="my-form">
<label for="input">My Input</label>
<my-input id="input" required></my-input>
<button>Submit</button>
</form>
单击“提交”按钮时,您应该查看所需字段的浏览器验证消息。如果我们选择表格并致电form.checkValidity()
,则应返回false
。
使用setValidity
更新验证
直到我们对其进行更新之前,验证状态将保持原样。如果您在输入中输入文本并单击“提交”,您仍然会看到一条错误消息,而form.checkValidity()
仍将返回false
。
对于此演示,每当用户输入字段中的内容时,我们都可以设置一个简单的更新。为此,我们将在选择输入元素后将事件侦听器添加到我们的connectedCallback
方法中。
connectedCallback() {
...
this.$input.addEventListener('input', () => this.handleInput());
}
private handleInput() {
this._internals.setValidity(this.$input.validity, this.$input.validationMessage, this.$input);
}
使用setFormValue
更新表单值
使用setFormValue
在ElementInternals
API上,我们现在可以在自定义元素中的值更改时更新表单。这使开发人员可以使用FormData
API轻松获取表单值。
当组件加载在connectedCallback
方法中时,请设置初始值。
connectedCallback() {
...
this._internals.setFormValue(this.value);
}
现在,让我们在input
事件触发的任何时候更新值。
private handleInput() {
...
this._internals.setFormValue(this.value);
}
测试表格数据
要测试我们的价值
<form id="my-form">
<label for="input">My Input</label>
<my-input id="input" name="myInput" required></my-input>
<button>Submit</button>
</form>
我们现在可以通过将事件侦听器添加到表单submit
事件并获取表单数据来查看我们的价值是否绑定到表单。
const form = document.getElementById("my-form");
form.addEventListener("submit", (e) => {
e.preventDefault();
const formData = new FormData(e.target);
console.log(`My Input Value - '${formData.get('myInput')}'`);
});
在输入中键入一个值,然后单击“提交”按钮。您应该在控制台中看到您的值登录。
ElementInternals生命周期钩
ElementInternals
API为我们提供了一些其他生命周钩,这些钩子对于控制与浏览器和其他元素的交互作用很重要。重要的是要注意,这些是可选的,仅应在必要时使用。
formAssociatedCallback(form: HTMLFormElement)
一旦元素与表单关联,这将立即调用。我们现在真的不需要这一点,所以我们现在就不会实施。
formDisabledCallback(disabled: boolean)
每当禁用元素或parent <fieldset>
元素时。我们可以使用它来帮助管理内部元素的残疾状态。我们将在类中添加回调方法并在更改内部输入元素时更新。
formDisabledCallback(disabled: boolean) {
this.$input.disabled = disabled;
}
formResetCallback()
这使我们能够在用户重置表单时控制元素的行为。在我们的情况下,我们将保持简单,并将输入值重置为加载组件时的初始值。我们将创建一个名为_defaultValue
的私有属性,将其设置在connectedCallback
方法中,然后使用formResetCallback
回调方法将值重置(如果复位)。
private _defaultValue = "";
connectedCallback() {
...
this._defaultValue = this.$input.value;
}
formResetCallback() {
this.$input.value = this._defaultValue;
}
让我们更新我们的表格以包括一个重置按钮,并在输入中添加初始值。现在我们可以更改值并按重置按钮,并将其恢复为原始值。
<form id="my-form">
<label for="input">My Input</label>
<my-input id="input" name="myInput" value="test" required></my-input>
<button type="reset">Reset</button>
<button>Submit</button>
</form>
formStateRestoreCallback(state, mode)
此方法回调使开发人员控制浏览器完成表单元素时发生的情况。 state
属性提供了使用setFormValue
设置的值,而mode
具有两个可能的值-restore
和autocomplete
。当用户从表单中导航并再次返回时,将设置restore
值,从而使其继续在其上停止的位置。当浏览器的输入辅助器试图自动完成表单时,使用autocomplete
值。 autocomplete
功能的缺点是,根据this article的说法,它尚未得到支持。在这种情况下,我们可以使用一个简单的实现来将输入恢复为保存值。
formStateRestoreCallback(state, mode) {
this.$input.value = state;
}
结论
您可以看到,这只会刮擦这些新API可以做的事情的潜力。我们仅在input
元素中可用的许多属性中实现了两个属性,当您介绍select
,textarea
和button
等其他表单元素时,事情变得更加有趣。希望这能为您提供与形式相关的自定义元素的扎实开端。愉快的编码!