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
相關文章
相關標籤/搜索