We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
在 React 中使用表单有个明显的痛点,就是需要维护大量的value和onChange,比如一个简单的登录框:
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
FormStore
为了能减少使用ref,同时又能操作表单数据(取值、修改值、手动校验等),我将用于存储数据的FormStore,从Form组件中分离出来,通过new FormStore()创建并手动传入Form组件。
ref
new FormStore()
使用方式大概会长这样子:
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> ); } }
用于存放表单数据、接受表单初始值,以及封装对表单数据的操作。
class FormStore { constructor(defaultValues = {}, rules = {}) { // 表单值 this.values = defaultValues; // 表单初始值,用于重置表单 this.defaultValues = deepCopy(defaultValues); // 表单校验规则 this.rules = rules; // 事件回调 this.listeners = []; } }
为了让表单数据变动时,能够响应到对应的表单域组件,这里使用了订阅方式,在FormStore中维护一个事件回调列表listeners,每个Field创建时,通过调用FormStore.subscribe(listener)订阅表单数据变动。
listeners
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),以保证所有的表单变动都会触发通知。
get
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("*"); } }
对于表单校验部分,不想考虑得太复杂,只做一些规定
rules
name
校验函数
boolean
string
true
false
然后巧妙地通过||符号判断是否校验通过,例如:
||
new FormStore({/* 初始值 */, { username: (val) => !!val.trim() || '用户名不能为空', password: (val) => !!(val.length > 6 && val.length < 18) || '密码长度必须大于6个字符,小于18个字符', passwordAgain: (val, vals) => val === vals.password || '两次输入密码不一致' }})
在FormStore实现一个validate函数:
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.errors[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组件相当简单,也只是为了提供一个入口和传递上下文。
props接收一个FormStore的实例,并通过Context传递给子组件(即Field)中。
props
Context
const FormStoreContext = React.createContext(undefined); function Form(props) { const { store, children, onSubmit } = props; return ( <FormStoreContext.Provider value={store}> <form onSubmit={onSubmit}>{children}</form> </FormStoreContext.Provider> ); }
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( name && store ? store.error(name) : 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> ); }
于是,这个表单组件就完成了,愉快地使用它吧:
这里只是把最核心的代码整理了出来,功能上当然比不上那些成百上千 star 的组件,但是用法上足够简单,并且已经能应对项目中的大多数情况。
我已在此基础上完善了一些细节,并发布了一个 npm 包——@react-hero/form,你可以通过npm安装,或者在github上找到源码。如果你有任何已经或建议,欢迎在评论或 issue 中讨论。
@react-hero/form
The text was updated successfully, but these errors were encountered:
No branches or pull requests
为什么要造轮子
在 React 中使用表单有个明显的痛点,就是需要维护大量的
value
和onChange
,比如一个简单的登录框:这已经是比较简单的登录页,一些涉及到详情编辑的页面,十多二十个组件也是常有的。一旦组件多起来就会有许多弊端:
setState
的使用,会导致重新渲染,如果子组件没有相关优化,相当影响性能。总结起来,作为一个开发者,迫切希望能有一个表单组件能够同时拥有这样的特性:
表单组件社区上已经有不少方案,例如react-final-form、formik,ant-plus、noform等,许多组件库也提供了不同方式的支持,如ant-design。
但这些方案都或多或少一些重量,又或者使用方法仍然不够简便,自然造轮子才是最能复合要求的选择。
怎么造轮子
这个表单组件实现起来主要分为三部分:
Form
:用于传递表单上下文。Field
: 表单域组件,用于自动传入value
和onChange
到表单组件。FormStore
: 存储表单数据,封装相关操作。为了能减少使用
ref
,同时又能操作表单数据(取值、修改值、手动校验等),我将用于存储数据的FormStore
,从Form
组件中分离出来,通过new FormStore()
创建并手动传入Form
组件。使用方式大概会长这样子:
FormStore
用于存放表单数据、接受表单初始值,以及封装对表单数据的操作。
为了让表单数据变动时,能够响应到对应的表单域组件,这里使用了订阅方式,在
FormStore
中维护一个事件回调列表listeners
,每个Field
创建时,通过调用FormStore.subscribe(listener)
订阅表单数据变动。再添加
get
和set
函数,用于获取和设置表单数据。其中,在set
函数中调用notify(name)
,以保证所有的表单变动都会触发通知。对于表单校验部分,不想考虑得太复杂,只做一些规定
FormStore
构造函数中传入的rules
是一个对象,该对象的键对应于表单域的name
,值是一个校验函数
。校验函数
参数接受表单域的值和整个表单值,返回boolean
或string
类型的结果。true
代表校验通过。false
和string
代表校验失败,并且string
结果代表错误信息。然后巧妙地通过
||
符号判断是否校验通过,例如:在
FormStore
实现一个validate
函数:至此,这个表单组件的核心部分
FormStore
已经完成了,接下来就是这么在Form
和Field
组件中使用它。Form
Form
组件相当简单,也只是为了提供一个入口和传递上下文。props
接收一个FormStore
的实例,并通过Context
传递给子组件(即Field
)中。Field
Field
组件也并不复杂,核心目标是实现value
和onChange
自动传入到表单组件中。于是,这个表单组件就完成了,愉快地使用它吧:
结语
这里只是把最核心的代码整理了出来,功能上当然比不上那些成百上千 star 的组件,但是用法上足够简单,并且已经能应对项目中的大多数情况。
我已在此基础上完善了一些细节,并发布了一个 npm 包——
@react-hero/form
,你可以通过npm安装,或者在github上找到源码。如果你有任何已经或建议,欢迎在评论或 issue 中讨论。The text was updated successfully, but these errors were encountered: