From d1f017954e38a3ddf2b2514f66a5452e39795d89 Mon Sep 17 00:00:00 2001 From: tjz <415800467@qq.com> Date: Sat, 5 May 2018 17:00:51 +0800 Subject: [PATCH] feat: add form --- components/_util/props-util.js | 4 +- components/form/Form.jsx | 193 +++++++ components/form/FormItem.jsx | 333 ++++++++++++ components/form/constants.jsx | 2 + components/form/demo/test.vue | 65 +++ components/form/index.en-US.md | 181 +++++++ components/form/index.jsx | 6 + components/form/index.zh-CN.md | 182 +++++++ components/form/style/index.js | 5 + components/form/style/index.less | 624 ++++++++++++++++++++++ components/form/style/mixin.less | 106 ++++ components/index.js | 5 +- components/input/inputProps.js | 1 - components/style.js | 1 + components/vc-form/src/createBaseForm.jsx | 15 +- site/components.js | 5 +- site/routes.js | 2 +- 17 files changed, 1720 insertions(+), 10 deletions(-) create mode 100755 components/form/Form.jsx create mode 100644 components/form/FormItem.jsx create mode 100644 components/form/constants.jsx create mode 100644 components/form/demo/test.vue create mode 100644 components/form/index.en-US.md create mode 100644 components/form/index.jsx create mode 100644 components/form/index.zh-CN.md create mode 100644 components/form/style/index.js create mode 100644 components/form/style/index.less create mode 100644 components/form/style/mixin.less diff --git a/components/_util/props-util.js b/components/_util/props-util.js index 6211c7bc5..1b9394a53 100644 --- a/components/_util/props-util.js +++ b/components/_util/props-util.js @@ -40,9 +40,9 @@ const filterProps = (props, propsData = {}) => { return res } const getSlots = (ele) => { - let componentOptions = ele.componentOptions + let componentOptions = ele.componentOptions || {} if (ele.$vnode) { - componentOptions = ele.$vnode.componentOptions + componentOptions = ele.$vnode.componentOptions || {} } const children = componentOptions.children || [] const slots = {} diff --git a/components/form/Form.jsx b/components/form/Form.jsx new file mode 100755 index 000000000..63de4a8e3 --- /dev/null +++ b/components/form/Form.jsx @@ -0,0 +1,193 @@ +import PropTypes from '../_util/vue-types' +import classNames from 'classnames' +import isRegExp from 'lodash/isRegExp' +import createDOMForm from '../vc-form/src/createDOMForm' +import createFormField from '../vc-form/src/createFormField' +import FormItem from './FormItem' +import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants' +import { initDefaultProps } from '../_util/props-util' + +export const FormCreateOption = { + onFieldsChange: PropTypes.func, + onValuesChange: PropTypes.func, + mapPropsToFields: PropTypes.func, + withRef: PropTypes.bool, +} + +// function create +export const WrappedFormUtils = { + /** 获取一组输入控件的值,如不传入参数,则获取全部组件的值 */ + getFieldsValue: PropTypes.func, + /** 获取一个输入控件的值*/ + getFieldValue: PropTypes.func, + /** 设置一组输入控件的值*/ + setFieldsValue: PropTypes.func, + /** 设置一组输入控件的值*/ + setFields: PropTypes.func, + /** 校验并获取一组输入域的值与 Error */ + validateFields: PropTypes.func, + // validateFields(fieldNames: Array, options: Object, callback: ValidateCallback): void; + // validateFields(fieldNames: Array, callback: ValidateCallback): void; + // validateFields(options: Object, callback: ValidateCallback): void; + // validateFields(callback: ValidateCallback): void; + // validateFields(): void; + /** 与 `validateFields` 相似,但校验完后,如果校验不通过的菜单域不在可见范围内,则自动滚动进可见范围 */ + validateFieldsAndScroll: PropTypes.func, + // validateFieldsAndScroll(fieldNames?: Array, options?: Object, callback?: ValidateCallback): void; + // validateFieldsAndScroll(fieldNames?: Array, callback?: ValidateCallback): void; + // validateFieldsAndScroll(options?: Object, callback?: ValidateCallback): void; + // validateFieldsAndScroll(callback?: ValidateCallback): void; + // validateFieldsAndScroll(): void; + /** 获取某个输入控件的 Error */ + getFieldError: PropTypes.func, + getFieldsError: PropTypes.func, + /** 判断一个输入控件是否在校验状态*/ + isFieldValidating: PropTypes.func, + isFieldTouched: PropTypes.func, + isFieldsTouched: PropTypes.func, + /** 重置一组输入控件的值与状态,如不传入参数,则重置所有组件 */ + resetFields: PropTypes.func, + + getFieldDecorator: PropTypes.func, +} + +export const FormProps = { + layout: PropTypes.oneOf(['horizontal', 'inline', 'vertical']), + form: PropTypes.shape(WrappedFormUtils).loose, + // onSubmit: React.FormEventHandler; + prefixCls: PropTypes.string, + hideRequiredMark: PropTypes.bool, +} + +export const ValidationRule = { + /** validation error message */ + message: PropTypes.string, + /** built-in validation type, available options: https://github.com/yiminghe/async-validator#type */ + type: PropTypes.string, + /** indicates whether field is required */ + required: PropTypes.boolean, + /** treat required fields that only contain whitespace as errors */ + whitespace: PropTypes.boolean, + /** validate the exact length of a field */ + len: PropTypes.number, + /** validate the min length of a field */ + min: PropTypes.number, + /** validate the max length of a field */ + max: PropTypes.number, + /** validate the value from a list of possible values */ + enum: PropTypes.oneOfType([String, PropTypes.arrayOf(String)]), + /** validate from a regular expression */ + pattern: PropTypes.custom(isRegExp), + /** transform a value before validation */ + transform: PropTypes.func, + /** custom validate function (Note: callback must be called) */ + validator: PropTypes.func, +} + +// export type ValidateCallback = (errors: any, values: any) => void; + +// export type GetFieldDecoratorOptions = { +// /** 子节点的值的属性,如 Checkbox 的是 'checked' */ +// valuePropName?: string; +// /** 子节点的初始值,类型、可选值均由子节点决定 */ +// initialValue?: any; +// /** 收集子节点的值的时机 */ +// trigger?: string; +// /** 可以把 onChange 的参数转化为控件的值,例如 DatePicker 可设为:(date, dateString) => dateString */ +// getValueFromEvent?: (...args: any[]) => any; +// /** 校验子节点值的时机 */ +// validateTrigger?: string | string[]; +// /** 校验规则,参见 [async-validator](https://github.com/yiminghe/async-validator) */ +// rules?: ValidationRule[]; +// /** 是否和其他控件互斥,特别用于 Radio 单选控件 */ +// exclusive?: boolean; +// /** Normalize value to form component */ +// normalize?: (value: any, prevValue: any, allValues: any) => any; +// /** Whether stop validate on first rule of error for this field. */ +// validateFirst?: boolean; +// }; + +export default { + name: 'AForm', + props: initDefaultProps(FormProps, { + prefixCls: 'ant-form', + layout: 'horizontal', + hideRequiredMark: false, + }), + // static defaultProps = { + // prefixCls: 'ant-form', + // layout: 'horizontal', + // hideRequiredMark: false, + // onSubmit (e) { + // e.preventDefault() + // }, + // }; + + // static propTypes = { + // prefixCls: PropTypes.string, + // layout: PropTypes.oneOf(['horizontal', 'inline', 'vertical']), + // children: PropTypes.any, + // onSubmit: PropTypes.func, + // hideRequiredMark: PropTypes.bool, + // }; + + Item: FormItem, + + createFormField: createFormField, + + create: (options = {}) => { + return createDOMForm({ + fieldNameProp: 'id', + ...options, + fieldMetaProp: FIELD_META_PROP, + fieldDataProp: FIELD_DATA_PROP, + }) + }, + + // constructor (props) { + // super(props) + + // warning(!props.form, 'It is unnecessary to pass `form` to `Form` after antd@1.7.0.') + // } + + // shouldComponentUpdate(...args) { + // return PureRenderMixin.shouldComponentUpdate.apply(this, args); + // } + + // getChildContext () { + // const { layout } = this.props + // return { + // vertical: layout === 'vertical', + // } + // }, + provide () { + return { + FormProps: this.$props, + } + }, + methods: { + onSubmit (e) { + const { $listeners } = this + if (!$listeners.submit) { + e.preventDefault() + } else { + this.$emit('submit', e) + } + }, + }, + + render () { + const { + prefixCls, hideRequiredMark, layout, onSubmit, $slots, + } = this + + const formClassName = classNames(prefixCls, { + [`${prefixCls}-horizontal`]: layout === 'horizontal', + [`${prefixCls}-vertical`]: layout === 'vertical', + [`${prefixCls}-inline`]: layout === 'inline', + [`${prefixCls}-hide-required-mark`]: hideRequiredMark, + }) + + return
{$slots.default}
+ }, +} diff --git a/components/form/FormItem.jsx b/components/form/FormItem.jsx new file mode 100644 index 000000000..55af99cb2 --- /dev/null +++ b/components/form/FormItem.jsx @@ -0,0 +1,333 @@ + +import PropTypes from '../_util/vue-types' +import classNames from 'classnames' +import Row from '../grid/Row' +import Col, { ColProps } from '../grid/Col' +import warning from '../_util/warning' +import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants' +import { initDefaultProps, getComponentFromProp, filterEmpty, getSlotOptions, getSlots } from '../_util/props-util' +import getTransitionProps from '../_util/getTransitionProps' +import BaseMixin from '../_util/BaseMixin' +export const FormItemProps = { + id: PropTypes.string, + prefixCls: PropTypes.string, + label: PropTypes.any, + labelCol: PropTypes.shape(ColProps).loose, + wrapperCol: PropTypes.shape(ColProps).loose, + help: PropTypes.any, + extra: PropTypes.any, + validateStatus: PropTypes.oneOf(['', 'success', 'warning', 'error', 'validating']), + hasFeedback: PropTypes.bool, + required: PropTypes.bool, + colon: PropTypes.bool, +} + +export default { + name: 'AFormItem', + __ANT_FORM_ITEM: true, + mixins: [BaseMixin], + props: initDefaultProps(FormItemProps, { + hasFeedback: false, + prefixCls: 'ant-form', + colon: true, + }), + inject: { + FormProps: { default: {}}, + }, + data () { + return { helpShow: false } + }, + mounted () { + warning( + this.getControls(this.$slots.default, true).length <= 1, + '`Form.Item` cannot generate `validateStatus` and `help` automatically, ' + + 'while there are more than one `getFieldDecorator` in it.', + ) + }, + + // shouldComponentUpdate(...args: any[]) { + // return PureRenderMixin.shouldComponentUpdate.apply(this, args); + // } + methods: { + getHelpMsg () { + const help = getComponentFromProp(this, 'help') + const onlyControl = this.getOnlyControl() + if (help === undefined && onlyControl) { + const errors = this.getField().errors + return errors ? errors.map((e) => e.message).join(', ') : '' + } + + return help + }, + + getControls (childrenArray = [], recursively) { + let controls = [] + for (let i = 0; i < childrenArray.length; i++) { + if (!recursively && controls.length > 0) { + break + } + + const child = childrenArray[i] + if (!child.tag && child.text.trim() === '') { + continue + } + + if (getSlotOptions(child).__ANT_FORM_ITEM) { + continue + } + const attrs = child.data && child.data.attrs + if (!attrs) { + continue + } + const slots = getSlots(child) + if (FIELD_META_PROP in attrs) { // And means FIELD_DATA_PROP in chidl.props, too. + controls.push(child) + } else if (slots.default) { + controls = controls.concat(this.getControls(slots.default, recursively)) + } + } + return controls + }, + + getOnlyControl () { + const child = this.getControls(this.$slots.default, false)[0] + return child !== undefined ? child : null + }, + + getChildAttr (prop) { + const child = this.getOnlyControl() + let data = {} + if (child.data) { + data = child.data + } else if (child.$vnode && child.$vnode.data) { + data = child.$vnode.data + } + return data[prop] || data.attrs[prop] + }, + + getId () { + return this.getChildAttr('id') + }, + + getMeta () { + return this.getChildAttr(FIELD_META_PROP) + }, + + getField () { + return this.getChildAttr(FIELD_DATA_PROP) + }, + + onHelpAnimEnd (_key, helpShow) { + this.setState({ helpShow }) + }, + + renderHelp () { + const prefixCls = this.prefixCls + const help = this.getHelpMsg() + const children = help ? ( +
+ {help} +
+ ) : null + const transitionProps = getTransitionProps('show-help', { + afterLeave: this.onHelpAnimEnd, + }) + return ( + + {children} + + ) + }, + + renderExtra () { + const { prefixCls } = this + const extra = getComponentFromProp(this, 'extra') + return extra ? ( +
{extra}
+ ) : null + }, + + getValidateStatus () { + const onlyControl = this.getOnlyControl() + if (!onlyControl) { + return '' + } + const field = this.getField() + if (field.validating) { + return 'validating' + } + if (field.errors) { + return 'error' + } + const fieldValue = 'value' in field ? field.value : this.getMeta().initialValue + if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') { + return 'success' + } + return '' + }, + + renderValidateWrapper (c1, c2, c3) { + const props = this.$props + const onlyControl = this.getOnlyControl + const validateStatus = (props.validateStatus === undefined && onlyControl) + ? this.getValidateStatus() + : props.validateStatus + + let classes = `${props.prefixCls}-item-control` + if (validateStatus) { + classes = classNames(`${props.prefixCls}-item-control`, { + 'has-feedback': props.hasFeedback || validateStatus === 'validating', + 'has-success': validateStatus === 'success', + 'has-warning': validateStatus === 'warning', + 'has-error': validateStatus === 'error', + 'is-validating': validateStatus === 'validating', + }) + } + return ( +
+ {c1} + {c2}{c3} +
+ ) + }, + + renderWrapper (children) { + const { prefixCls, wrapperCol = {}} = this + const { class: cls, style, id, on, ...restProps } = wrapperCol + const className = classNames( + `${prefixCls}-item-control-wrapper`, + cls, + ) + const colProps = { + props: restProps, + class: className, + key: 'wrapper', + style, + id, + on, + } + return ( + + {children} + + ) + }, + + isRequired () { + const { required } = this + if (required !== undefined) { + return required + } + if (this.getOnlyControl()) { + const meta = this.getMeta() || {} + const validate = meta.validate || [] + + return validate.filter((item) => !!item.rules).some((item) => { + return item.rules.some((rule) => rule.required) + }) + } + return false + }, + + // Resolve duplicated ids bug between different forms + // https://github.com/ant-design/ant-design/issues/7351 + onLabelClick (e) { + const label = getComponentFromProp(this, 'label') + const id = this.id || this.getId() + if (!id) { + return + } + const controls = document.querySelectorAll(`[id="${id}"]`) + if (controls.length !== 1) { + // Only prevent in default situation + // Avoid preventing event in `label={link}`` + if (typeof label === 'string') { + e.preventDefault() + } + const control = this.$el.querySelector(`[id="${id}"]`) + if (control && control.focus) { + control.focus() + } + } + }, + + renderLabel () { + const { prefixCls, labelCol = {}, colon, id } = this + const label = getComponentFromProp(this, 'label') + const required = this.isRequired() + const { class: labelColClass, style: labelColStyle, id: labelColId, on, ...restProps } = labelCol + const labelColClassName = classNames( + `${prefixCls}-item-label`, + labelColClass, + ) + const labelClassName = classNames({ + [`${prefixCls}-item-required`]: required, + }) + + let labelChildren = label + // Keep label is original where there should have no colon + const haveColon = colon && this.FormProps.layout !== 'vertical' + // Remove duplicated user input colon + if (haveColon && typeof label === 'string' && label.trim() !== '') { + labelChildren = label.replace(/[:|:]\s*$/, '') + } + const colProps = { + props: restProps, + class: labelColClassName, + key: 'label', + style: labelColStyle, + id: labelColId, + on, + } + + return label ? ( + + + + ) : null + }, + renderChildren () { + const { $slots } = this + return [ + this.renderLabel(), + this.renderWrapper( + this.renderValidateWrapper( + filterEmpty($slots.default || []), + this.renderHelp(), + this.renderExtra(), + ), + ), + ] + }, + renderFormItem (children) { + const props = this.$props + const prefixCls = props.prefixCls + const itemClassName = { + [`${prefixCls}-item`]: true, + [`${prefixCls}-item-with-help`]: !!this.getHelpMsg() || this.helpShow, + [`${prefixCls}-item-no-colon`]: !props.colon, + } + + return ( + + {children} + + ) + }, + }, + + render () { + const children = this.renderChildren() + return this.renderFormItem(children) + }, +} diff --git a/components/form/constants.jsx b/components/form/constants.jsx new file mode 100644 index 000000000..4c84e31ce --- /dev/null +++ b/components/form/constants.jsx @@ -0,0 +1,2 @@ +export const FIELD_META_PROP = 'data-__meta' +export const FIELD_DATA_PROP = 'data-__field' diff --git a/components/form/demo/test.vue b/components/form/demo/test.vue new file mode 100644 index 000000000..11d110f9e --- /dev/null +++ b/components/form/demo/test.vue @@ -0,0 +1,65 @@ + + \ No newline at end of file diff --git a/components/form/index.en-US.md b/components/form/index.en-US.md new file mode 100644 index 000000000..41504e7fb --- /dev/null +++ b/components/form/index.en-US.md @@ -0,0 +1,181 @@ +--- +category: Components +type: Data Entry +cols: 1 +title: Form +--- + +Form is used to collect, validate, and submit the user input, usually contains various form items including checkbox, radio, input, select, and etc. + +## Form + +You can align the controls of a `form` using the `layout` prop: + +- `horizontal`:to horizontally align the `label`s and controls of the fields. (Default) +- `vertical`:to vertically align the `label`s and controls of the fields. +- `inline`:to render form fields in one line. + +## Form fields + +A form consists of one or more form fields whose type includes input, textarea, checkbox, radio, select, tag, and more. +A form field is defined using ``. + +```jsx + + {children} + +``` + +## API + +### Form + +**more example [rc-form](http://react-component.github.io/form/)**。 + +| Property | Description | Type | Default Value | +| -------- | ----------- | ---- | ------------- | +| form | Decorated by `Form.create()` will be automatically set `this.props.form` property, so just pass to form, you don't need to set it by yourself after 1.7.0. | object | n/a | +| hideRequiredMark | Hide required mark of all form items | Boolean | false | +| layout | Define form layout(Support after 2.8) | 'horizontal'\|'vertical'\|'inline' | 'horizontal' | +| onSubmit | Defines a function will be called if form data validation is successful. | Function(e:Event) | | + +### Form.create(options) + +How to use: + +```jsx +class CustomizedForm extends React.Component {} + +CustomizedForm = Form.create({})(CustomizedForm); +``` + +The following `options` are available: + +| Property | Description | Type | +| -------- | ----------- | ---- | +| mapPropsToFields | Convert props to field value(e.g. reading the values from Redux store). And you must mark returned fields with [`Form.createFormField`](#Form.createFormField) | (props) => Object{ fieldName: FormField { value } } | +| validateMessages | Default validate message. And its format is similar with [newMessages](https://github.com/yiminghe/async-validator/blob/master/src/messages.js)'s returned value | Object { [nested.path]: String } | +| onFieldsChange | Specify a function that will be called when the value a `Form.Item` gets changed. Usage example: saving the field's value to Redux store. | Function(props, fields) | +| onValuesChange | A handler while value of any field is changed | (props, values) => void | + +If the form has been decorated by `Form.create` then it has `this.props.form` property. `this.props.form` provides some APIs as follows: + +> Note: Before using `getFieldsValue` `getFieldValue` `setFieldsValue` and so on, please make sure that corresponding field had been registered with `getFieldDecorator`. + +| Method | Description | Type | +| ------ | ----------- | ---- | +| getFieldDecorator | Two-way binding for form, please read below for details. | | +| getFieldError | Get the error of a field. | Function(name) | +| getFieldsError | Get the specified fields' error. If you don't specify a parameter, you will get all fields' error. | Function(\[names: string\[]]) | +| getFieldsValue | Get the specified fields' values. If you don't specify a parameter, you will get all fields' values. | Function(\[fieldNames: string\[]]) | +| getFieldValue | Get the value of a field. | Function(fieldName: string) | +| isFieldsTouched | Check whether any of fields is touched by `getFieldDecorator`'s `options.trigger` event | (names?: string\[]) => boolean | +| isFieldTouched | Check whether a field is touched by `getFieldDecorator`'s `options.trigger` event | (name: string) => boolean | +| isFieldValidating | Check if the specified field is being validated. | Function(name) | +| resetFields | Reset the specified fields' value(to `initialValue`) and status. If you don't specify a parameter, all the fields will be reset. | Function(\[names: string\[]]) | +| setFields | Set the value and error of a field. [Code Sample](https://github.com/react-component/form/blob/3b9959b57ab30b41d8890ff30c79a7e7c383cad3/examples/server-validate.js#L74-L79) | Function({ [fieldName]: { value: any, errors: [Error] } }) | +| setFields | | Function(obj: object) | +| setFieldsValue | Set the value of a field.(Note: please don't use it in `componentWillReceiveProps`, otherwise, it will cause an endless loop, [more](https://github.com/ant-design/ant-design/issues/2985)) | Function({ [fieldName]: value } | +| validateFields | Validate the specified fields and get theirs values and errors. If you don't specify the parameter of fieldNames, you will vaildate all fields. | Function(\[fieldNames: string\[]], [options: object], callback: Function(errors, values)) | +| validateFieldsAndScroll | This function is similar to `validateFields`, but after validation, if the target field is not in visible area of form, form will be automatically scrolled to the target field area. | same as `validateFields` | + +### this.props.form.validateFields/validateFieldsAndScroll(\[fieldNames: string\[]], [options: object], callback: Function(errors, values)) + +| Method | Description | Type | Default | +| ------ | ----------- | ---- | ------- | +| options.first | If `true`, every field will stop validation at first failed rule | boolean | false | +| options.firstFields | Those fields will stop validation at first failed rule | String\[] | \[] | +| options.force | Should validate validated field again when `validateTrigger` is been triggered again | boolean | false | +| options.scroll | Config scroll behavior of `validateFieldsAndScroll`, more: [dom-scroll-into-view's config](https://github.com/yiminghe/dom-scroll-into-view#function-parameter) | Object | {} | + +### Form.createFormField + +To mark the returned fields data in `mapPropsToFields`, [demo](#components-form-demo-global-state). + +### this.props.form.getFieldDecorator(id, options) + +After wrapped by `getFieldDecorator`, `value`(or other property defined by `valuePropName`) `onChange`(or other property defined by `trigger`) props will be added to form controls,the flow of form data will be handled by Form which will cause: + +1. You shouldn't use `onChange` to collect data, but you still can listen to `onChange`(and so on) events. +2. You can not set value of form control via `value` `defaultValue` prop, and you should set default value with `initialValue` in `getFieldDecorator` instead. +3. You shouldn't call `setState` manually, please use `this.props.form.setFieldsValue` to change value programmatically. + +#### Special attention + +1. `getFieldDecorator` can not be used to decorate stateless component. +2. If you use `react@<15.3.0`, then, you can't use `getFieldDecorator` in stateless component: + +#### getFieldDecorator(id, options) parameters + +| Property | Description | Type | Default Value | +| -------- | ----------- | ---- | ------------- | +| id | The unique identifier is required. support [nested fields format](https://github.com/react-component/form/pull/48). | string | | +| options.getValueFromEvent | Specify how to get value from event or other onChange arguments | function(..args) | [reference](https://github.com/react-component/form#option-object) | +| options.initialValue | You can specify initial value, type, optional value of children node. (Note: Because `Form` will test equality with `===` internaly, we recommend to use vairable as `initialValue`, instead of literal) | | n/a | +| options.normalize | Normalize value to form component, [a select-all example](https://codepen.io/afc163/pen/JJVXzG?editors=001) | function(value, prevValue, allValues): any | - | +| options.rules | Includes validation rules. Please refer to "Validation Rules" part for details. | object\[] | n/a | +| options.trigger | When to collect the value of children node | string | 'onChange' | +| options.validateFirst | Whether stop validate on first rule of error for this field. | boolean | false | +| options.validateTrigger | When to validate the value of children node. | string\|string\[] | 'onChange' | +| options.valuePropName | Props of children node, for example, the prop of Switch is 'checked'. | string | 'value' | + +More option at [rc-form option](https://github.com/react-component/form#option-object)。 + +### Form.Item + +Note: + +- If Form.Item has multiple children that had been decorated by `getFieldDecorator`, `help` and `required` and `validateStatus` can't be generated automatically. +- Before `2.2.0`, form controls must be child of Form.Item, otherwise, you need to set `help`, `required` and `validateStatus` by yourself. + +| Property | Description | Type | Default Value | +| -------- | ----------- | ---- | ------------- | +| colon | Used with `label`, whether to display `:` after label text. | boolean | true | +| extra | The extra prompt message. It is similar to help. Usage example: to display error message and prompt message at the same time. | string\|ReactNode | | +| hasFeedback | Used with `validateStatus`, this option specifies the validation status icon. Recommended to be used only with `Input`. | boolean | false | +| help | The prompt message. If not provided, the prompt message will be generated by the validation rule. | string\|ReactNode | | +| label | Label text | string\|ReactNode | | +| labelCol | The layout of label. You can set `span` `offset` to something like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` same as with `` | [object](https://ant.design/components/grid/#Col) | | +| required | Whether provided or not, it will be generated by the validation rule. | boolean | false | +| validateStatus | The validation status. If not provided, it will be generated by validation rule. options: 'success' 'warning' 'error' 'validating' | string | | +| wrapperCol | The layout for input controls, same as `labelCol` | [object](https://ant.design/components/grid/#Col) | | + +### Validation Rules + +| Property | Description | Type | Default Value | +| -------- | ----------- | ---- | ------------- | +| enum | validate a value from a list of possible values | string | - | +| len | validate an exact length of a field | number | - | +| max | validate a max length of a field | number | - | +| message | validation error message | string | - | +| min | validate a min length of a field | number | - | +| pattern | validate from a regular expression | RegExp | - | +| required | indicates whether field is required | boolean | `false` | +| transform | transform a value before validation | function(value) => transformedValue:any | - | +| type | built-in validation type, [available options](https://github.com/yiminghe/async-validator#type) | string | 'string' | +| validator | custom validate function (Note: [callback must be called](https://github.com/ant-design/ant-design/issues/5155)) | function(rule, value, callback) | - | +| whitespace | treat required fields that only contain whitespace as errors | boolean | `false` | + +See more advanced usage at [async-validator](https://github.com/yiminghe/async-validator). + + + +## Using in TypeScript + +```jsx +import { Form } from 'antd'; +import { FormComponentProps } from 'antd/lib/form'; + +interface UserFormProps extends FormComponentProps { + age: number; + name: string; +} + +class UserForm extends React.Component { + +} +``` diff --git a/components/form/index.jsx b/components/form/index.jsx new file mode 100644 index 000000000..07ea84244 --- /dev/null +++ b/components/form/index.jsx @@ -0,0 +1,6 @@ +import Form from './Form' + +export { FormProps, FormComponentProps, FormCreateOption, ValidateCallback, ValidationRule } from './Form' +export { FormItemProps } from './FormItem' + +export default Form diff --git a/components/form/index.zh-CN.md b/components/form/index.zh-CN.md new file mode 100644 index 000000000..b0629f433 --- /dev/null +++ b/components/form/index.zh-CN.md @@ -0,0 +1,182 @@ +--- +category: Components +subtitle: 表单 +type: Data Entry +cols: 1 +title: Form +--- + +具有数据收集、校验和提交功能的表单,包含复选框、单选框、输入框、下拉选择框等元素。 + +## 表单 + +我们为 `form` 提供了以下三种排列方式: + +- 水平排列:标签和表单控件水平排列;(默认) +- 垂直排列:标签和表单控件上下垂直排列; +- 行内排列:表单项水平行内排列。 + +## 表单域 + +表单一定会包含表单域,表单域可以是输入控件,标准表单域,标签,下拉菜单,文本域等。 + +这里我们封装了表单域 `` 。 + +```jsx + + {children} + +``` + +## API + +### Form + +**更多示例参考 [rc-form](http://react-component.github.io/form/)**。 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| form | 经 `Form.create()` 包装过的组件会自带 `this.props.form` 属性,直接传给 Form 即可。1.7.0 之后无需设置 | object | 无 | +| hideRequiredMark | 隐藏所有表单项的必选标记 | Boolean | false | +| layout | 表单布局(2.8 之后支持) | 'horizontal'\|'vertical'\|'inline' | 'horizontal' | +| onSubmit | 数据验证成功后回调事件 | Function(e:Event) | | + +### Form.create(options) + +使用方式如下: + +```jsx +class CustomizedForm extends React.Component {} + +CustomizedForm = Form.create({})(CustomizedForm); +``` + +`options` 的配置项如下。 + +| 参数 | 说明 | 类型 | +| --- | --- | --- | +| mapPropsToFields | 把父组件的属性映射到表单项上(如:把 Redux store 中的值读出),需要对返回值中的表单域数据用 [`Form.createFormField`](#Form.createFormField) 标记 | (props) => Object{ fieldName: FormField { value } } | +| validateMessages | 默认校验信息,可用于把默认错误信息改为中文等,格式与 [newMessages](https://github.com/yiminghe/async-validator/blob/master/src/messages.js) 返回值一致 | Object { [nested.path]: String } | +| onFieldsChange | 当 `Form.Item` 子节点的值发生改变时触发,可以把对应的值转存到 Redux store | Function(props, fields) | +| onValuesChange | 任一表单域的值发生改变时的回调 | (props, values) => void | + +经过 `Form.create` 包装的组件将会自带 `this.props.form` 属性,`this.props.form` 提供的 API 如下: + +> 注意:使用 `getFieldsValue` `getFieldValue` `setFieldsValue` 等时,应确保对应的 field 已经用 `getFieldDecorator` 注册过了。 + +| 方法      | 说明                                     | 类型       | +| ------- | -------------------------------------- | -------- | +| getFieldDecorator | 用于和表单进行双向绑定,详见下方描述 | | +| getFieldError | 获取某个输入控件的 Error | Function(name) | +| getFieldsError | 获取一组输入控件的 Error ,如不传入参数,则获取全部组件的 Error | Function(\[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。 [代码](https://github.com/react-component/form/blob/3b9959b57ab30b41d8890ff30c79a7e7c383cad3/examples/server-validate.js#L74-L79) | Function({ [fieldName]: { value: any, errors: [Error] } }) | +| setFieldsValue | 设置一组输入控件的值(注意:不要在 `componentWillReceiveProps` 内使用,否则会导致死循环,[更多](https://github.com/ant-design/ant-design/issues/2985)) | Function({ [fieldName]: value } | +| validateFields | 校验并获取一组输入域的值与 Error,若 fieldNames 参数为空,则校验全部组件 | Function(\[fieldNames: string\[]], [options: object], callback: Function(errors, values)) | +| validateFieldsAndScroll | 与 `validateFields` 相似,但校验完后,如果校验不通过的菜单域不在可见范围内,则自动滚动进可见范围 | 参考 `validateFields` | + +### this.props.form.validateFields/validateFieldsAndScroll(\[fieldNames: string\[]], [options: object], callback: Function(errors, values)) + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| options.first | 若为 true,则每一表单域的都会在碰到第一个失败了的校验规则后停止校验 | boolean | false | +| options.firstFields | 指定表单域会在碰到第一个失败了的校验规则后停止校验 | String\[] | \[] | +| options.force | 对已经校验过的表单域,在 validateTrigger 再次被触发时是否再次校验 | boolean | false | +| options.scroll | 定义 validateFieldsAndScroll 的滚动行为,详细配置见 [dom-scroll-into-view config](https://github.com/yiminghe/dom-scroll-into-view#function-parameter) | Object | {} | + +### Form.createFormField + +用于标记 `mapPropsToFields` 返回的表单域数据,[例子](#components-form-demo-global-state)。 + +### 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` 调用不能位于纯函数组件中: + +#### getFieldDecorator(id, options) 参数 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| id | 必填输入控件唯一标志。支持嵌套式的[写法](https://github.com/react-component/form/pull/48)。 | string | | +| options.getValueFromEvent | 可以把 onChange 的参数(如 event)转化为控件的值 | function(..args) | [reference](https://github.com/react-component/form#option-object) | +| options.initialValue | 子节点的初始值,类型、可选值均由子节点决定(注意:由于内部校验时使用 `===` 判断是否变化,建议使用变量缓存所需设置的值而非直接使用字面量)) | | | +| options.normalize | 转换默认的 value 给控件,[一个选择全部的例子](https://codepen.io/afc163/pen/JJVXzG?editors=001) | function(value, prevValue, allValues): any | - | +| options.rules | 校验规则,参考下方文档 | object\[] | | +| options.trigger | 收集子节点的值的时机 | string | 'onChange' | +| options.validateFirst | 当某一规则校验不通过时,是否停止剩下的规则的校验 | boolean | false | +| options.validateTrigger | 校验子节点值的时机 | string\|string\[] | 'onChange' | +| options.valuePropName | 子节点的值的属性,如 Switch 的是 'checked' | string | 'value' | + +更多参数请查看 [rc-form option](https://github.com/react-component/form#option-object)。 + +### Form.Item + +注意: + +- 一个 Form.Item 建议只放一个被 getFieldDecorator 装饰过的 child,当有多个被装饰过的 child 时,`help` `required` `validateStatus` 无法自动生成。 +- `2.2.0` 之前,只有当表单域为 Form.Item 的子元素时,才会自动生成 `help` `required` `validateStatus`,嵌套情况需要自行设置。 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| colon | 配合 label 属性使用,表示是否显示 label 后面的冒号 | boolean | true | +| extra | 额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 | string\|ReactNode | | +| hasFeedback | 配合 validateStatus 属性使用,展示校验状态图标,建议只配合 Input 组件使用 | boolean | false | +| help | 提示信息,如不设置,则会根据校验规则自动生成 | string\|ReactNode | | +| label | label 标签的文本 | string\|ReactNode | | +| labelCol | label 标签布局,同 `` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}` 或 `sm: {span: 3, offset: 12}` | [object](https://ant.design/components/grid/#Col) | | +| required | 是否必填,如不设置,则会根据校验规则自动生成 | boolean | false | +| validateStatus | 校验状态,如不设置,则会根据校验规则自动生成,可选:'success' 'warning' 'error' 'validating' | string | | +| wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol | [object](https://ant.design/components/grid/#Col) | | + +### 校验规则 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| enum | 枚举类型 | string | - | +| len | 字段长度 | number | - | +| max | 最大长度 | number | - | +| message | 校验文案 | string | - | +| min | 最小长度 | number | - | +| pattern | 正则表达式校验 | RegExp | - | +| required | 是否必选 | boolean | `false` | +| transform | 校验前转换字段值 | function(value) => transformedValue:any | - | +| type | 内建校验类型,[可选项](https://github.com/yiminghe/async-validator#type) | string | 'string' | +| validator | 自定义校验(注意,[callback 必须被调用](https://github.com/ant-design/ant-design/issues/5155)) | function(rule, value, callback) | - | +| whitespace | 必选时,空格是否会被视为错误 | boolean | `false` | + +更多高级用法可研究 [async-validator](https://github.com/yiminghe/async-validator)。 + + + +## 在 TypeScript 中使用 + +```jsx +import { Form } from 'antd'; +import { FormComponentProps } from 'antd/lib/form'; + +interface UserFormProps extends FormComponentProps { + age: number; + name: string; +} + +class UserForm extends React.Component { + +} +``` diff --git a/components/form/style/index.js b/components/form/style/index.js new file mode 100644 index 000000000..753451010 --- /dev/null +++ b/components/form/style/index.js @@ -0,0 +1,5 @@ +import '../../style/index.less' +import './index.less' + +// style dependencies +import '../../grid/style' diff --git a/components/form/style/index.less b/components/form/style/index.less new file mode 100644 index 000000000..a26e07c2c --- /dev/null +++ b/components/form/style/index.less @@ -0,0 +1,624 @@ +@import "../../style/themes/default"; +@import "../../style/mixins/index"; +@import "../../input/style/mixin"; +@import "../../button/style/mixin"; +@import "../../grid/style/mixin"; +@import "./mixin"; + +@form-prefix-cls: ~"@{ant-prefix}-form"; +@form-component-height: @input-height-base; +@form-component-max-height: @input-height-lg; +@form-feedback-icon-size: 14px; +@form-help-margin-top: (@form-component-height - @form-component-max-height) / 2 + 2px; + +.@{form-prefix-cls} { + .reset-component; + .reset-form; +} + +.@{form-prefix-cls}-item-required:before { + display: inline-block; + margin-right: 4px; + content: "*"; + font-family: SimSun; + line-height: 1; + font-size: @font-size-base; + color: @label-required-color; + .@{form-prefix-cls}-hide-required-mark & { + display: none; + } +} + +// Radio && Checkbox +input[type="radio"], +input[type="checkbox"] { + &[disabled], + &.disabled { + cursor: not-allowed; + } +} + +// These classes are used directly on