本文讲的如何利用context,将多个组件串联起来,实现一个更大的联合组件。最具备这个特性的就是表单组件,因此本文例子就是一个表单组件。本文例子参考 Ant Design 。本次不讲 context 知识,须要的话等到下一次分享。html
或者直接使用本文 demo Gitee地址react
<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>
明白本身所须要的内容后,咱们建立基本代码中的几个组件,Form , FormItem ,Input , 以及 Button。
具体内容看代码中的注释git
首先咱们要知道 Form 组件在联合组件中的负责的内容es6
代码以下web
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;
它的功能很少npm
代码以下数组
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 ,后面能够按照该组件内容,继续增长其余操做组件
该类型组件负责的东西不少app
代码以下函数
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: [], }; static contextTypes = { form: PropTypes.object, 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}/> ); } }
负责内容很简单优化
代码以下
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,这两个组合起来,也能够是一个单独的验证器。