本文講的如何利用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,這兩個組合起來,也能夠是一個單獨的驗證器。