痛點
在使用antd的表單時,大家覺得不夠清爽,總結如下:
-
大量的模板語法,需要一定的學習成本。
-
需要手動地進行數據綁定,使用大量的onChange/setFieldsValue去控制數據。
-
無法通過state動態地控制表單。
-
提交表單時,需要將props.form的數據和其他數據組合。
-
表單聯動時處理復雜。
解決方向
現狀
-
類比Angular與Vue,大家覺得雙向綁定的模式,在表單的開發中是比較好的,所以如果能將表單的數據直接綁定到state上,那么react的開發表單就會相對高效一些。
-
由於antd的表單是以react為基礎,遵循單向數據流的設計哲學,所以想讓antd團隊去提供綁定的機制可能性不大,並且現有的表單已經具備綁定到form屬性的能力,所以應該另行探索出路。
-
項目里面已經遵循antd的api進行了開發,方案不能影響之前的代碼使用,同時賦予雙向綁定的能力,所以不應該創建新的語法,當然,如果可以由json直接構建表單,也不失為一種便捷的方式,但是,個人覺得不該引入新的語法去增加成本,所以本文沒有在此方向進行探索。
-
解決方案不能依賴於antd的具體實現,即不能侵入式地修改源碼去實現雙向綁定,這樣就與antd解耦,也不用隨着antd的版本去更新方法。
原則
基於上述現狀,此方案有幾條原則:
-
實現state與表單數據的雙向綁定
-
項目可以無痛地引入此方案,不需要修改之前的使用方式
-
相對於使用者透明,不需要額外的處理,不引入新的語法
-
不能修改antd的實現方式
-
表單數據不能影響原有state中的數據
方案
利用antd的現有能力
antd提供了兩個很有用的API:
mapPropsToFields
,onValuesChange
這就為我們初始化表單和表單變化時接收回調提供了可能,
我們可以利用mapPropsToFields去初始化表單的數據
onValuesChange去將表單的值返回。

提供雙向綁定的能力
由於antd不能簡單地直接與state進行綁定(其實可以的,后面會講解),需要設計一個可以與表單數據進行綁定的容器
formBinding
,這個容器可以為表單指定初始值,也可以接受到表單值變更去更新自己的狀態。
更新數據到組件的state
因為form組件並沒有顯式的暴露他所包含的組件,所以需要一個機制去將
formBinding
已經綁定好的數據同步給使用表單的組件<DEMO />
。
這里借鑒了Vue實現雙向綁定的方法,訂閱/發布模式,即當具有雙向綁定能力的
forBinding
發生數據變化時,發布一個事件去通知訂閱這個事件的組件去用表單的數據更新自己的state
還記得我們遵守的第3條和第5條原則嗎?
我們需要一個修飾器
watch
去做這件事,這樣就不用手動的監聽事件了。
同時,表單的數據不能影響原有state的值,所以,我們將表單的數據同步在
<DEMO />
state中的formScope中,算是約定吧。
整體的流程:

前面之所以說antd的表單沒法同步state是因為
form
沒有給出他包裹組件的引用,但是,看他的源碼后發現,在rc-form中可以直接通過wrappedcomponentref
來拿到包裹組件的引用,鏈接

watch
的,可以直接在formBinding
中完成state的綁定
好處:不需要額外的機制去同步state;
壞處:依賴了源碼的能力,如果wrappedcomponentref改變,方案也需要變化,帶有侵入性。

Demo

