痛点

在使用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. delphi------项目类型

    Console Application:控制台应用程序 writeln('HelloWorld'); //接收用户输入字符 readln: //直到用户输入回车结束 VCL Forms Applica ...

  2. color depth 色彩深度 像素深度

    Screen.colorDepth - Web APIs | MDN https://developer.mozilla.org/en-US/docs/Web/API/Screen/colorDept ...

  3. Java 之反射机制

    java 语言的反射机制 - 在运行状态中,对于任意一个类 (class 文件),都能够知道这个类的所有属性和方法; - 能动态获取类中的信息,也可以理解为对类(字节码文件)的解剖 描述字节码文件的类 ...

  4. selenium入门基础知识

    内容转载自:http://blog.csdn.net/huangbowen521/article/details/7816538 1.selenium介绍: Selenium是一个浏览器自动化操作框架 ...

  5. 007-shiro与spring web项目整合【一】基础搭建

    一.需求 将原来基于url的工程改成使用shiro实现 二.代码 https://github.com/bjlhx15/shiro.git 中的permission_shiro 三.去除原项目拦截器 ...

  6. C#简单实现动态数据生成Word文档并保存

    今天正好有人问我,怎么生成一个报表式的Word文档. 就是文字的样式和位置相对固定不变,只是里面的内容从数据中读取. 我觉得类似这种的一般用第三方报表来做比较简便.但既然要求了Word,只好硬着头皮来 ...

  7. 关于java.lang.NoSuchMethodError: org.slf4j.spi.LocationAwareLogger.log

    配置JFinal环境时,jar包已导入,web.xml已配置,Config也已经配置好,测试服务器时不停地出现 Exception in thread "main" java.la ...

  8. SQL基础一

    一.什么是SQL? SQL是结构化查询语言 SQL使我们有能力访问数据库 SQL是一种ANSI的标准计算机语言 二.SQL能做什么? SQL 面向数据库执行查询 SQL 可从数据库取回数据 SQL 可 ...

  9. maven的相关命令

    maven的相关命令 mvn archetype:create :创建 Maven 项目 mvn compile :编译源代码(编译到target文件夹中) mvn test-compile :编译测 ...

  10. how can i get the source code path && file names from an ELF file(compired with -g)?

    https://stackoverflow.com/questions/31333337/how-can-i-get-the-source-code-path-file-names-from-an-e ...