痛点

在使用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. php自定义函数: 下载本地服务器的大文件

    // 使用方法 $file_path = './a.zip'; // 只能是本地服务器文件, 多大的文件都支持!! down_file($file_path); // 函数参数: 服务器文件路径,下载 ...

  2. IO流入门-第五章-FileWriter

    FileWriter基本用法和方法示例 /* java.io.Writer java.io.OutputStreamWriter 转换流(字节输出流--->字符输出流) java.io.File ...

  3. 一篇搞定vue-router

    由于Vue常见于前后端分离开发场景下,所以页面跳转工作全部交给了前端,所以基于集中管理的原则,就有了vue-router插件,它给定了url和组件之间的跳转规则 Demo准备 vue init web ...

  4. mysql 建立表之间关系 练习 2

    创建数据库db6 create database db6 charset=utf8; user db6; # 创建班级表 mysql) not null unique); Query OK, rows ...

  5. HDU1158:Employment Planning(线性dp)

    题目:http://acm.hdu.edu.cn/showproblem.php?pid=1158 这题又是看了题解,题意是一项工作需要n个月完成,雇佣一个人需要m1的钱,一个人的月工资为sa,辞退一 ...

  6. SCSS入门

    1. CSS预处理器 定义了一种新的专门的编程语言,编译后成正常的CSS文件.为CSS增加一些编程的特性,无需考虑浏览器的兼容问题,让CSS更加简洁,适应性更强,可读性更佳,更易于代码的维护等诸多好处 ...

  7. python16_day11【MQ、Redis、Memcache】

    一.RabbitMQ 是一个在AMQP基础上完整的,可复用的企业消息系统.他遵循Mozilla Public License开源协议. MQ全称为Message Queue, 消息队列(MQ)是一种应 ...

  8. python网络编程——IO多路复用select/poll/epoll的使用

    转载博客: http://www.haiyun.me/archives/1056.html http://www.cnblogs.com/coser/archive/2012/01/06/231521 ...

  9. 跟着实例学习ZooKeeper的用法: 分布式锁

    锁 分布式的锁全局同步, 这意味着任何一个时间点不会有两个客户端都拥有相同的锁. 可重入锁Shared Reentrant Lock 首先我们先看一个全局可重入的锁. Shared意味着锁是全局可见的 ...

  10. ie9下面的console的bug

    摘自:http://blog.csdn.net/cdnight/article/details/51094464 ie9下面,很奇怪的是有console的代码有时候执行不下去,不过当f12打开控制台的 ...