antd 表单双向绑定的研究
痛点
在使用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);