内容简介:在 React 中使用表单有个明显的痛点,就是需要维护大量的这已经是比较简单的登录页,一些涉及到详情编辑的页面,十多二十个组件也是常有的。一旦组件多起来就会有许多弊端:总结起来,作为一个开发者,迫切希望能有一个表单组件能够同时拥有这样的特性:
在 React 中使用表单有个明显的痛点,就是需要维护大量的 value
和 onChange
,比如一个简单的登录框:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
username: "",
password: ""
};
}
onUsernameChange = e => {
this.setState({ username: e.target.value });
};
onPasswordChange = e => {
this.setState({ password: e.target.value });
};
onSubmit = () => {
const data = this.state;
// ...
};
render() {
const { username, password } = this.state;
return (
<form onSubmit={this.onSubmit}>
<input value={username} onChange={this.onUsernameChange} />
<input
type="password"
value={password}
onChange={this.onPasswordChange}
/>
<button>Submit</button>
</form>
);
}
}
复制代码
这已经是比较简单的登录页,一些涉及到详情编辑的页面,十多二十个组件也是常有的。一旦组件多起来就会有许多弊端:
setState
总结起来,作为一个开发者,迫切希望能有一个表单组件能够同时拥有这样的特性:
- 简单易用
- 父组件可通过代码操作表单数据
- 避免不必要的组件重绘
- 支持自定义组件
- 支持表单校验
表单组件社区上已经有不少方案,例如 react-final-form 、 formik , ant-plus 、 noform 等,许多组件库也提供了不同方式的支持,如 ant-design 。
但这些方案都或多或少一些重量,又或者使用方法仍然不够简便,自然造轮子才是最能复合要求的选择。
怎么造轮子
这个表单组件实现起来主要分为三部分:
-
Form:用于传递表单上下文。 -
Field: 表单域组件,用于自动传入value和onChange到表单组件。 -
FormStore: 存储表单数据,封装相关操作。
为了能减少使用 ref
,同时又能操作表单数据(取值、修改值、手动校验等),我将用于存储数据的 FormStore
,从 Form
组件中分离出来,通过 new FormStore()
创建并手动传入 Form
组件。
使用方式大概会长这样子:
class App extends React.Component {
constructor(props) {
super(props);
this.store = new FormStore();
}
onSubmit = () => {
const data = this.store.get();
// ...
};
render() {
return (
<Form store={this.store} onSubmit={this.onSubmit}>
<Field name="username">
<input />
</Field>
<Field name="password">
<input type="password" />
</Field>
<button>Submit</button>
</Form>
);
}
}
复制代码
FormStore
用于存放表单数据、接受表单初始值,以及封装对表单数据的操作。
class FormStore {
constructor(defaultValues = {}, rules = {}) {
// 表单值
this.values = defaultValues;
// 表单初始值,用于重置表单
this.defaultValues = deepCopy(defaultValues);
// 表单校验规则
this.rules = rules;
// 事件回调
this.listeners = [];
}
}
复制代码
为了让表单数据变动时,能够响应到对应的表单域组件,这里使用了订阅方式,在 FormStore
中维护一个事件回调列表 listeners
,每个 Field
创建时,通过调用 FormStore.subscribe(listener)
订阅表单数据变动。
class FormStore {
// constructor ...
subscribe(listener) {
this.listeners.push(listener);
// 返回一个用于取消订阅的函数
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) this.listeners.splice(index, 1);
};
}
// 通知表单变动,调用所有listener
notify(name) {
this.listeners.forEach(listener => listener(name));
}
}
复制代码
再添加 get
和 set
函数,用于获取和设置表单数据。其中,在 set
函数中调用 notify(name)
,以保证所有的表单变动都会触发通知。
class FormStore {
// constructor ...
// subscribe ...
// notify ...
// 获取表单值
get(name) {
// 如果传入name,返回对应的表单值,否则返回整个表单的值
return name === undefined ? this.values : this.values[name];
}
// 设置表单值
set(name, value) {
//如果指定了name
if (typeof name === "string") {
// 设置name对应的值
this.values[name] = value;
// 执行表单校验,见下
this.validate(name);
// 通知表单变动
this.notify(name);
}
// 批量设置表单值
else if (name) {
const values = name;
Object.keys(values).forEach(key => this.set(key, values[key]));
}
}
// 重置表单值
reset() {
// 清空错误信息
this.errors = {};
// 重置默认值
this.values = deepCopy(this.defaultValues);
// 执行通知
this.notify("*");
}
}
复制代码
对于表单校验部分,不想考虑得太复杂,只做一些规定
-
FormStore构造函数中传入的rules是一个对象,该对象的键对应于表单域的name,值是一个校验函数。 -
校验函数参数接受表单域的值和整个表单值,返回boolean或string类型的结果。
-
true代表校验通过。 -
false和string代表校验失败,并且string结果代表错误信息。
然后巧妙地通过 ||
符号判断是否校验通过,例如:
new FormStore({/* 初始值 */, {
username: (val) => !!val.trim() || '用户名不能为空',
password: (val) => !!(val.length > 6 && val.length < 18) || '密码长度必须大于6个字符,小于18个字符',
passwordAgain: (val, vals) => val === vals.password || '两次输入密码不一致'
}})
复制代码
在 FormStore
实现一个 validate
函数:
class FormStore {
// constructor ...
// subscribe ...
// notify ...
// get
// set
// reset
// 用于设置和获取错误信息
error(name, value) {
const args = arguments;
// 如果没有传入参数,则返回错误信息中的第一条
// const errors = store.error()
if (args.length === 0) return this.errors;
// 如果传入的name是number类型,返回第i条错误信息
// const error = store.error(0)
if (typeof name === "number") {
name = Object.keys(this.errors)[name];
}
// 如果传了value,则根据value值设置或删除name对应的错误信息
if (args.length === 2) {
if (value === undefined) {
delete this.error[name];
} else {
this.errors[name] = value;
}
}
// 返回错误信息
return this.errors[name];
}
// 用于表单校验
validate(name) {
if (name === undefined) {
// 遍历校验整个表单
Object.keys(this.rules).forEach(n => this.validate(n));
// 并通知整个表单的变动
this.notify("*");
// 返回一个包含第一条错误信息和表单值的数组
return [this.error(0), this.get()];
}
// 根据name获取校验函数
const validator = this.rules[name];
// 根据name获取表单值
const value = this.get(name);
// 执行校验函数得到结果
const result = validator ? validator(name, this.values) : true;
// 获取并设置结果中的错误信息
const message = this.error(
name,
result === true ? undefined : result || ""
);
// 返回Error对象或undefind,和表单值
const error = message === undefined ? undefined : new Error(message);
return [error, value];
}
}
复制代码
至此,这个表单组件的核心部分 FormStore
已经完成了,接下来就是这么在 Form
和 Field
组件中使用它。
Form
Form
组件相当简单,也只是为了提供一个入口和传递上下文。
props
接收一个 FormStore
的实例,并通过 Context
传递给子组件(即 Field
)中。
const FormStoreContext = React.createContext();
function Form(props) {
const { store, children, onSubmit } = props;
return (
<FormStoreContext.Provider value={store}>
<form onSubmit={onSubmit}>{children}</form>
</FormStoreContext.Provider>
);
}
复制代码
Field
Field
组件也并不复杂,核心目标是实现 value
和 onChange
自动传入到表单组件中。
// 从onChange事件中获取表单值,这里主要应对checkbox的特殊情况
function getValueFromEvent(e) {
return e && e.target
? e.target.type === "checkbox"
? e.target.checked
: e.target.value
: e;
}
function Field(props) {
const { label, name, children } = props;
// 拿到Form传下来的FormStore实例
const store = React.useContext(FormStoreContext);
// 组件内部状态,用于触发组件的重新渲染
const [value, setValue] = React.useState(
name && store ? store.get(name) : undefined
);
const [error, setError] = React.useState(undefined);
// 表单组件onChange事件,用于从事件中取得表单值
const onChange = React.useCallback(
(...args) => name && store && store.set(name, valueGetter(...args)),
[name, store]
);
// 订阅表单数据变动
React.useEffect(() => {
if (!name || !store) return;
return store.subscribe(n => {
// 当前name的数据发生了变动,获取数据并重新渲染
if (n === name || n === "*") {
setValue(store.get(name));
setError(store.error(name));
}
});
}, [name, store]);
let child = children;
// 如果children是一个合法的组件,传入value和onChange
if (name && store && React.isValidElement(child)) {
const childProps = { value, onChange };
child = React.cloneElement(child, childProps);
}
// 表单结构,具体的样式就不贴出来了
return (
<div className="form">
<label className="form__label">{label}</label>
<div className="form__content">
<div className="form__control">{child}</div>
<div className="form__message">{error}</div>
</div>
</div>
);
}
复制代码
于是,这个表单组件就完成了,愉快地使用它吧:
class App extends React.Component {
constructor(props) {
super(props);
this.store = new FormStore();
}
onSubmit = () => {
const data = this.store.get();
// ...
};
render() {
return (
<Form store={this.store} onSubmit={this.onSubmit}>
<Field name="username">
<input />
</Field>
<Field name="password">
<input type="password" />
</Field>
<button>Submit</button>
</Form>
);
}
}
复制代码
结语
这里只是把最核心的代码整理了出来,功能上当然比不上那些成百上千 star 的组件,但是用法上足够简单,并且已经能应对项目中的大多数情况。
我已在此基础上完善了一些细节,并发布了一个 npm 包—— @react-hero/form
,你可以通过npm安装,或者在 github
上找到源码。如果你有任何已经或建议,欢迎在评论或 issue 中讨论。
以上所述就是小编给大家介绍的《React 实现一个简单实用的 Form 组件》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 从零实现Vue的组件库(二)-Slider组件实现
- Vue自定义组件(简单实现一个自定义组件)
- 实现一个沙漏⏳组件
- angular 实现下拉列表组件
- 从零实现Vue的组件库(五)- Breadcrumb 实现
- 从零实现Vue的组件库(十)- Select 实现
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Rationality for Mortals
Gerd Gigerenzer / Oxford University Press, USA / 2008-05-02 / USD 65.00
Gerd Gigerenzer's influential work examines the rationality of individuals not from the perspective of logic or probability, but from the point of view of adaptation to the real world of human behavio......一起来看看 《Rationality for Mortals》 这本书的介绍吧!
Markdown 在线编辑器
Markdown 在线编辑器
HEX CMYK 转换工具
HEX CMYK 互转工具