antd 表单双向绑定的研究

痛点

在使用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);
View Code

 

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);
View Code
相关文章
相关标签/搜索