上一篇我们实现一个最基本的表单验证。
如果我们想要再写一个添加图书的功能,以同样的方式去写就会发现会出现很多重复的代码。因为所有的验证代码都是耦合在组件中的,没有办法实现复用。
重复代码是混乱的根源!
既然我们用了React,那我们是不是可以用组件化的方式去给表单验证这些逻辑解耦呢?
什么是高阶组件?
高阶组件就是返回组件的组件(函数)
为什么要通过一个组件去返回另一个组件?
使用高阶组件可以在不修改原组件代码的情况下,修改原组件的行为或增强功能。
我们现在已经有了带有表单校验功能的添加用户的表单,这里的表单有3个字段:name、age、gender,并且每个字段都有它自己的校验规则和对应的错误信息。
要做一个添加图书的功能,图书的表单有name、price、owner_id三个字段,一样地,每个字段有它自己的校验规则和错误信息。
仔细想想,每当我们需要写一个表单的时候,都需要有一个地方来保存表单字段的值(state),有一个函数来处理表单值的更新和校验(handleValueChange),这些东西我们可以用高阶组件来封装。
而添加用户的表单和添加图书的表单之间的不同之处仅仅是表单字段以及字段的默认值、校验规则和错误信息。
那么我们的高阶组件模型就出来了:
function formProvider (fields) { return function (Comp) { constructor (props) { super(props); this.state = { form: {...}, formValid: false // 加了一个formValid用来保存整个表单的校验状态 }; } handleValueChange (field, value) {...} class FormComponent extends React.Component { render () { const {form, formValid} = this.state; return ( <comp form="{form}" formvalid="{formValid}" data-cke-pa-onformchange="{this.handleValueChange}/"> ); } } return FormComponent; } }
formProvider接收一个fields参数,并返回一个函数,这个函数接收一个组件作为参数并返回一个组件,所以它的用法是这样的:
UserAdd = formProvider(fields)(UserAdd);
经过formProvider处理后的UserAdd组件会得到额外的props:
form formValid onFormChange在/src下新建一个目录utils,新建formProvider.js文件,写入具体的代码实现:
import React from 'react'; function formProvider (fields) { return function (Comp) { const initialFormState = {}; for (const key in fields) { initialFormState[key] = { value: fields[key].defaultValue, error: '' }; } class FormComponent extends React.Component { constructor (props) { super(props); this.state = { form: initialFormState, formValid: false }; this.handleValueChange = this.handleValueChange.bind(this); } handleValueChange (fieldName, value) { const { form } = this.state; const fieldState = form[fieldName]; const newFieldState = {...fieldState, value, valid: true, error: ''}; const fieldRules = fields[fieldName].rules; for (let i = 0; i < fieldRules.length; i++) { if (!fieldRules[i].pattern.test(value)) { newFieldState.valid = false; newFieldState.error = fieldRules[i].error; break; } } const newForm = {...form, [fieldName]: newFieldState}; const formValid = Object.values(newForm).every(f => f.valid); this.setState({ form: newForm, formValid }); } render () { const {form, formValid} = this.state; return <comp form="{form}" formvalid="{formValid}" data-cke-pa-onformchange="{this.handleValueChange}/"> } } return FormComponent; } } export default formProvider;
formProvider的第一个参数fields是一个对象,其结构为:
// 表示表单中有name、age、gender3个字段 const fields = { name: { defaultValue: '', rules: [ { // pattern用于对值进行校验,可以为方法或一个RegExp对象 // 若方法的返回值为一个真值或RegExp.test(value)返回true则校验通过 pattern: function (value) { return value.length > 0; }, // 每个pattern对应一个error信息 error: '请输入用户名' }, { pattern: /^.{1,4}$/, error: '用户名最多4个字符' } ] }, age: {...}, gender: {...} }
然后UserAdd.js就可以改成这个样子了:
import React from 'react'; import formProvider from '../utils/formProvider'; class UserAdd extends React.Component { handleSubmit (e) { e.preventDefault(); const {form: {name, age, gender}, formValid} = this.props; if (!formValid) { alert('请填写正确的信息后重试'); return; } fetch('http://localhost:3000/user', { method: 'post', body: JSON.stringify({ name: name.value, age: age.value, gender: gender.value }), headers: { 'Content-Type': 'application/json' } }) .then((res) => res.json()) .then((res) => { if (res.id) { alert('添加用户成功'); this.setState({ name: '', age: 0, gender: '' }); } else { alert('添加失败'); } }) .catch((err) => console.error(err)); } render () { const {form: {name, age, gender}, onFormChange} = this.props; return ( <div> <header> <h1>添加用户</h1> </header> <main> <form onSubmit={(e) => this.handleSubmit(e)}> <label>用户名:</label> <input type="text" value={name.value} onChange={(e) => onFormChange('name', e.target.value)} /> {!name.valid && <span>{name.error}</span>} <br/> <label>年龄:</label> <input type="number" value={age.value || ''} onChange={(e) => onFormChange('age', +e.target.value)} /> {!age.valid && <span>{age.error}</span>} <br/> <label>性别:</label> <select value={gender.value} onChange={(e) => onFormChange('gender', e.target.value)} > <option value="">请选择</option> <option value="male">男</option> <option value="female">女</option> </select> {!gender.valid && <span>{gender.error}</span>} <br/> <br/> <input type="submit" value="提交"/> </form> </main> </div> ); } } UserAdd = formProvider({ name: { defaultValue: '', rules: [ { pattern: function (value) { return value.length > 0; }, error: '请输入用户名' }, { pattern: /^.{1,4}$/, error: '用户名最多4个字符' } ] }, age: { defaultValue: 0, rules: [ { pattern: function (value) { return value >= 1 && value <= 100; }, error: '请输入1~100的年龄' } ] }, gender: { defaultValue: '', rules: [ { pattern: function (value) { return !!value; }, error: '请选择性别' } ] } })(UserAdd); export default UserAdd;
上面我们抽离了表单的状态的维护和更新逻辑,但这并不够完美。
在UserAdd.js里的render方法中,我们可以看到还存在着一些重复的代码:
... <label>用户名:</label> <input type="text" value={name.value} onChange={(e) => onFormChange('name', e.target.value)} /> {!name.valid && <span>{name.error}</span>} <br/> <label>年龄:</label> <input type="number" value={age.value || ''} onChange={(e) => onFormChange('age', +e.target.value)} /> {!age.valid && <span>{age.error}</span>} <br/> <label>性别:</label> <select value={gender.value} onChange={(e) => onFormChange('gender', e.target.value)} > <option value="">请选择</option> <option value="male">男</option> <option value="female">女</option> </select> {!gender.valid && <span>{gender.error}</span>} <br/> ...
每一个表单控件都包含一个label、一个具体的控件元素、一个根据valid来控制显示的span元素。
我们可以将其封装成一个FormItem组件,新建/src/components目录和FormItem.js文件,写入以下代码:
import React from 'react'; class FormItem extends React.Component { render () { const {label, children, valid, error} = this.props; return ( <div> <label>{label}</label> {children} {!valid && <span>{error}</span>} </div> ); } } export default FormItem;
在UserAdd.js中使用FormItem组件:
import React from 'react'; import FormItem from '../components/FormItem'; import formProvider from '../utils/formProvider'; class UserAdd extends React.Component { ... render () { const {form: {name, age, gender}, onFormChange} = this.props; return ( <div> <header> <h1>添加用户</h1> </header> <main> <form onSubmit={(e) => this.handleSubmit(e)}> <FormItem label="用户名:" valid={name.valid} error={name.error}> <input type="text" value={name.value} onChange={(e) => onFormChange('name', e.target.value)} /> </FormItem> <FormItem label="年龄:" valid={age.valid} error={age.error}> <input type="number" value={age.value || ''} onChange={(e) => onFormChange('age', +e.target.value)} /> </FormItem> <FormItem label="性别:" valid={gender.valid} error={gender.error}> <select value={gender.value} onChange={(e) => onFormChange('gender', e.target.value)} > <option value="">请选择</option> <option value="male">男</option> <option value="female">女</option> </select> </FormItem> <br/> <input type="submit" value="提交"/> </form> </main> </div> ); } } UserAdd = formProvider({...})(UserAdd); export default UserAdd;