专栏名称: 前端早读课
我们关注前端,产品体验设计,更关注前端同行的成长。 每天清晨五点早读,四万+同行相伴成长。
目录
相关文章推荐
奇舞精选  ·  vercel是如何做微前端迁移的 ·  昨天  
奇舞精选  ·  vercel是如何做微前端迁移的 ·  昨天  
前端大全  ·  预测一波,最近前端即将起飞! ·  5 天前  
光伏资讯  ·  来看看曲面屋顶的工商业光伏安装现场! ·  5 天前  
光伏资讯  ·  来看看曲面屋顶的工商业光伏安装现场! ·  5 天前  
前端之巅  ·  Shopify将应用迁移到React ... ·  6 天前  
Seebug漏洞平台  ·  原创 Paper | CodeQL 入门和基本使用 ·  1 周前  
Seebug漏洞平台  ·  原创 Paper | CodeQL 入门和基本使用 ·  1 周前  
51好读  ›  专栏  ›  前端早读课

【第3418期】HTML 表单验证:未被充分利用的利器

前端早读课  · 公众号  · 前端  · 2024-11-18 08:00

正文

前言

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>Submitbutton>
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>Submitbutton>
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>Submitbutton>
form>
);
}

太棒了!我们已经做好准备嘞。它由两部分关键组成:

  • 由 useQuery 管理的验证请求状态

  • 我们的自定义  组件,能够接受 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>Submitbutton>
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>Submitbutton>
form>
);
}

你可以试一试:

结论

我希望我已经向你展示了 setCustomValidity 如何满足各种验证需求。

但真正的力量来自于优秀的 API。

希望你现在已经拥有其中一种工具了。

更令人期待的是,有朝一日它可能会被直接纳入 HTML 规范中。

一个示例:https://codepen.io/dmack/pen/QbmgVv

关于本文
译者:@飘飘
作者:@everdimension
原文:https://expressionstatement.com/html-form-validation-is-heavily-underused

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。