Posted on:
Last modified:
表单元素和所有其他元素的不同在于:表单元素是输入组件,有内部状态,而其他元素都是输出元素, 并不需要内部状态。原生 HTML 表单在 React 中也能用,但一般情况下,我们会交给一个函数来处理 提交表单。由这个函数负责验证表单等操作,这时我们可以用"受控组件"。
受控组件中,数据不保存在 DOM 中,而是始终在 JS 中,随时通过 value/onChange
读取验证。
非受控组件数据依然保存在 DOM 树中,只在 submit 时候读取。
function MyForm(props) {
value, setValue = useState("");
function handleChange(e) {
setValue(e.target.value)
}
function handleSubmit(e) {
// 直接读取 js 中保存的 state.value
console.log("A name was submitted: " + value);
axios.post("/api/myresource", {value});
e.preventDefault();
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" value={value} onChange={onChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
如果完全采用非受控的方式,只在最后的 onSubmit 中访问 form,局限性还是很大的。比如说,好些 时候我们甚至并不想要一个完整的 form。
用 ref 来引用一个 input 是介于受控和非受控之间的一种方式。在这种模式下,input 每次变化的 值依然存储在 DOM 中,但是也可以在 EventHandler 中方便得访问到。
这其实有点类似 jQuery 的 $("#name") 方式读取值
import {useRef} from 'react';
const App = () => {
const inputRef = useRef(null);
function handleClick() {
console.log(inputRef.current.value);
// 还可以直接设置
input.current.value = ""
}
return (
<div>
<input
ref={inputRef}
type="text"
id="message"
name="message"
/>
<button onClick={handleClick}>Log message</button>
</div>
);
};
也可以使用原生组件,而不直接使用受控组件。
function form2kv(form) {
const fd = new FormData(form);
const ret = {};
for (let key of fd.keys()) {
ret[key] = fd.get(key);
}
return ret;
}
function MyForm(props) {
// 也可以直接通过读取 events.target 的值来获取
function handleSubmit1(e) {
e.preventDefault();
console.log(e.target.elements.username.value) // from elements property
console.log(e.target.username.value) // or directly
}
function handleSubmit2(e) {
e.preventDefault();
const data = form2kv(e.target);
axios.post("/api/myresource", data);
}
return (
<form onSubmit={handleSubmit2}>
<label htmlFor="username">Enter username</label>
<input type="text" id="username" name="username">
<label htmlFor="password">Enter password</label>
<input type="password" id="password" name="password">
<label htmlFor="birthdate">Enter your birth date</label>
<input type="date" id="birthdate" name="birthdate">
<button type="submit">Send data!</button>
</form>
)
}
在上面的例子中,我们使用了浏览器自带的 Formdata
对象,通过传入 DOM 中的 Form 元素来
读取其中的数据。另外需要注意一点,我们使用了 onSubmit
事件,而不是 Submit 按钮的
onClick
事件,这样保留了浏览器的原生激活方式,也就是可以使用回车键提交。
input type=date 接受的默认值是 YYYY-MM-DD 类型的
<input defalutValue={new Date(date).toISOString().split('T')[0]} />
如果当我们需要对数据做一些后处理时呢?比如:
这时候可以使用另一个小技巧,data-x
属性, 在 data 中存储要使用的处理方法。
const inputParsers = {
date(input) {
const [month, day, year] = input.split('/');
return `${year}-${month}-${day}`;
},
uppercase(input) {
return input.toUpperCase();
},
number(input) {
return parseFloat(input);
},
};
function form2kv(form, parsers = {}) {
const fd = new FormData(form);
const ret = {};
for (let name of data.keys()) {
const input = form.elements[name];
const parseName = input.dataset.parse;
if (parseName) {
const parser = inputParsers[parseName];
const parsedValue = parser(data.get(name));
ret[name] = parsedValue;
} else {
ret[name] = data.get(name);
}
}
return ret;
}
function MyForm(props) {
function handleSubmit(e) {
e.preventDefault();
const data = form2kv(e.target);
fetch("api/myresource", method="POST", body=JSON.stringify(data));
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="username">Enter username</label>
<input type="text" id="username" name="username" data-parse="uppercase">
<label htmlFor="password">Enter password</label>
<input type="password" id="password" name="password">
<label htmlFor="birthdate">Enter your birth date</label>
<input type="date" id="birthdate" name="birthdate" data-parse="date">
<button type="submit">Send data!</button>
</form>
)
}
在上面的例子中,我们使用 data-x 属性指定了应该使用的处理函数。通过表单的 elements 属性,
我们按照 input 的 name 访问元素。而节点的 data-x 属性可以在 dataset[x]
属性中访问。
HTML 的原生 API 就支持了很多的验证属性,其中最最好用的就是 pattern 属性了,他可以指定一个 正则来验证表单。而且我们也可以通过 type 属性来指定 email 等特殊的 text 类型。HTML 支持的 验证属性有:
在 JS 中可以调用 form.checkValidity()
函数获得检查结果。最好在 form 中添加 novalidate
属性,否则浏览器就会在校验失败的时候直接不触发 submit 事件了。
当表单验证不通过时,input 会获得一个 :invalid
的伪类。但是比较坑爹的是,当表单还没有
填写的时候,验证就已经生效了,这样对用户非常不友好,我们可以设定一个类,比如 display-errors
,
只有当用户尝试提交表单之后再应用这个类就好啦~
displayErrors, setDisplayErrors = useState(false);
function handleSubmit(e) {
if (!e.target.checkValidity()) {
setDisplayError(true);
}
}
return (
<form
onSubmit={handleSubmit}
noValidate // 即使校验失败也要触发 submit event,这样控制权才会到 JS 中
className={displayErrors ? 'displayErrors' : ''}
>
{/* ... */}
</form>
)
.displayErrors input:invalid {
border-color: red;
}
对于更复杂的逻辑,推荐使用 react-hook-form
react-hook-form v7 中使用了未受控组件,这样性能更好。结合 tailwind UI,给原生组件写样式 也更方便一点。RHF 可以使用 html 原生的验证,还可以配合使用 Yup 等第三方库来做表单验证。
...register("name", {options})
用于指定input 的属性import React from "react";
import { useForm, SubmitHandler } from "react-hook-form";
interface FormValues {
firstName string,
lastName string,
email string,
}
export default function App() {
const { register, handleSubmit, formState, setValue, trigger } = useForm<FormValues>({
defaultValues: {
firstName: "foo",
lastName: "bar",
email: "foobar@example.com"
}
});
const onSubmit: SubmitHandler<FormValuse> = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<input {...register("email")} />
<button type="submit">Submit</button>
</form>
)
}
当用户提交特别快,而网速不够快的时候,可能会产生重复提交的现象。使用 RHF 可以很简单得防止 这种情况。
import {useForm} from 'react-hook-form'
export default function App() {
- const {handleSubmit, register} - useForm({})
+ const {handleSubmit, register, formState} - useForm({})
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<input {...register("email")} />
- <button type="submit">Submit</button>
+ <button type="submit" disabled={formState.isSubmitting}>Submit</button>
</form>
)
}
不过,前端阻止用户重复提交只是交互上更加友好,从安全角度来说很容易绕过。因此,还需要在后端 也采取防止重复提交的措施,这样在逻辑上才比较完备。
© 2016-2022 Yifei Kong. Powered by ynotes
All contents are under the CC-BY-NC-SA license, if not otherwise specified.
Opinions expressed here are solely my own and do not express the views or opinions of my employer.
友情链接: MySQL 教程站