import { Form, Input, Tooltip, Icon, Cascader, Select, Row, Col, Checkbox, Button, AutoComplete, } from 'antd'; const FormItem = Form.Item; const Option = Select.Option; // 簡單的eventemit,在實際項目中使用成熟的第三方組件 const isFunction = function(obj) { return typeof ojb === 'function' || false; }; class EventEmitter { constructor() { this.listeners = new Map(); } addListener(label, callback) { this.listeners.has(label) || this.listeners.set(label, []); this.listeners.get(label).push(callback); } removeListener(label, callback) { let listeners = this.listeners.get(label); let index; if (listeners && listeners.length) { index = listeners.reduce((i, listener, index) => { return isFunction(listener) && listener === callback ? (i = index) : i; }, -1); } if (index > -1) { listeners.splice(index, 1); this.listeners.set(label, listeners); return true; } return false; } emit(label, ...args) { let listeners = this.listeners.get(label); if (listeners && listeners.length) { listeners.forEach(listener => { listener(...args); }); return true; } return false; } } class Observer { constructor(subject) { this.subject = subject; } on(label, callback) { this.subject.addListener(label, callback); } } let observable = new EventEmitter(); let observer = new Observer(observable); //############################################################## // 雙向綁定的表單的數據 const formBinding = WrappedComponent => { return class extends React.Component { state = { scope: {}, }; onFormChange = values => { console.log('form change'); console.log(values); console.log(this.state.scope); const tempScope = Object.assign({}, this.state.scope); this.setState( { scope: Object.assign(tempScope, values), }, () => { // 發送同步實際組件的事件 observable.emit('syncFormState', this.state.scope); }, ); }; render() { return ( <WrappedComponent scope={this.state.scope} onFormChange={this.onFormChange} /> ); } }; }; // 監聽事件,將表單的數據同步到實際組件的state上 const watcher = Component => { return class extends React.Component { componentDidMount() { observer.on('syncFormState', data => { this.handleSyncEvent(data); }); } handleSyncEvent(data) { this.node.setState({ formScope: Object.assign({}, data), }); } render() { return <Component ref={node => (this.node = node)} {...this.props} />; } }; }; @formBinding @Form.create({ mapPropsToFields(props) { // 使用上層組件的scope的值作為表單的數據 const { scope } = props; return { nickname: Form.createFormField({ value: scope.nickname, }), phone: Form.createFormField({ value: scope.phone, }), address: Form.createFormField({ value: scope.address, }), agreement: Form.createFormField({ value: scope.agreement, }), }; }, onValuesChange(props, values) { // 將表單的變化值回填到上層組件的scope中 props.onFormChange(values); }, }) @watcher // 接受事件去更新state class Demo extends React.Component { state = { formScope: {}, }; handleSubmit = e => { e.preventDefault(); this.props.form.validateFieldsAndScroll((err, values) => { if (err) { console.log('Received values of form: ', values); } console.log('value'); console.log(values); }); }; render() { const { getFieldDecorator } = this.props.form; const { autoCompleteResult } = this.state; const formItemLayout = { labelCol: { xs: { span: 24 }, sm: { span: 6 }, }, wrapperCol: { xs: { span: 24 }, sm: { span: 14 }, }, }; const tailFormItemLayout = { wrapperCol: { xs: { span: 24, offset: 0, }, sm: { span: 14, offset: 6, }, }, }; const prefixSelector = getFieldDecorator('prefix', { initialValue: '86', })( <Select style={{ width: 60 }}> <Option value="86">+86</Option> <Option value="87">+87</Option> </Select>, ); return ( <Form onSubmit={this.handleSubmit}> <FormItem {...formItemLayout} label={<span>Nickname</span>} hasFeedback> {getFieldDecorator('nickname', { rules: [ { required: true, message: 'Please input your nickname!', whitespace: true, }, ], })(<Input />)} </FormItem> <FormItem {...formItemLayout} label="Phone Number"> {getFieldDecorator('phone', { rules: [ { required: true, message: 'Please input your phone number!' }, ], })(<Input addonBefore={prefixSelector} style={{ width: '100%' }} />)} </FormItem> {this.state.formScope.nickname && this.state.formScope.phone ? ( <FormItem {...formItemLayout} label="Address"> {getFieldDecorator('address', { rules: [{ required: true, message: 'Please input your address' }], })(<Input style={{ width: '100%' }} />)} </FormItem> ) : null} <FormItem {...tailFormItemLayout} style={{ marginBottom: 8 }}> {getFieldDecorator('agreement', { valuePropName: 'checked', })( <Checkbox> I have read the agreement </Checkbox>, )} </FormItem> <FormItem {...tailFormItemLayout}> <Button type="primary" htmlType="submit"> Register </Button> </FormItem> <pre>{JSON.stringify(this.state.formScope,null,2)}</pre> </Form> ); } } ReactDOM.render(<Demo />, mountNode);

import { Form, Input } from 'antd'; import _ from 'lodash' const FormItem = Form.Item; // 監聽表單的變化,同步組件的state const decorator = WrappedComponent => { return class extends React.Component { componentDidMount() { const func = this.node.setFields Reflect.defineProperty(this.node, 'setFields', { get: () => { return (values, cb) => { this.inst.setState({ scope: _.mapValues(values, 'value'), }) func(values, cb) } } }) } render() { console.debug(this.props) return <WrappedComponent wrappedComponentRef={inst => this.inst = inst} ref={node => this.node = node} {...this.props} /> } } } @decorator @Form.create({ mapPropsToFields(props) { return { username: Form.createFormField({ ...props.username, value: props.username.value, }), }; }, }) class DemoForm extends React.Component { state = { scope: {}, } render() { const { getFieldDecorator } = this.props.form; return ( <Form layout="inline"> <FormItem label="Username"> {getFieldDecorator('username', { rules: [{ required: true, message: 'Username is required!' }], })(<Input />)} </FormItem> <pre className="language-bash"> {JSON.stringify(this.state.scope, null, 2)} </pre> { this.state.scope.username ? <FormItem label={<span>address</span>}> {getFieldDecorator('address', { rules: [ { required: true, message: 'Please input your address!', whitespace: true, }, ], })(<Input />)} </FormItem> : null } </Form> ); } } class Demo extends React.Component { state = { fields: { username: { value: 'benjycui', }, }, }; handleFormChange = (changedFields) => { this.setState(({ fields }) => ({ fields: { ...fields, ...changedFields }, })); } render() { const fields = this.state.fields; return ( <div> <DemoForm {...fields} /> </div> ); } } ReactDOM.render(<Demo />, mountNode);