• Form 表单

    具有数据收集、校验和提交功能的表单,包含复选框、单选框、输入框、下拉选择框等元素。

    表单

    我们为 form 提供了以下三种排列方式:

    表单域

    表单一定会包含表单域,表单域可以是输入控件,标准表单域,标签,下拉菜单,文本域等。

    这里我们封装了表单域 <Form.Item />

    <Form.Item {...props}>
      {children}
    </Form.Item>
    

    代码演示

    水平登录栏,常用在顶部导航栏中。
    expand code expand code
    import { Form, Icon, Input, Button } from 'choerodon-ui';
    const FormItem = Form.Item;
    
    function hasErrors(fieldsError) {
      return Object.keys(fieldsError).some(field => fieldsError[field]);
    }
    
    class HorizontalLoginForm extends React.Component {
      componentDidMount() {
        // To disabled submit button at the beginning.
        this.props.form.validateFields();
      }
    
      handleSubmit = (e) => {
        e.preventDefault();
        this.props.form.validateFields((err, values) => {
          if (!err) {
            console.log('Received values of form: ', values);
          }
        });
      }
    
      render() {
        const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form;
    
        // Only show error after a field is touched.
        const userNameError = isFieldTouched('userName') && getFieldError('userName');
        const passwordError = isFieldTouched('password') && getFieldError('password');
        return (
          <Form layout="inline" onSubmit={this.handleSubmit}>
            <FormItem
              validateStatus={userNameError ? 'error' : ''}
              help={userNameError || ''}
            >
              {getFieldDecorator('userName', {
                rules: [{ required: true, message: 'Please input your username!' }],
              })(
                <Input prefix={<Icon type="person_outline" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="Username" />
              )}
            </FormItem>
            <FormItem
              validateStatus={passwordError ? 'error' : ''}
              help={passwordError || ''}
            >
              {getFieldDecorator('password', {
                rules: [{ required: true, message: 'Please input your Password!' }],
              })(
                <Input prefix={<Icon type="lock_outline" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="Password" />
              )}
            </FormItem>
            <FormItem>
              <Button
                type="primary"
                htmlType="submit"
                disabled={hasErrors(getFieldsError())}
              >
                Log in
              </Button>
            </FormItem>
          </Form>
        );
      }
    }
    
    const WrappedHorizontalLoginForm = Form.create()(HorizontalLoginForm);
    
    ReactDOM.render(<WrappedHorizontalLoginForm />, mountNode);
    
    普通的登录框,可以容纳更多的元素。
    expand code expand code
    import { Form, Icon, Input, Button, Checkbox } from 'choerodon-ui';
    const FormItem = Form.Item;
    
    class NormalLoginForm extends React.Component {
      handleSubmit = (e) => {
        e.preventDefault();
        this.props.form.validateFields((err, values) => {
          if (!err) {
            console.log('Received values of form: ', values);
          }
        });
      }
    
      render() {
        const { getFieldDecorator } = this.props.form;
        return (
          <Form onSubmit={this.handleSubmit} className="login-form">
            <FormItem>
              {getFieldDecorator('userName', {
                rules: [{ required: true, message: 'Please input your username!' }],
              })(
                <Input prefix={<Icon type="person_outline" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="Username" />
              )}
            </FormItem>
            <FormItem>
              {getFieldDecorator('password', {
                rules: [{ required: true, message: 'Please input your Password!' }],
              })(
                <Input prefix={<Icon type="lock_outline" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="Password" />
              )}
            </FormItem>
            <FormItem>
              {getFieldDecorator('remember', {
                valuePropName: 'checked',
                initialValue: true,
              })(
                <Checkbox>Remember me</Checkbox>
              )}
              <a className="login-form-forgot" href="">Forgot password</a>
              <Button type="primary" funcType="raised" htmlType="submit" className="login-form-button">
                Log in
              </Button>
              Or <a href="">register now!</a>
            </FormItem>
          </Form>
        );
      }
    }
    
    const WrappedNormalLoginForm = Form.create()(NormalLoginForm);
    
    ReactDOM.render(<WrappedNormalLoginForm />, mountNode);
    
    用户填写必须的信息以注册新用户。
    expand code expand code
    import { Form, Input, Tooltip, Icon, Cascader, Row, Col, Checkbox, Button, AutoComplete, InputNumber } from 'choerodon-ui';
    const FormItem = Form.Item;
    const AutoCompleteOption = AutoComplete.Option;
    
    const residences = [{
      value: 'zhejiang',
      label: 'Zhejiang',
      children: [{
        value: 'hangzhou',
        label: 'Hangzhou',
        children: [{
          value: 'xihu',
          label: 'West Lake',
        }],
      }],
    }, {
      value: 'jiangsu',
      label: 'Jiangsu',
      children: [{
        value: 'nanjing',
        label: 'Nanjing',
        children: [{
          value: 'zhonghuamen',
          label: 'Zhong Hua Men',
        }],
      }],
    }];
    
    class RegistrationForm extends React.Component {
      state = {
        confirmDirty: false,
        showLengthInfo: true,
        autoCompleteResult: [],
      }
    
      handleSubmit = (e) => {
        e.preventDefault();
        this.props.form.validateFields((err, values) => {
          if (err.nickname) {
            this.setState({
              showLengthInfo: false,
            });
          }
          if (!err) {
            console.log('Received values of form: ', values);
          }
        });
      }
    
      handleConfirmBlur = (e) => {
        const value = e.target.value;
        this.setState({ confirmDirty: this.state.confirmDirty || !!value });
      }
    
      compareToFirstPassword = (rule, value, callback) => {
        const form = this.props.form;
        if (value && value !== form.getFieldValue('password')) {
          callback('Two passwords that you enter is inconsistent!');
        } else {
          callback();
        }
      }
    
      validateToNextPassword = (rule, value, callback) => {
        const form = this.props.form;
        if (value && this.state.confirmDirty) {
          form.validateFields(['confirm'], { force: true });
        }
        callback();
      }
    
      handleWebsiteChange = (value) => {
        let autoCompleteResult;
        if (!value) {
          autoCompleteResult = [];
        } else {
          autoCompleteResult = ['.com', '.org', '.net'].map(domain => `${value}${domain}`);
        }
        this.setState({ autoCompleteResult });
      }
    
      handleNickName = () => {
        const { showLengthInfo } = this.state;
        if (!showLengthInfo) {
          this.setState({
            showLengthInfo: true,
          });
        }
      }
    
      render() {
        const { getFieldDecorator } = this.props.form;
        const { autoCompleteResult, showLengthInfo } = this.state;
    
        const formItemLayout = {
          labelCol: {
            xs: { span: 24 },
            sm: { span: 8 },
          },
          wrapperCol: {
            xs: { span: 24 },
            sm: { span: 16 },
          },
        };
        const tailFormItemLayout = {
          wrapperCol: {
            xs: {
              span: 24,
              offset: 0,
            },
            sm: {
              span: 16,
              offset: 8,
            },
          },
        };
    
        const websiteOptions = autoCompleteResult.map(website => (
          <AutoCompleteOption key={website}>{website}</AutoCompleteOption>
        ));
    
        return (
          <Form onSubmit={this.handleSubmit}>
            <FormItem
              {...formItemLayout}
              label="E-mail"
            >
              {getFieldDecorator('email', {
                rules: [{
                  type: 'email', message: 'The input is not valid E-mail!',
                }, {
                  required: true, message: 'Please input your E-mail!',
                }],
              })(
                <Input label="email" />
              )}
            </FormItem>
            <FormItem
              {...formItemLayout}
              label="Password"
            >
              {getFieldDecorator('password', {
                rules: [{
                  required: true, message: 'Please input your password!',
                }, {
                  validator: this.validateToNextPassword,
                }],
              })(
                <Input label="password" type="password" />
              )}
            </FormItem>
            <FormItem
              {...formItemLayout}
              label="Confirm Password"
            >
              {getFieldDecorator('confirm', {
                rules: [{
                  required: true, message: 'Please confirm your password!',
                }, {
                  validator: this.compareToFirstPassword,
                }],
              })(
                <Input label="password" type="password" onBlur={this.handleConfirmBlur} />
              )}
            </FormItem>
            <FormItem
              {...formItemLayout}
              label={(
                <span>
                  Nickname&nbsp;
                  <Tooltip title="What do you want others to call you?">
                    <Icon type="question-circle-o" />
                  </Tooltip>
                </span>
              )}
            >
              {getFieldDecorator('nickname', {
                rules: [{ required: true, message: 'Please input your nickname!', whitespace: true }],
              })(
                <Input
                  maxLength={20}
                  label="名称"
                  showLengthInfo={showLengthInfo}
                  onChange={this.handleNickName}
                />
              )}
            </FormItem>
            <FormItem
              {...formItemLayout}
            >
              {getFieldDecorator('age', {
                rules: [{
                  required: true,
                  message: 'Please input your age!',
                  type: 'number',
                  whitespace: true,
                }],
                initialValue: 20,
              })(
                <InputNumber label="Age" min={1} max={100} />
              )}
            </FormItem>
            <FormItem
              {...formItemLayout}
              label="Habitual Residence"
            >
              {getFieldDecorator('residence', {
                initialValue: ['zhejiang', 'hangzhou', 'xihu'],
                rules: [{ type: 'array', required: true, message: 'Please select your habitual residence!' }],
              })(
                <Cascader options={residences} />
              )}
            </FormItem>
            <FormItem
              {...formItemLayout}
            >
              {getFieldDecorator('website', {
                rules: [{ required: true, message: 'Please input website!' }],
              })(
                <AutoComplete
                  dataSource={websiteOptions}
                  onChange={this.handleWebsiteChange}
                  placeholder="website"
                  label="Website"
                />
              )}
            </FormItem>
            <FormItem
              {...formItemLayout}
              extra="We must make sure that your are a human."
            >
              <Row gutter={8}>
                <Col span={12}>
                  {getFieldDecorator('captcha', {
                    rules: [{ required: true, message: 'Please input the captcha you got!' }],
                  })(
                    <Input label="Captcha" />
                  )}
                </Col>
                <Col span={12}>
                  <Button funcType="raised">Get captcha</Button>
                </Col>
              </Row>
            </FormItem>
            <FormItem {...tailFormItemLayout}>
              {getFieldDecorator('agreement', {
                valuePropName: 'checked',
              })(
                <Checkbox>I have read the <a href="">agreement</a></Checkbox>
              )}
            </FormItem>
            <FormItem {...tailFormItemLayout}>
              <Button type="primary" funcType="raised" htmlType="submit">Register</Button>
            </FormItem>
          </Form>
        );
      }
    }
    
    const WrappedRegistrationForm = Form.create()(RegistrationForm);
    
    ReactDOM.render(<WrappedRegistrationForm />, mountNode);
    

    三列栅格式的表单排列方式,常用于数据表格的高级搜索。

    有部分定制的样式代码,由于输入标签长度不确定,需要根据具体情况自行调整。

    expand code expand code
    import { Form, Row, Col, Input, Button, Icon } from 'choerodon-ui';
    const FormItem = Form.Item;
    
    class AdvancedSearchForm extends React.Component {
      state = {
        expand: false,
      };
    
      handleSearch = (e) => {
        e.preventDefault();
        this.props.form.validateFields((err, values) => {
          console.log('Received values of form: ', values);
        });
      }
    
      handleReset = () => {
        this.props.form.resetFields();
      }
    
      toggle = () => {
        const { expand } = this.state;
        this.setState({ expand: !expand });
      }
    
      // To generate mock Form.Item
      getFields() {
        const count = this.state.expand ? 10 : 6;
        const { getFieldDecorator } = this.props.form;
        const children = [];
        for (let i = 0; i < 10; i++) {
          children.push(
            <Col span={8} key={i} style={{ display: i < count ? 'block' : 'none' }}>
              <FormItem label={`Field ${i}`}>
                {getFieldDecorator(`field-${i}`)(
                  <Input placeholder="placeholder" />
                )}
              </FormItem>
            </Col>
          );
        }
        return children;
      }
    
      render() {
        return (
          <Form
            className="ant-advanced-search-form"
            onSubmit={this.handleSearch}
          >
            <Row gutter={24}>{this.getFields()}</Row>
            <Row>
              <Col span={24} style={{ textAlign: 'right' }}>
                <Button type="primary" htmlType="submit">Search</Button>
                <Button style={{ marginLeft: 8 }} onClick={this.handleReset}>
                  Clear
                </Button>
                <a style={{ marginLeft: 8, fontSize: 12 }} onClick={this.toggle}>
                  Collapse <Icon type={this.state.expand ? 'up' : 'down'} />
                </a>
              </Col>
            </Row>
          </Form>
        );
      }
    }
    
    const WrappedAdvancedSearchForm = Form.create()(AdvancedSearchForm);
    ReactDOM.render(
      <div>
        <WrappedAdvancedSearchForm />
        <div className="search-result-list">Search Result List</div>
      </div>,
      mountNode
    );
    
    当用户访问一个展示了某个列表的页面,想新建一项但又不想跳转页面时,可以用 Modal 弹出一个表单,用户填写必要信息后创建新的项。
    expand code expand code
    import { Button, Modal, Form, Input, Radio } from 'choerodon-ui';
    const FormItem = Form.Item;
    
    const CollectionCreateForm = Form.create()(
      class extends React.Component {
        render() {
          const { visible, onCancel, onCreate, form } = this.props;
          const { getFieldDecorator } = form;
          return (
            <Modal
              visible={visible}
              title="Create a new collection"
              okText="Create"
              onCancel={onCancel}
              onOk={onCreate}
            >
              <Form layout="vertical">
                <FormItem label="Title">
                  {getFieldDecorator('title', {
                    rules: [{ required: true, message: 'Please input the title of collection!' }],
                  })(
                    <Input />
                  )}
                </FormItem>
                <FormItem label="Description">
                  {getFieldDecorator('description')(<Input type="textarea" />)}
                </FormItem>
                <FormItem className="collection-create-form_last-form-item">
                  {getFieldDecorator('modifier', {
                    initialValue: 'public',
                  })(
                    <Radio.Group>
                      <Radio value="public">Public</Radio>
                      <Radio value="private">Private</Radio>
                    </Radio.Group>
                  )}
                </FormItem>
              </Form>
            </Modal>
          );
        }
      }
    );
    
    class CollectionsPage extends React.Component {
      state = {
        visible: false,
      };
    
      showModal = () => {
        this.setState({ visible: true });
      }
    
      handleCancel = () => {
        this.setState({ visible: false });
      }
    
      handleCreate = () => {
        const form = this.formRef.props.form;
        form.validateFields((err, values) => {
          if (err) {
            return;
          }
    
          console.log('Received values of form: ', values);
          form.resetFields();
          this.setState({ visible: false });
        });
      }
    
      saveFormRef = (formRef) => {
        this.formRef = formRef;
      }
    
      render() {
        return (
          <div>
            <Button type="primary" onClick={this.showModal}>New Collection</Button>
            <CollectionCreateForm
              wrappedComponentRef={this.saveFormRef}
              visible={this.state.visible}
              onCancel={this.handleCancel}
              onCreate={this.handleCreate}
            />
          </div>
        );
      }
    }
    
    ReactDOM.render(<CollectionsPage />, mountNode);
    
    动态增加、减少表单项。
    expand code expand code
    import { Form, Input, Icon, Button } from 'choerodon-ui';
    const FormItem = Form.Item;
    
    let uuid = 0;
    class DynamicFieldSet extends React.Component {
      remove = (k) => {
        const { form } = this.props;
        // can use data-binding to get
        const keys = form.getFieldValue('keys');
        // We need at least one passenger
        if (keys.length === 1) {
          return;
        }
    
        // can use data-binding to set
        form.setFieldsValue({
          keys: keys.filter(key => key !== k),
        });
      }
    
      add = () => {
        const { form } = this.props;
        // can use data-binding to get
        const keys = form.getFieldValue('keys');
        const nextKeys = keys.concat(uuid);
        uuid++;
        // can use data-binding to set
        // important! notify form to detect changes
        form.setFieldsValue({
          keys: nextKeys,
        });
      }
    
      handleSubmit = (e) => {
        e.preventDefault();
        this.props.form.validateFields((err, values) => {
          if (!err) {
            console.log('Received values of form: ', values);
          }
        });
      }
    
      render() {
        const { getFieldDecorator, getFieldValue } = this.props.form;
        const formItemLayout = {
          labelCol: {
            xs: { span: 24 },
            sm: { span: 4 },
          },
          wrapperCol: {
            xs: { span: 24 },
            sm: { span: 20 },
          },
        };
        const formItemLayoutWithOutLabel = {
          wrapperCol: {
            xs: { span: 24, offset: 0 },
            sm: { span: 20, offset: 4 },
          },
        };
        getFieldDecorator('keys', { initialValue: [] });
        const keys = getFieldValue('keys');
        const formItems = keys.map((k, index) => {
          return (
            <FormItem
              {...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
              label={index === 0 ? 'Passengers' : ''}
              required={false}
              key={k}
            >
              {getFieldDecorator(`names[${k}]`, {
                validateTrigger: ['onChange', 'onBlur'],
                rules: [{
                  required: true,
                  whitespace: true,
                  message: "Please input passenger's name or delete this field.",
                }],
              })(
                <Input placeholder="passenger name" style={{ width: '60%', marginRight: 8 }} />
              )}
              {keys.length > 1 ? (
                <Icon
                  className="dynamic-delete-button"
                  type="minus-circle-o"
                  disabled={keys.length === 1}
                  onClick={() => this.remove(k)}
                />
              ) : null}
            </FormItem>
          );
        });
        return (
          <Form onSubmit={this.handleSubmit}>
            {formItems}
            <FormItem {...formItemLayoutWithOutLabel}>
              <Button type="dashed" onClick={this.add} style={{ width: '60%' }}>
                <Icon type="plus" /> Add field
              </Button>
            </FormItem>
            <FormItem {...formItemLayoutWithOutLabel}>
              <Button type="primary" htmlType="submit">Submit</Button>
            </FormItem>
          </Form>
        );
      }
    }
    
    const WrappedDynamicFieldSet = Form.create()(DynamicFieldSet);
    ReactDOM.render(<WrappedDynamicFieldSet />, mountNode);
    
    时间类组件的 valuemoment 类型,所以在提交前需要预处理。
    expand code expand code
    import { Form, DatePicker, TimePicker, Button } from 'choerodon-ui';
    const FormItem = Form.Item;
    const MonthPicker = DatePicker.MonthPicker;
    const RangePicker = DatePicker.RangePicker;
    
    class TimeRelatedForm extends React.Component {
      handleSubmit = (e) => {
        e.preventDefault();
    
        this.props.form.validateFields((err, fieldsValue) => {
          if (err) {
            return;
          }
    
          // Should format date value before submit.
          const rangeValue = fieldsValue['range-picker'];
          const rangeTimeValue = fieldsValue['range-time-picker'];
          const values = {
            ...fieldsValue,
            'date-picker': fieldsValue['date-picker'].format('YYYY-MM-DD'),
            'date-time-picker': fieldsValue['date-time-picker'].format('YYYY-MM-DD HH:mm:ss'),
            'month-picker': fieldsValue['month-picker'].format('YYYY-MM'),
            'range-picker': [rangeValue[0].format('YYYY-MM-DD'), rangeValue[1].format('YYYY-MM-DD')],
            'range-time-picker': [
              rangeTimeValue[0].format('YYYY-MM-DD HH:mm:ss'),
              rangeTimeValue[1].format('YYYY-MM-DD HH:mm:ss'),
            ],
            'time-picker': fieldsValue['time-picker'].format('HH:mm:ss'),
          };
          console.log('Received values of form: ', values);
        });
      }
    
      render() {
        const { getFieldDecorator } = this.props.form;
        const formItemLayout = {
          labelCol: {
            xs: { span: 24 },
            sm: { span: 8 },
          },
          wrapperCol: {
            xs: { span: 24 },
            sm: { span: 16 },
          },
        };
        const config = {
          rules: [{ type: 'object', required: true, message: 'Please select time!' }],
        };
        const rangeConfig = {
          rules: [{ type: 'array', required: true, message: 'Please select time!' }],
        };
        return (
          <Form onSubmit={this.handleSubmit}>
            <FormItem
              {...formItemLayout}
              label="DatePicker"
            >
              {getFieldDecorator('date-picker', config)(
                <DatePicker />
              )}
            </FormItem>
            <FormItem
              {...formItemLayout}
              label="DatePicker[showTime]"
            >
              {getFieldDecorator('date-time-picker', config)(
                <DatePicker showTime format="YYYY-MM-DD HH:mm:ss" />
              )}
            </FormItem>
            <FormItem
              {...formItemLayout}
              label="MonthPicker"
            >
              {getFieldDecorator('month-picker', config)(
                <MonthPicker />
              )}
            </FormItem>
            <FormItem
              {...formItemLayout}
              label="RangePicker"
            >
              {getFieldDecorator('range-picker', rangeConfig)(
                <RangePicker />
              )}
            </FormItem>
            <FormItem
              {...formItemLayout}
              label="RangePicker[showTime]"
            >
              {getFieldDecorator('range-time-picker', rangeConfig)(
                <RangePicker showTime format="YYYY-MM-DD HH:mm:ss" />
              )}
            </FormItem>
            <FormItem
              {...formItemLayout}
              label="TimePicker"
            >
              {getFieldDecorator('time-picker', config)(
                <TimePicker />
              )}
            </FormItem>
            <FormItem
              wrapperCol={{
                xs: { span: 24, offset: 0 },
                sm: { span: 16, offset: 8 },
              }}
            >
              <Button type="primary" htmlType="submit">Submit</Button>
            </FormItem>
          </Form>
        );
      }
    }
    
    const WrappedTimeRelatedForm = Form.create()(TimeRelatedForm);
    
    ReactDOM.render(<WrappedTimeRelatedForm />, mountNode);
    
    自定义或第三方的表单控件,也可以与 Form 组件一起使用。只要该组件遵循以下的约定: > * 提供受控属性 value 或其它与 valuePropName的值同名的属性。 > * 提供 onChange 事件或 trigger的值同名的事件。 > * 不能是函数式组件。
    expand code expand code
    import { Form, Input, Select, Button } from 'choerodon-ui';
    const FormItem = Form.Item;
    const Option = Select.Option;
    
    class PriceInput extends React.Component {
      constructor(props) {
        super(props);
    
        const value = this.props.value || {};
        this.state = {
          number: value.number || 0,
          currency: value.currency || 'rmb',
        };
      }
    
      componentWillReceiveProps(nextProps) {
        // Should be a controlled component.
        if ('value' in nextProps) {
          const value = nextProps.value;
          this.setState(value);
        }
      }
    
      handleNumberChange = (e) => {
        const number = parseInt(e.target.value || 0, 10);
        if (isNaN(number)) {
          return;
        }
        if (!('value' in this.props)) {
          this.setState({ number });
        }
        this.triggerChange({ number });
      }
    
      handleCurrencyChange = (currency) => {
        if (!('value' in this.props)) {
          this.setState({ currency });
        }
        this.triggerChange({ currency });
      }
    
      triggerChange = (changedValue) => {
        // Should provide an event to pass value to Form.
        const onChange = this.props.onChange;
        if (onChange) {
          onChange(Object.assign({}, this.state, changedValue));
        }
      }
    
      render() {
        const { size } = this.props;
        const state = this.state;
        return (
          <span>
            <Input
              type="text"
              size={size}
              value={state.number}
              onChange={this.handleNumberChange}
              style={{ width: '65%', marginRight: '3%' }}
            />
            <Select
              value={state.currency}
              size={size}
              style={{ width: '32%' }}
              onChange={this.handleCurrencyChange}
            >
              <Option value="rmb">RMB</Option>
              <Option value="dollar">Dollar</Option>
            </Select>
          </span>
        );
      }
    }
    
    class Demo extends React.Component {
      handleSubmit = (e) => {
        e.preventDefault();
        this.props.form.validateFields((err, values) => {
          if (!err) {
            console.log('Received values of form: ', values);
          }
        });
      }
    
      checkPrice = (rule, value, callback) => {
        if (value.number > 0) {
          callback();
          return;
        }
        callback('Price must greater than zero!');
      }
    
      render() {
        const { getFieldDecorator } = this.props.form;
        return (
          <Form layout="inline" onSubmit={this.handleSubmit}>
            <FormItem label="Price">
              {getFieldDecorator('price', {
                initialValue: { number: 0, currency: 'rmb' },
                rules: [{ validator: this.checkPrice }],
              })(<PriceInput />)}
            </FormItem>
            <FormItem>
              <Button type="primary" htmlType="submit">Submit</Button>
            </FormItem>
          </Form>
        );
      }
    }
    
    const WrappedDemo = Form.create()(Demo);
    
    ReactDOM.render(<WrappedDemo />, mountNode);
    
    通过使用 onFieldsChangemapPropsToFields,可以把表单的数据存储到上层组件或者 Reduxdva 中。
    expand code expand code
    import { Form, Input } from 'choerodon-ui';
    const FormItem = Form.Item;
    
    const CustomizedForm = Form.create({
      onFieldsChange(props, changedFields) {
        props.onChange(changedFields);
      },
      mapPropsToFields(props) {
        return {
          username: Form.createFormField({
            ...props.username,
            value: props.username.value,
          }),
        };
      },
      onValuesChange(_, values) {
        console.log(values);
      },
    })((props) => {
      const { getFieldDecorator } = props.form;
      return (
        <Form layout="inline">
          <FormItem label="Username">
            {getFieldDecorator('username', {
              rules: [{ required: true, message: 'Username is required!' }],
            })(<Input />)}
          </FormItem>
        </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>
            <CustomizedForm {...fields} onChange={this.handleFormChange} />
            <pre className="language-bash">
              {JSON.stringify(fields, null, 2)}
            </pre>
          </div>
        );
      }
    }
    
    ReactDOM.render(<Demo />, mountNode);
    
    使用 Form.create 处理后的表单具有自动收集数据并校验的功能,但如果您不需要这个功能,或者默认的行为无法满足业务需求,可以选择不使用 Form.create 并自行处理数据。
    expand code expand code
    import { Form, InputNumber } from 'choerodon-ui';
    const FormItem = Form.Item;
    
    function validatePrimeNumber(number) {
      if (number === 11) {
        return {
          validateStatus: 'success',
          errorMsg: null,
        };
      }
      return {
        validateStatus: 'error',
        errorMsg: 'The prime between 8 and 12 is 11!',
      };
    }
    
    class RawForm extends React.Component {
      state = {
        number: {
          value: 11,
        },
      }
    
      handleNumberChange = (value) => {
        this.setState({
          number: {
            ...validatePrimeNumber(value),
            value,
          },
        });
      }
    
      render() {
        const formItemLayout = {
          labelCol: { span: 7 },
          wrapperCol: { span: 12 },
        };
        const number = this.state.number;
        const tips = 'A prime is a natural number greater than 1 that has no positive divisors other than 1 and itself.';
        return (
          <Form>
            <FormItem
              {...formItemLayout}
              label="Prime between 8 & 12"
              validateStatus={number.validateStatus}
              help={number.errorMsg || tips}
            >
              <InputNumber
                min={8}
                max={12}
                value={number.value}
                onChange={this.handleNumberChange}
              />
            </FormItem>
          </Form>
        );
      }
    }
    
    ReactDOM.render(<RawForm />, mountNode);
    
    我们提供了 validateStatus help hasFeedback 等属性,你可以不需要使用 Form.creategetFieldDecorator,自己定义校验的时机和内容。

    1. validateStatus: 校验状态,可选 ‘success’, ‘warning’, ‘error’, ‘validating’。
    2. hasFeedback:用于给输入框添加反馈图标。
    3. help:设置校验文案。
    expand code expand code
    import { Form, Input, DatePicker, Col, TimePicker, Select, Cascader, InputNumber } from 'choerodon-ui';
    const FormItem = Form.Item;
    const Option = Select.Option;
    
    const formItemLayout = {
      labelCol: {
        xs: { span: 24 },
        sm: { span: 5 },
      },
      wrapperCol: {
        xs: { span: 24 },
        sm: { span: 12 },
      },
    };
    
    ReactDOM.render(
      <Form>
        <FormItem
          {...formItemLayout}
          label="Fail"
          validateStatus="error"
          help="Should be combination of numbers & alphabets"
        >
          <Input placeholder="unavailable choice" id="error" />
        </FormItem>
    
        <FormItem
          {...formItemLayout}
          label="Warning"
          validateStatus="warning"
        >
          <Input placeholder="Warning" id="warning" />
        </FormItem>
    
        <FormItem
          {...formItemLayout}
          label="Validating"
          hasFeedback
          validateStatus="validating"
          help="The information is being validated..."
        >
          <Input placeholder="I'm the content is being validated" id="validating" />
        </FormItem>
    
        <FormItem
          {...formItemLayout}
          label="Success"
          hasFeedback
          validateStatus="success"
        >
          <Input placeholder="I'm the content" id="success" />
        </FormItem>
    
        <FormItem
          {...formItemLayout}
          label="Warning"
          hasFeedback
          validateStatus="warning"
        >
          <Input placeholder="Warning" id="warning" />
        </FormItem>
    
        <FormItem
          {...formItemLayout}
          label="Fail"
          hasFeedback
          validateStatus="error"
          help="Should be combination of numbers & alphabets"
        >
          <Input placeholder="unavailable choice" id="error" />
        </FormItem>
    
        <FormItem
          {...formItemLayout}
          label="Success"
          hasFeedback
          validateStatus="success"
        >
          <DatePicker style={{ width: '100%' }} />
        </FormItem>
    
        <FormItem
          {...formItemLayout}
          label="Warning"
          hasFeedback
          validateStatus="warning"
        >
          <TimePicker style={{ width: '100%' }} />
        </FormItem>
    
        <FormItem
          {...formItemLayout}
          label="Error"
          hasFeedback
          validateStatus="error"
        >
          <Select defaultValue="1">
            <Option value="1">Option 1</Option>
            <Option value="2">Option 2</Option>
            <Option value="3">Option 3</Option>
          </Select>
        </FormItem>
    
        <FormItem
          {...formItemLayout}
          label="Validating"
          hasFeedback
          validateStatus="validating"
          help="The information is being validated..."
        >
          <Cascader defaultValue={['1']} options={[]} />
        </FormItem>
    
        <FormItem
          label="inline"
          {...formItemLayout}
        >
          <Col span={11}>
            <FormItem validateStatus="error" help="Please select the correct date">
              <DatePicker />
            </FormItem>
          </Col>
          <Col span={2}>
            <span style={{ display: 'inline-block', width: '100%', textAlign: 'center' }}>
              -
            </span>
          </Col>
          <Col span={11}>
            <FormItem>
              <DatePicker />
            </FormItem>
          </Col>
        </FormItem>
    
        <FormItem
          {...formItemLayout}
          label="Success"
          hasFeedback
          validateStatus="success"
        >
          <InputNumber style={{ width: '100%' }} />
        </FormItem>
      </Form>,
      mountNode);
    
    使用 setFieldsValue 来动态设置其他控件的值。
    expand code expand code
    import { Form, Select, Input, Button } from 'choerodon-ui';
    const FormItem = Form.Item;
    const Option = Select.Option;
    
    class App extends React.Component {
      handleSubmit = (e) => {
        e.preventDefault();
        this.props.form.validateFields((err, values) => {
          if (!err) {
            console.log('Received values of form: ', values);
          }
        });
      }
    
      handleSelectChange = (value) => {
        console.log(value);
        this.props.form.setFieldsValue({
          note: `Hi, ${value === 'male' ? 'man' : 'lady'}!`,
        });
      }
    
      render() {
        const { getFieldDecorator } = this.props.form;
        return (
          <Form onSubmit={this.handleSubmit}>
            <FormItem
              label="Note"
              labelCol={{ span: 5 }}
              wrapperCol={{ span: 12 }}
            >
              {getFieldDecorator('note', {
                rules: [{ required: true, message: 'Please input your note!' }],
              })(
                <Input />
              )}
            </FormItem>
            <FormItem
              label="Gender"
              labelCol={{ span: 5 }}
              wrapperCol={{ span: 12 }}
            >
              {getFieldDecorator('gender', {
                rules: [{ required: true, message: 'Please select your gender!' }],
              })(
                <Select
                  placeholder="Select a option and change input text above"
                  onChange={this.handleSelectChange}
                >
                  <Option value="male">male</Option>
                  <Option value="female">female</Option>
                </Select>
              )}
            </FormItem>
            <FormItem
              wrapperCol={{ span: 12, offset: 5 }}
            >
              <Button type="primary" htmlType="submit">
                Submit
              </Button>
            </FormItem>
          </Form>
        );
      }
    }
    
    const WrappedApp = Form.create()(App);
    
    ReactDOM.render(<WrappedApp />, mountNode);
    
    表单有三种布局。
    expand code expand code
    import { Form, Input, Button, Radio } from 'choerodon-ui';
    const FormItem = Form.Item;
    
    class FormLayoutDemo extends React.Component {
      constructor() {
        super();
        this.state = {
          formLayout: 'horizontal',
        };
      }
    
      handleFormLayoutChange = (e) => {
        this.setState({ formLayout: e.target.value });
      }
    
      render() {
        const { formLayout } = this.state;
        const formItemLayout = formLayout === 'horizontal' ? {
          labelCol: { span: 4 },
          wrapperCol: { span: 14 },
        } : null;
        const buttonItemLayout = formLayout === 'horizontal' ? {
          wrapperCol: { span: 14, offset: 4 },
        } : null;
        return (
          <div>
            <Form layout={formLayout}>
              <FormItem
                label="Form Layout"
                {...formItemLayout}
              >
                <Radio.Group defaultValue="horizontal" onChange={this.handleFormLayoutChange}>
                  <Radio.Button value="horizontal">Horizontal</Radio.Button>
                  <Radio.Button value="vertical">Vertical</Radio.Button>
                  <Radio.Button value="inline">Inline</Radio.Button>
                </Radio.Group>
              </FormItem>
              <FormItem
                label="Field A"
                {...formItemLayout}
              >
                <Input placeholder="input placeholder" />
              </FormItem>
              <FormItem
                label="Field B"
                {...formItemLayout}
              >
                <Input placeholder="input placeholder" />
              </FormItem>
              <FormItem {...buttonItemLayout}>
                <Button type="primary">Submit</Button>
              </FormItem>
            </Form>
          </div>
        );
      }
    }
    
    ReactDOM.render(<FormLayoutDemo />, mountNode);
    
    根据不同情况执行不同的校验规则。
    expand code expand code
    import { Form, Input, Button, Checkbox } from 'choerodon-ui';
    const FormItem = Form.Item;
    
    const formItemLayout = {
      labelCol: { span: 4 },
      wrapperCol: { span: 8 },
    };
    const formTailLayout = {
      labelCol: { span: 4 },
      wrapperCol: { span: 8, offset: 4 },
    };
    class DynamicRule extends React.Component {
      state = {
        checkNick: false,
      };
    
      check = () => {
        this.props.form.validateFields(
          (err) => {
            if (!err) {
              console.info('success');
            }
          },
        );
      }
    
      handleChange = (e) => {
        this.setState({
          checkNick: e.target.checked,
        }, () => {
          this.props.form.validateFields(['nickname'], { force: true });
        });
      }
    
      render() {
        const { getFieldDecorator } = this.props.form;
        return (
          <div>
            <FormItem {...formItemLayout} label="Name">
              {getFieldDecorator('username', {
                rules: [{
                  required: true,
                  message: 'Please input your name',
                }],
              })(
                <Input placeholder="Please input your name" />
              )}
            </FormItem>
            <FormItem {...formItemLayout} label="Nickname">
              {getFieldDecorator('nickname', {
                rules: [{
                  required: this.state.checkNick,
                  message: 'Please input your nickname',
                }],
              })(
                <Input placeholder="Please input your nickname" />
              )}
            </FormItem>
            <FormItem {...formTailLayout}>
              <Checkbox
                value={this.state.checkNick}
                onChange={this.handleChange}
              >
                Nickname is required
              </Checkbox>
            </FormItem>
            <FormItem {...formTailLayout}>
              <Button type="primary" onClick={this.check}>
                Check
              </Button>
            </FormItem>
          </div>
        );
      }
    }
    
    const WrappedDynamicRule = Form.create()(DynamicRule);
    ReactDOM.render(<WrappedDynamicRule />, mountNode);
    
    以上演示没有出现的表单控件对应的校验演示。
    expand code expand code
    import {
      Form, Select, InputNumber, Switch, Radio,
      Slider, Button, Upload, Icon, Rate,
    } from 'choerodon-ui';
    const FormItem = Form.Item;
    const Option = Select.Option;
    const RadioButton = Radio.Button;
    const RadioGroup = Radio.Group;
    
    class Demo extends React.Component {
      handleSubmit = (e) => {
        e.preventDefault();
        this.props.form.validateFields((err, values) => {
          if (!err) {
            console.log('Received values of form: ', values);
          }
        });
      }
    
      normFile = (e) => {
        console.log('Upload event:', e);
        if (Array.isArray(e)) {
          return e;
        }
        return e && e.fileList;
      }
    
      render() {
        const { getFieldDecorator } = this.props.form;
        const formItemLayout = {
          labelCol: { span: 6 },
          wrapperCol: { span: 14 },
        };
        return (
          <Form onSubmit={this.handleSubmit}>
            <FormItem
              {...formItemLayout}
              label="Plain Text"
            >
              <span className="ant-form-text">China</span>
            </FormItem>
            <FormItem
              {...formItemLayout}
              label="Select"
              hasFeedback
            >
              {getFieldDecorator('select', {
                rules: [
                  { required: true, message: 'Please select your country!' },
                ],
              })(
                <Select placeholder="Please select a country">
                  <Option value="china">China</Option>
                  <Option value="use">U.S.A</Option>
                </Select>
              )}
            </FormItem>
    
            <FormItem
              {...formItemLayout}
              label="Select[multiple]"
            >
              {getFieldDecorator('select-multiple', {
                rules: [
                  { required: true, message: 'Please select your favourite colors!', type: 'array' },
                ],
              })(
                <Select mode="multiple" placeholder="Please select favourite colors">
                  <Option value="red">Red</Option>
                  <Option value="green">Green</Option>
                  <Option value="blue">Blue</Option>
                </Select>
              )}
            </FormItem>
    
            <FormItem
              {...formItemLayout}
              label="InputNumber"
            >
              {getFieldDecorator('input-number', { initialValue: 3 })(
                <InputNumber min={1} max={10} />
              )}
              <span className="ant-form-text"> machines</span>
            </FormItem>
    
            <FormItem
              {...formItemLayout}
              label="Switch"
            >
              {getFieldDecorator('switch', { valuePropName: 'checked' })(
                <Switch />
              )}
            </FormItem>
    
            <FormItem
              {...formItemLayout}
              label="Slider"
            >
              {getFieldDecorator('slider')(
                <Slider marks={{ 0: 'A', 20: 'B', 40: 'C', 60: 'D', 80: 'E', 100: 'F' }} />
              )}
            </FormItem>
    
            <FormItem
              {...formItemLayout}
              label="Radio.Group"
            >
              {getFieldDecorator('radio-group')(
                <RadioGroup>
                  <Radio value="a">item 1</Radio>
                  <Radio value="b">item 2</Radio>
                  <Radio value="c">item 3</Radio>
                </RadioGroup>
              )}
            </FormItem>
    
            <FormItem
              {...formItemLayout}
              label="Radio.Button"
            >
              {getFieldDecorator('radio-button')(
                <RadioGroup>
                  <RadioButton value="a">item 1</RadioButton>
                  <RadioButton value="b">item 2</RadioButton>
                  <RadioButton value="c">item 3</RadioButton>
                </RadioGroup>
              )}
            </FormItem>
    
            <FormItem
              {...formItemLayout}
              label="Rate"
            >
              {getFieldDecorator('rate', {
                initialValue: 3.5,
              })(
                <Rate />
              )}
            </FormItem>
    
            <FormItem
              {...formItemLayout}
              label="Upload"
              extra="longgggggggggggggggggggggggggggggggggg"
            >
              {getFieldDecorator('upload', {
                valuePropName: 'fileList',
                getValueFromEvent: this.normFile,
              })(
                <Upload name="logo" action="/upload.do" listType="picture">
                  <Button>
                    <Icon type="upload" /> Click to upload
                  </Button>
                </Upload>
              )}
            </FormItem>
    
            <FormItem
              {...formItemLayout}
              label="Dragger"
            >
              <div className="dropbox">
                {getFieldDecorator('dragger', {
                  valuePropName: 'fileList',
                  getValueFromEvent: this.normFile,
                })(
                  <Upload.Dragger name="files" action="/upload.do">
                    <p className="ant-upload-drag-icon">
                      <Icon type="inbox" />
                    </p>
                    <p className="ant-upload-text">Click or drag file to this area to upload</p>
                    <p className="ant-upload-hint">Support for a single or bulk upload.</p>
                  </Upload.Dragger>
                )}
              </div>
            </FormItem>
    
            <FormItem
              wrapperCol={{ span: 12, offset: 6 }}
            >
              <Button type="primary" htmlType="submit">Submit</Button>
            </FormItem>
          </Form>
        );
      }
    }
    
    const WrappedDemo = Form.create()(Demo);
    
    ReactDOM.render(<WrappedDemo />, mountNode);
    

    API

    Form

    更多示例参考 rc-form

    参数说明类型默认值
    formForm.create() 包装过的组件会自带 this.props.form 属性,直接传给 Form 即可。1.7.0 之后无需设置object
    hideRequiredMark隐藏所有表单项的必选标记Booleanfalse
    layout表单布局(2.8 之后支持)‘horizontal’|‘vertical’|‘inline’‘horizontal’
    onSubmit数据验证成功后回调事件Function(e:Event)

    Form.create(options)

    使用方式如下:

    class CustomizedForm extends React.Component {}
    
    CustomizedForm = Form.create({})(CustomizedForm);
    

    options 的配置项如下。

    参数说明类型
    mapPropsToFields把父组件的属性映射到表单项上(如:把 Redux store 中的值读出),需要对返回值中的表单域数据用 Form.createFormField 标记(props) => Object{ fieldName: FormField { value } }
    validateMessages默认校验信息,可用于把默认错误信息改为中文等,格式与 newMessages 返回值一致Object { [nested.path]: String }
    onFieldsChangeForm.Item 子节点的值发生改变时触发,可以把对应的值转存到 Redux storeFunction(props, fields)
    onValuesChange任一表单域的值发生改变时的回调(props, values) => void

    经过 Form.create 包装的组件将会自带 this.props.form 属性,this.props.form 提供的 API 如下:

    注意:使用 getFieldsValue getFieldValue setFieldsValue 等时,应确保对应的 field 已经用 getFieldDecorator 注册过了。

    方法     说明                                    类型      
    getFieldDecorator用于和表单进行双向绑定,详见下方描述
    getFieldError获取某个输入控件的 ErrorFunction(name)
    getFieldsError获取一组输入控件的 Error ,如不传入参数,则获取全部组件的 ErrorFunction([names: string[]])
    getFieldsValue获取一组输入控件的值,如不传入参数,则获取全部组件的值Function([fieldNames: string[]])
    getFieldValue获取一个输入控件的值Function(fieldName: string)
    isFieldsTouched判断是否任一输入控件经历过 getFieldDecorator 的值收集时机 options.trigger(names?: string[]) => boolean
    isFieldTouched判断一个输入控件是否经历过 getFieldDecorator 的值收集时机 options.trigger(name: string) => boolean
    isFieldValidating判断一个输入控件是否在校验状态Function(name)
    resetFields重置一组输入控件的值(为 initialValue)与状态,如不传入参数,则重置所有组件Function([names: string[]])
    setFields设置一组输入控件的值与 Error。 代码Function({ [fieldName]: { value: any, errors: [Error] } })
    setFieldsValue设置一组输入控件的值(注意:不要在 componentWillReceiveProps 内使用,否则会导致死循环,更多Function({ [fieldName]: value }
    validateFields校验并获取一组输入域的值与 Error,若 fieldNames 参数为空,则校验全部组件Function([fieldNames: string[]], [options: object], callback: Function(errors, values, modify))
    validateFieldsAndScrollvalidateFields 相似,但校验完后,如果校验不通过的菜单域不在可见范围内,则自动滚动进可见范围参考 validateFields
    isModifiedFields判断一组输入控件的值是否有修改(names?: string[]) => boolean
    isModifiedField判断一个输入控件的值是否有修改(name: string) => boolean

    this.props.form.validateFields/validateFieldsAndScroll([fieldNames: string[]], [options: object], callback: Function(errors, values))

    参数说明类型默认值
    options.first若为 true,则每一表单域的都会在碰到第一个失败了的校验规则后停止校验booleanfalse
    options.firstFields指定表单域会在碰到第一个失败了的校验规则后停止校验String[][]
    options.force对已经校验过的表单域,在 validateTrigger 再次被触发时是否再次校验booleanfalse
    options.scroll定义 validateFieldsAndScroll 的滚动行为,详细配置见 dom-scroll-into-view configObject{}

    Form.createFormField

    用于标记 mapPropsToFields 返回的表单域数据,例子

    this.props.form.getFieldDecorator(id, options)

    经过 getFieldDecorator 包装的控件,表单控件会自动添加 value(或 valuePropName 指定的其他属性) onChange(或 trigger 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:

    1. 不再需要也不应该onChange 来做同步,但还是可以继续监听 onChange 等事件。
    2. 你不能用控件的 value defaultValue 等属性来设置表单域的值,默认值可以用 getFieldDecorator 里的 initialValue
    3. 你不应该用 setState,可以使用 this.props.form.setFieldsValue 来动态改变表单值。

    特别注意

    1. getFieldDecorator 不能用于装饰纯函数组件。
    2. 如果使用的是 react@<15.3.0,则 getFieldDecorator 调用不能位于纯函数组件中: https://github.com/facebook/react/pull/6534

    getFieldDecorator(id, options) 参数

    参数说明类型默认值
    id必填输入控件唯一标志。支持嵌套式的写法string
    options.getValueFromEvent可以把 onChange 的参数(如 event)转化为控件的值function(..args)reference
    options.initialValue子节点的初始值,类型、可选值均由子节点决定(注意:由于内部校验时使用 === 判断是否变化,建议使用变量缓存所需设置的值而非直接使用字面量))
    options.normalize转换默认的 value 给控件,一个选择全部的例子function(value, prevValue, allValues): any-
    options.rules校验规则,参考下方文档object[]
    options.trigger收集子节点的值的时机string‘onChange’
    options.validateFirst当某一规则校验不通过时,是否停止剩下的规则的校验booleanfalse
    options.validateTrigger校验子节点值的时机string|string[]‘onChange’
    options.valuePropName子节点的值的属性,如 Switch 的是 ‘checked’string‘value’

    更多参数请查看 rc-form option

    Form.Item

    注意:

    参数说明类型默认值
    colon配合 label 属性使用,表示是否显示 label 后面的冒号booleantrue
    extra额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。string|ReactNode
    hasFeedback配合 validateStatus 属性使用,展示校验状态图标,建议只配合 Input 组件使用booleanfalse
    help提示信息,如不设置,则会根据校验规则自动生成string|ReactNode
    labellabel 标签的文本string|ReactNode
    labelCollabel 标签布局,同 <Col> 组件,设置 span offset 值,如 {span: 3, offset: 12}sm: {span: 3, offset: 12}object
    required是否必填,如不设置,则会根据校验规则自动生成booleanfalse
    validateStatus校验状态,如不设置,则会根据校验规则自动生成,可选:’success’ ‘warning’ ‘error’ ‘validating’string
    wrapperCol需要为输入控件设置布局样式时,使用该属性,用法同 labelColobject

    校验规则

    参数说明类型默认值
    enum枚举类型string-
    len字段长度number-
    max最大长度number-
    message校验文案string-
    min最小长度number-
    pattern正则表达式校验RegExp-
    required是否必选booleanfalse
    transform校验前转换字段值function(value) => transformedValue:any-
    type内建校验类型,可选项string‘string’
    validator自定义校验(注意,callback 必须被调用function(rule, value, callback)-
    whitespace必选时,空格是否会被视为错误booleanfalse

    更多高级用法可研究 async-validator

    在 TypeScript 中使用

    import { Form } from 'choerodon-ui';
    import { FormComponentProps } from 'choerodon-ui/lib/form';
    
    interface UserFormProps extends FormComponentProps {
      age: number;
      name: string;
    }
    
    class UserForm extends React.Component<UserFormProps, any> {
    
    }