前言
HTML 表单验证功能强大但使用不足,主要原因是 API 设计的不便。今日前端早读课文章由 @飘飘翻译分享。
正文从这开始~~
HTML 表单具有强大的验证机制,但它们的使用率却非常低。实际上,很多人甚至对它们了解甚少。这是设计上的缺陷导致的吗?让我们来探讨一下。
【早阅】David A. Patterson:职业生涯前半个世纪的人生教训
Attributes, methods, and properties
通过添加一个 required
属性,可以轻松禁止输入为空:
<input required={true} />
除了这些之外,还有其他几种可以为您的输入添加约束的方法。具体来说,有三种方法可以实现这一点:
使用特定的 type 属性值,例如 "email" , "number" ,或 "url"
使用其他创建约束的输入属性,例如 "pattern" 或 "maxlength"
使用输入的
setCustomValidity
DOM 方法
最后一种是最强大的,因为它允许创建任意的验证逻辑并处理复杂情况。你注意到它与前两种技术有何不同吗?前两种是通过属性定义的,但 setCustomValidity
是一个方法。
命令式 API 的细微差别
setCustomValidity
API 仅作为方法暴露,并且没有与属性相对应的版本,这导致了糟糕的易用性问题。我将通过一个例子来演示。
不过,先简单介绍一下这个 API 的工作原理:
// Make input invalid
input.setCustomValidity("Any text message");
这将使输入无效,并且浏览器将显示原因:“任何文本消息”。
// Remove custom constraints and make input valid
input.setCustomValidity("");
传递一个空字符串可以使输入有效(除非应用了其他约束条件)。
基本上就是这样了!现在让我们运用这些知识吧。
我们假设要实现一个与 required
属性等效的功能。这意味着必须阻止用户提交空的表单。
<input
name="example"
placeholder="..."
onChange={(event) => {
const input = event.currentTarget;
if (input.value === "") {
input.setCustomValidity("Custom message: input is empty");
} else {
input.setCustomValidity("");
}
}}
/>
这种看起来像是已经完成任务了,这些代码应该足以实现所需的功能。但试着看看它的实际运行情况:
这似乎可行,但有一个重要的特殊情况:输入最初处于有效状态。如果你重置组件并按下 “提交” 按钮,表单提交将通过。但是,在我们触及输入之前,它肯定是空的,因此必须是无效的。但我们只在输入值发生变化时执行某些操作。
我们该如何解决这个问题呢?
当组件挂载时,让我们执行一些代码:
import { useRef, useLayoutEffect } from "react";
function Form() {
const ref = useRef();
useLayoutEffect(() => {
// Make input invalid on initial render if it's empty
const input = ref.current;
const empty = input.value === "";
input.setCustomValidity(empty ? "Initial message: input is empty" : "");
}, []);
return (
<form>
<input
ref={ref}
name="example"
onChange={(event) => {
const input = event.currentTarget;
if (input.value === "") {
input.setCustomValidity("Custom message: input is empty");
} else {
input.setCustomValidity("");
}
}}
/>
<button>Submit</button>
</form>
);
}
太棒了!现在一切如预期般运行。但付出的代价是什么?
模板问题
让我们来看看我们笨拙的初始值验证方法:
const ref = useRef();
useLayoutEffect(() => {
// Make input invalid on initial render if it's empty
const input = ref.current;
const empty = input.value !== "";
input.setCustomValidity(empty ? "Initial message: input is empty" : "");
}, []);
哎呀!可不想每次都写这个。我们来想想这个有什么问题吧。
验证逻辑在
onChange
处理程序和初始渲染阶段之间被重复使用了。初始验证代码与输入代码不在同一位置,因此我们正在失去代码的内聚性。它很脆弱的:如果你更新验证逻辑,你可能会忘记在两个位置更新代码。
这种
useRef
+useLayouEffect
+onChange
组合太过繁琐,尤其是当表单有很多输入项时。如果只有部分输入项使用customValidity
,情况会更加混乱。
这是当你在声明式组件中处理纯命令式 API 时,就会出现这种情况。
与验证属性不同,
CustomValidity
是一个纯粹的命令式 API。换句话说,我们没有可以用来设置自定义有效性的输入属性。
实际上,我可以断言,这是原生表单验证未能得到广泛应用的主要原因。如果 API 使用起来很麻烦,那么它的功能再强大也无济于事。
缺失的部分
归根到底,这就是我们需要的属性:
<input custom-validity="error message" />
在声明性框架中,可以以一种非常强大的方式定义输入验证:
function Form() {
const [value, setValue] = useState();
const handleChange = (event) => setValue(event.target.value);
return (
<form>
<input
name="example"
value={value}
onChange={handleChange}
custom-validity={value.length ? "Fill out this field" : ""}
/>
<button>Submit</button>
</form>
);
}
太酷了!至少在我看来是这样。不过你也可以有理有据地指出,这只是实现了现有的 required
属性已经具备的功能。那 “力量” 又在哪里呢?
让我来演示一下,不过目前 HTML 规范中没有实际的 custom-validity
标签 ,让我们在用户端实现它。
function Input({ customValidity, ...props }) {
const ref = useRef();
useLayoutEffect(() => {
if (customValidity != null) {
const input = ref.current;
input.setCustomValidity(customValidity);
}
}, [customValidity]);
return <input ref={ref} {...props} />;
}
这对我们的演示很有帮助。
对于一个准备投入生产的组件,请查看一个更完整的实现方案。https://gist.github.com/everdimension/a5c1e991a8a6b6aab060ce349b37b825
力量
现在我们来探讨这种设计可以帮助解决哪些非平凡问题。
在实际应用中,验证往往比本地检查更为复杂。想象一下一个用户名输入框,只有在用户名未被占用的情况下才应为有效。这需要异步调用到你的服务器,并且需要一个中间状态:在验证过程中,表单不应被认为是有效的。让我们看看我们的抽象如何处理这种情况。
可以尝试修改这个示例。它使用了 required
来防止输入为空。但随后它又依赖于 customValidity
在加载状态时标记无效输入,并根据响应来实现。
实现
首先,我们创建一个异步函数来检查用户名是否唯一,该函数模仿带有延迟的服务器请求。
export async function verifyUsername(userValue) {
// imitate network delay
await new Promise((r) => setTimeout(r, 3000));
const value = userValue.trim().toLowerCase();
if (value === "bad input") {
throw new Error("Bad Input");
}
const validationMessage = value === "taken" ? "Username is taken" : "";
return { validationMessage };
}
接下来,我们将创建一个受控表单组件,并在输入值更改时使用 react-query
管理对服务器的请求:
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { verifyUsername } from "./verifyUsername";
import { Input } from "./Input";
function Form() {
const [value, setValue] = useState("");
const { data, isLoading, isError } = useQuery({
queryKey: ["verifyUsername", value],
queryFn: () => verifyUsername(value),
enabled: Boolean(value),
});
return (
<form>
<Input
name="username"
required={true}
value={value}
onChange={(event) => {
setValue(event.currentTarget.value);
}}
/>
<button>Submit</button>
</form>
);
}
太棒了!我们已经做好准备嘞。它由两部分关键组成:
由 useQuery 管理的验证请求状态
我们的自定义
<Input />
组件,能够接受customValidity
属性
让我们把这些信息整合起来:
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { verifyUsername } from "./verifyUsername";
import { Input } from "./Input";
function Form() {
const [value, setValue] = useState("");
const { data, isLoading, isError } = useQuery({
queryKey: ["verifyUsername", value],
queryFn: () => verifyUsername(value),
enabled: Boolean(value),
});
const validationMessage = data?.validationMessage;
return (
<form>
<Input
name="username"
required={true}
customValidity={
isLoading
? "Verifying username..."
: isError
? "Could not verify"
: validationMessage
}
value={value}
onChange={(event) => {
setValue(event.currentTarget.value);
}}
/>
<button>Submit</button>
</form>
);
}
这就完成了!我们将整个异步验证流程(包括加载、错误和成功状态)都包含在一个属性中。如果您愿意,可以再次查看结果。
再来一个
这个例子会比较短,但也很有意思,因为它涉及到依赖输入字段的情况。我们来实现一个需要重复输入密码的表单:
import { useState } from "react";
import { Input } from "./Input";
function ConfirmPasswordForm() {
const [password, setPassword] = useState("");
const [confirmedPass, setConfirmedPass] = useState("");
const matches = confirmedPass === password;
return (
<form>
<Input
type="password"
name="password"
required={true}
value={password}
onChange={(event) => {
setPassword(event.currentTarget.value);
}}
/>
<Input
type="password"
name="confirmedPassword"
required={true}
value={confirmedPass}
customValidity={matches ? "" : "Password must match"}
onChange={(event) => {
setConfirmedPass(event.currentTarget.value);
}}
/>
<button>Submit</button>
</form>
);
}
你可以试一试:
结论
我希望我已经向你展示了 setCustomValidity
如何满足各种验证需求。
但真正的力量来自于优秀的 API。
希望你现在已经拥有其中一种工具了。
更令人期待的是,有朝一日它可能会被直接纳入 HTML 规范中。
一个示例:https://codepen.io/dmack/pen/QbmgVv
关于本文
译者:@飘飘
作者:@everdimension
原文:https://expressionstatement.com/html-form-validation-is-heavily-underused
这期前端早读课
对你有帮助,帮” 赞 “一下,
期待下一期,帮” 在看” 一下 。