痛点

在使用antd的表单时,大家觉得不够清爽,总结如下:
  1. 大量的模板语法,需要一定的学习成本。
  2. 需要手动地进行数据绑定,使用大量的onChange/setFieldsValue去控制数据。
  3. 无法通过state动态地控制表单。
  4. 提交表单时,需要将props.form的数据和其他数据组合。
  5. 表单联动时处理复杂。
 

解决方向

现状

  1. 类比Angular与Vue,大家觉得双向绑定的模式,在表单的开发中是比较好的,所以如果能将表单的数据直接绑定到state上,那么react的开发表单就会相对高效一些。
  2. 由于antd的表单是以react为基础,遵循单向数据流的设计哲学,所以想让antd团队去提供绑定的机制可能性不大,并且现有的表单已经具备绑定到form属性的能力,所以应该另行探索出路。
  3. 项目里面已经遵循antd的api进行了开发,方案不能影响之前的代码使用,同时赋予双向绑定的能力,所以不应该创建新的语法,当然,如果可以由json直接构建表单,也不失为一种便捷的方式,但是,个人觉得不该引入新的语法去增加成本,所以本文没有在此方向进行探索。
  4. 解决方案不能依赖于antd的具体实现,即不能侵入式地修改源码去实现双向绑定,这样就与antd解耦,也不用随着antd的版本去更新方法。
 

原则

基于上述现状,此方案有几条原则:
  1. 实现state与表单数据的双向绑定
  2. 项目可以无痛地引入此方案,不需要修改之前的使用方式
  3. 相对于使用者透明,不需要额外的处理,不引入新的语法
  4. 不能​修改antd的实现方式
  5. 表单数据不能影响原有state中的数据
 

方案

利用antd的现有能力

antd提供了两个很有用的API: mapPropsToFieldsonValuesChange
这就为我们初始化表单和表单变化时接收回调提供了可能,
我们可以利用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);
 

antd 表单双向绑定的研究的更多相关文章

  1. html表单-双向绑定

    潜水多年.一直是只看不评不写多年,每每看到各位大牛分享的经典文章都是默默的收藏,对大牛技术分享技术表示感谢,这么多年从博客园学到了很多. 这段时间项目告一段落. 正好这段时间相对清闲,我也整理一些常用 ...

  2. react中使用Input表单双向绑定方法

    input react 表单 input 密码框在谷歌浏览器下 会有黄色填充 官网的不太用,这个比较好用 type="password" autoComplete="ne ...

  3. 前端MVC Vue2学习总结(五)——表单输入绑定、组件

    一.表单输入绑定 1.1.基础用法 你可以用 v-model 指令在表单控件元素上创建双向数据绑定.它会根据控件类型自动选取正确的方法来更新元素.尽管有些神奇,但 v-model 本质上不过是语法糖, ...

  4. Vue的指令系统、计算属性和表单输入绑定

    指令系统 指令 (Directives) 是带有 v- 前缀的特殊特性.指令特性的值预期是单个 JavaScript 表达式 (v-for 是例外情况,稍后我们再讨论).指令的职责是,当表达式的值改变 ...

  5. Vue.js教程--基础2(事件处理 表单输入绑定

    事件处理 表单输入绑定 事件处理 监听v-on 监听 DOM 事件,并在触发时运行一些 JavaScript 代码. 可以在v-on:click=''加内联语句. 有时也需要在内联语句处理器中访问原始 ...

  6. Vue学习计划基础笔记(五) - 表单输入绑定、组件基础

    表单输入绑定.组件基础 目标: 熟练掌握vue中表单的处理方式 对之前学习的内容简单回顾一下,并写一个实例,学以致用(最好脱离文档) vue中表单的处理方式 vue中表单的处理使用了v-model指令 ...

  7. VUE:事件处理和表单输入绑定

    事件处理 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <titl ...

  8. vue样式绑定、事件监听、表单输入绑定、响应接口

    1.样式绑定 操作元素的 class 列表和内联样式是数据绑定的一个常见需求.因为它们都是属性,所以我们可以用 v-bind 处理它们:只需要通过表达式计算出字符串结果即可.不过,字符串拼接麻烦且易错 ...

  9. Vue(10)表单输入绑定v-model

    v-model v-model指定可以实现表单值与属性的双向绑定.即表单元素中更改了值会自动的更新属性中的值,属性中的值更新了会自动更新表单中的值 绑定的属性和事件 v-model在内部为不同的输入元 ...

随机推荐

  1. [刷题]ACM ICPC 2016北京赛站网络赛 D - Pick Your Players

    Description You are the manager of a small soccer team. After seeing the shameless behavior of your ...

  2. html常见兼容性问题

    html常见兼容性问题? 1.双边距BUG float引起的  使用display 2.3像素问题 使用float引起的 使用dislpay:inline -3px 3.超链接hover 点击后失效 ...

  3. Babel编译

    Babel的目的就是让你可以使用最新的标准来开发,然后把兼容的问题交给它来完成.比如我如何在使用ES6的语法写完之后将其转换为ES5满足通用性呢? 先用这个最常用的Babel的用法来引入吧. 一  首 ...

  4. for...in循环取Json数据

    var result = { "Tables":{ "B2B_DS_ORDERMX0":{ "ordernum":"tables- ...

  5. Android-BoundService

    Android-BoundService 一 binder 内核->字符设备binder(负责进程间通信的驱动)->servicemanager->binder类->binge ...

  6. Linux学习笔记(7)CRT实现windows与linux的文件上传下载

    Linux学习笔记(7)CRT实现windows与linux的文件上传下载 按下Alt + p 进入SFTP模式,或者右击选项卡进入 命令介绍 help 显示该FTP提供所有的命令 lcd 改变本地上 ...

  7. (转)live555在Linux下最简单地实现实时流媒体点播

    通过Live555交叉编译后运行发现,上面实现的流媒体实时通过文件服务器的文件点播,没有相关的流媒体实现方式, 但在Linux下,可以通过某些技巧实现Live555服务器实时流媒体服务器,并且是傻瓜式 ...

  8. Python3.6写socket程序

    Python进行Socket程序编写使用的主要模块就是 socket 模块,在这个模块中可以找到 socket()函数,该函数用于创建套接字对象.套接字也有自己的方法集,这些方法可以实现基于套接字的网 ...

  9. likely(x)与unlikely(x) __builtin_expect

    本文讲的likely()和unlikely()两个宏,在linux内核代码和一些应用中可常见到它们的身影.实质上,这两个宏是关于GCC编译器内置宏__builtin_expect的使用. 顾名思义,l ...

  10. C#检测两个文件内容是否相同

    不知道为什么对Excel 2010 xlsx后缀的文件没有效果,求解! 对其他文件有效,如.txt,.csv using System; using System.Security.Cryptogra ...