前言
本文讲的如何利用context,将多个组件串联起来,实现一个更大的联合组件。最具有这个特性的就是表单组件,所以本文例子就是一个表单组件。本文例子参考 Ant Design 。本次不讲 context 知识,需要的话等到下一次分享。
准备
- es6 基本知识。参考地址
- react 基本知识。参考地址
- create-react-app 脚手架。 参考地址
- react context 知识。 参考地址
- react prop-types 相关知识。 参考地址
或者直接使用本文 demo Gitee地址
基本代码
<Form onSubmit={(e,v) => { console.log(e,'error'); console.log(v,'value'); }}> <Form.Item label={'手机号'}> <Form.Input name={'phone'} rules={[{validator: (e) => /^1[3-9]\d+$/.test(e),message: '手机号格式错误'}]}/> </Form.Item> <Form.Item label={'年龄'}> <Form.Input name={'age'} rules={[{validator: (e) => /^\d+$/.test(e),message: '只允许输入数字'}]}/> </Form.Item> <Form.Button>提交</Form.Button> <Form.Button type={'reset'}>重置</Form.Button> </Form>
需求
- 自定义校验规则
- 表单内容组件不限组合方式
- 点击提交按钮就可以提交
- 提交时候可以校验值并且可以自动拦截,然后将错误信息下发给 FormItem 组件并且显示出来
- 通过传入 Form 组件的 onSubmit 参数就可以获取到内容
实现
明白自己所需要的内容后,我们创建基本代码中的几个组件,Form , FormItem ,Input,以及 Button。
具体内容看代码中的注释
Form
首先我们要知道 Form 组件在联合组件中的负责的内容
- 数据收集
- 数据校验
- 提交、重置动作处理
代码如下
import React,{Component} from 'React'; import PropTypes from 'prop-types'; import {Item} from './Item'; import {Button} from './Button'; import {Input} from './Input'; export class Form extends Component{ static propTypes = { onSubmit: PropTypes.func.isrequired,// 需要该参数因为,如果没有该参数,整个组件就没有意义 defaultValues: PropTypes.object,// 如果有些需要默认参数的,就需要该参数 children: PropTypes.any,}; static defaultProps = { defaultValues: {},}; static childContextTypes = { form: PropTypes.any,// 定义上下文参数名称和格式,格式太麻烦,直接any了或者 object也可以。 }; state = { validates: {},change: 0,}; // 为什么不将数据全部放在 state 里面,在本文最后会讲到 registerState = { form: {},rules: {},label: {},}; getChildContext() { // 定义上下文返回内容 const {validates} = this.state; const {form} = this.registerState; return { form: { submit: this.submit.bind(this),reset: this.reset.bind(this),register: this.register.bind(this),registerLabel: this.registerLabel.bind(this),setFieldValue: this.setFieldValue.bind(this),data: form,validates,},}; } submit() { // 提交动作 const {onSubmit} = this.props; if (onSubmit) { const validates = []; const {form,rules,label} = this.registerState; Object.keys(form).forEach(key => { const item = form[key]; const itemRules = rules[key]; itemRules.forEach(rule => { //To do something validator 简单列出几种基本校验方法,可自行添加 let res = true; // 如果校验规则里面有基本规则时候,使用基本规则 if (rule.hasOwnProperty('type')) { switch (rule) { case 'phone': /^1[3-9]\d+$/.test(item); res = false; break; default: break; } } // 如果校验规则里面有 校验函数时候,使用它 if (rule.hasOwnProperty('validator')) { res = rule.validator(item); } // 校验不通过,向校验结果数组里面增加,并且结束本次校验 if (!res) { validates.push({key,message: rule.message,label: label.hasOwnProperty(key) ? label[key] : ''}); return false; } }); }); if (validates.length > 0) { // 在控制台打印出来 validates.forEach(item => { console.warn(`item: ${item.label ? item.label : item.key}; message: ${item.message}`); }); // 将错误信息返回到 state 并且由 context 向下文传递内容,例如 FormItem 收集到该信息,就可以显示出错误内容和样式 this.setState({ validates,}); } // 最后触发 onSubmit 参数,将错误信息和数据返回 onSubmit(validates,this.registerState.form); } } reset() { // 重置表单内容 const {form} = this.registerState; const {defaultValues} = this.props; this.registerState.form = Object.keys(form).reduce((t,c) => { t[c] = defaultValues.hasOwnProperty(c) ? defaultValues[c] : ''; return t; },{}); // 因为值不在 state 中,需要刷新一下state,完成值在 context 中的更新 this.change(); } //更新某一个值 setFieldValue(name,value) { this.registerState.form[name] = value; this.change(); } // 值和规则都不在state中,需要借助次方法更新内容 change() { this.setState({ change: this.state.change + 1,}); } // 注册参数,最后数据收集和规则校验都是通过该方法向里面添加的内容完成 register(name,itemRules) { if (this.registerFields.indexOf(name) === -1) { this.registerFields.push(name); const {defaultValues} = this.props; this.registerState.form[name] = defaultValues.hasOwnProperty(name) ? defaultValues[name] : ''; this.registerState.rules[name] = itemRules; } else { // 重复的话提示错误 console.warn(`\`${name}\` has repeat`); } } // 添加 字段名称,优化体验 registerLabel(name,label) { this.registerState.label[name] = label; } render() { return ( <div className="form"> {this.props.children} </div> ); // 这里使用括号因为在 webStrom 下格式化代码后的格式看起来更舒服。 } } // 将子组件加入到 Form 中 表示关联关系 Form.Item = Item; Form.Button = Button; Form.Input = Input;
FormItem
它的功能不多
代码如下
import React,{Component} from 'react'; import PropTypes from 'prop-types'; export class Item extends Component { // 这个值在 FormItem 组件 被包裹在 Form 组件中时,必须有 name; static propTypes = { label: PropTypes.string,}; static childContextTypes = { formItem: PropTypes.any,children: PropTypes.any,}; static contextTypes = { form: PropTypes.object,}; // 防止重复覆盖 name 的值 lock = false; // 获取到 包裹的输入组件的 name值,如果在存在 Form 中,则向 Form 注册name值相对的label值 setName(name) { if (!this.lock) { this.lock = true; this.name = name; const {form} = this.context; if (form) { form.registerLabel(name,this.props.label); } } else { // 同样,一个 FormItem 只允许操作一个值 console.warn('Allows only once `setName`'); } } getChildContext() { return { formItem: { setName: this.setName.bind(this),}; } render() { const {label} = this.props; const {form} = this.context; let className = 'form-item'; let help = false; if (form) { const error = form.validates.find(err => err.key === this.name); // 如果有找到属于自己错误,就修改状态 if (error) { className += ' form-item-warning'; help = error.message; return false; } } return ( <div className={className}> <div className="label"> {label} </div> <div className="input"> {this.props.children} </div> {help ? ( <div className="help"> {help} </div> ) : ''} </div> ); } }
Input
暂时演示输入组件为 Input ,后面可以按照该组件内容,继续增加其他操作组件
该类型组件负责的东西很多
- 唯一name,通知 FormItem 它所包裹的是谁
- Form 组件里面,收集的数据
- 校验规则
代码如下
import React,{Component} from 'react'; import PropTypes from 'prop-types'; export class Input extends Component { constructor(props,context) { super(props); // 如果在 Form 中,或者在 FormItem 中,name值为必填 if ((context.form || context.formItem) && !props.name) { throw new Error('You should set the `name` props'); } // 如果在 Form 中,不在 FormItem 中,提示一下,不在 FormItem 中不影响最后的值 if (context.form && !context.formItem) { console.warn('Maybe used `Input` in `FormItem` can be better'); } // 在 FormItem 中,就要通知它自己是谁 if (context.formItem) { context.formItem.setName(props.name); } // 在 Form 中,就向 Form 注册自己的 name 和 校验规则 if (context.form) { context.form.register(props.name,props.rules); } } shouldComponentUpdate(nextProps) { const {form} = this.context; const {name} = this.props; // 当 有 onChange 事件 或者外部使用组件,强行更改了 Input 值,就需要通知 Form 更新值 if (form && this.changeLock && form.data[name] !== nextProps.value) { form.setFieldValue(name,nextProps.value); return false; } return true; } static propTypes = { name: PropTypes.string,value: PropTypes.string,onChange: PropTypes.func,rules: PropTypes.arrayOf(PropTypes.shape({ type: PropTypes.oneOf(['phone']),validator: PropTypes.func,message: PropTypes.string.isrequired,})),type: PropTypes.oneOf(['text','tel','number','color','date']),}; static defaultProps = { value: '',rules: [],formItem: PropTypes.object,}; onChange(e) { const val = e.currentTarget.value; const {onChange,name} = this.props; const {form} = this.context; if (onChange) { this.changeLock = true; onChange(val); } else { if (form) { form.setFieldValue(name,val); } } } render() { let {value,name,type} = this.props; const {form} = this.context; if (form) { value = form.data[name] || ''; } return ( <input onChange={this.onChange.bind(this)} type={type} value={value}/> ); } }
Button
负责内容很简单
- 提交,触发 submit
- 重置,触发 reset
代码如下
import React,{Component} from 'react'; import PropTypes from 'prop-types'; export class Button extends Component { componentWillMount() { const {form} = this.context; // 该组件只能用于 Form if (!form) { throw new Error('You should used `FormButton` in the `Form`'); } } static propTypes = { children: PropTypes.any,type: PropTypes.oneOf(['submit','reset']),}; static defaultProps = { type: 'submit',}; static contextTypes = { form: PropTypes.any,}; onClick() { const {form} = this.context; const {type} = this.props; if (type === 'reset') { form.reset(); } else { form.submit(); } } render() { return ( <button onClick={this.onClick.bind(this)} className={'form-button'}> {this.props.children} </button> ); } }
后言
首先先讲明为何 不将label 和数据不放在state 里面因为多个组件同时注册时候,state更新来不及,会导致部分值初始化不成功,所以最后将值收集在 另外的 object 里面,并且是直接赋值看了上面几个组件的代码,应该有所明确,这些组件组合起来使用就是一个大的组件。同时又可以单独使用,知道该如何使用后,又可以按照规则,更新整个各个组件,而不会说,一个巨大无比的单独组件,无法拆分,累赘又复杂。通过联合组件,可以达成很多奇妙的组合方式。上文的例子中,如果没有 Form 组件, 单独的 FormInput 加 Input,这两个组合起来,也可以是一个单独的验证器。