Browse Source
* init form * first demo * add normal login * add style * webit * support nest errors * beauti form errors * use onReset * modal demo * add list demo * match key of errors logic * date demo * customize component * moving style * add status style * without form create * add demos * add inline style * clean up legacy * fix drawer demo * mention * fix edit-row * editable table cell * update mentions demo * fix some test case * fix upload test * fix lint * part of doc * fix ts * doc update * rm react 15 * rm config * enhance test coverage * clean up * fix FormItem context pass logic * add more demo * en to build * update demo * update demo & snapshot * more doc * update list doc * update doc * update demo to display condition render * update snapshot * add provider doc * support configProvider * more doc about validateMessages * more description * more and more doc * fix typo * en doc * Form.List doc * m v3 -> v4 * add skippull/17536/head
zombieJ
5 years ago
committed by
GitHub
113 changed files with 10307 additions and 11841 deletions
@ -1,280 +0,0 @@ |
|||
import * as React from 'react'; |
|||
import * as PropTypes from 'prop-types'; |
|||
import classNames from 'classnames'; |
|||
import createDOMForm from 'rc-form/lib/createDOMForm'; |
|||
import createFormField from 'rc-form/lib/createFormField'; |
|||
import omit from 'omit.js'; |
|||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; |
|||
import { ColProps } from '../grid/col'; |
|||
import { tuple } from '../_util/type'; |
|||
import warning from '../_util/warning'; |
|||
import FormItem, { FormLabelAlign } from './FormItem'; |
|||
import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants'; |
|||
import { FormContext } from './context'; |
|||
import { FormWrappedProps } from './interface'; |
|||
|
|||
type FormCreateOptionMessagesCallback = (...args: any[]) => string; |
|||
|
|||
interface FormCreateOptionMessages { |
|||
[messageId: string]: string | FormCreateOptionMessagesCallback | FormCreateOptionMessages; |
|||
} |
|||
|
|||
export interface FormCreateOption<T> { |
|||
onFieldsChange?: (props: T, fields: any, allFields: any) => void; |
|||
onValuesChange?: (props: T, changedValues: any, allValues: any) => void; |
|||
mapPropsToFields?: (props: T) => void; |
|||
validateMessages?: FormCreateOptionMessages; |
|||
withRef?: boolean; |
|||
name?: string; |
|||
} |
|||
|
|||
const FormLayouts = tuple('horizontal', 'inline', 'vertical'); |
|||
export type FormLayout = (typeof FormLayouts)[number]; |
|||
|
|||
export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> { |
|||
layout?: FormLayout; |
|||
form?: WrappedFormUtils; |
|||
onSubmit?: React.FormEventHandler<any>; |
|||
style?: React.CSSProperties; |
|||
className?: string; |
|||
prefixCls?: string; |
|||
hideRequiredMark?: boolean; |
|||
/** |
|||
* @since 3.14.0 |
|||
*/ |
|||
wrapperCol?: ColProps; |
|||
labelCol?: ColProps; |
|||
/** |
|||
* @since 3.15.0 |
|||
*/ |
|||
colon?: boolean; |
|||
labelAlign?: FormLabelAlign; |
|||
} |
|||
|
|||
export type ValidationRule = { |
|||
/** validation error message */ |
|||
message?: React.ReactNode; |
|||
/** built-in validation type, available options: https://github.com/yiminghe/async-validator#type */ |
|||
type?: string; |
|||
/** indicates whether field is required */ |
|||
required?: boolean; |
|||
/** treat required fields that only contain whitespace as errors */ |
|||
whitespace?: boolean; |
|||
/** validate the exact length of a field */ |
|||
len?: number; |
|||
/** validate the min length of a field */ |
|||
min?: number; |
|||
/** validate the max length of a field */ |
|||
max?: number; |
|||
/** validate the value from a list of possible values */ |
|||
enum?: string | string[]; |
|||
/** validate from a regular expression */ |
|||
pattern?: RegExp; |
|||
/** transform a value before validation */ |
|||
transform?: (value: any) => any; |
|||
/** custom validate function (Note: callback must be called) */ |
|||
validator?: (rule: any, value: any, callback: any, source?: any, options?: any) => any; |
|||
}; |
|||
|
|||
export type ValidateCallback<V> = (errors: any, values: V) => void; |
|||
|
|||
export type GetFieldDecoratorOptions = { |
|||
/** 子节点的值的属性,如 Checkbox 的是 'checked' */ |
|||
valuePropName?: string; |
|||
/** 子节点的初始值,类型、可选值均由子节点决定 */ |
|||
initialValue?: any; |
|||
/** 收集子节点的值的时机 */ |
|||
trigger?: string; |
|||
/** 可以把 onChange 的参数转化为控件的值,例如 DatePicker 可设为:(date, dateString) => dateString */ |
|||
getValueFromEvent?: (...args: any[]) => any; |
|||
/** Get the component props according to field value. */ |
|||
getValueProps?: (value: 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; |
|||
/** 是否一直保留子节点的信息 */ |
|||
preserve?: boolean; |
|||
}; |
|||
|
|||
/** dom-scroll-into-view 组件配置参数 */ |
|||
export type DomScrollIntoViewConfig = { |
|||
/** 是否和左边界对齐 */ |
|||
alignWithLeft?: boolean; |
|||
/** 是否和上边界对齐 */ |
|||
alignWithTop?: boolean; |
|||
/** 顶部偏移量 */ |
|||
offsetTop?: number; |
|||
/** 左侧偏移量 */ |
|||
offsetLeft?: number; |
|||
/** 底部偏移量 */ |
|||
offsetBottom?: number; |
|||
/** 右侧偏移量 */ |
|||
offsetRight?: number; |
|||
/** 是否允许容器水平滚动 */ |
|||
allowHorizontalScroll?: boolean; |
|||
/** 当内容可见时是否允许滚动容器 */ |
|||
onlyScrollIfNeeded?: boolean; |
|||
}; |
|||
|
|||
export type ValidateFieldsOptions = { |
|||
/** 所有表单域是否在第一个校验规则失败后停止继续校验 */ |
|||
first?: boolean; |
|||
/** 指定哪些表单域在第一个校验规则失败后停止继续校验 */ |
|||
firstFields?: string[]; |
|||
/** 已经校验过的表单域,在 validateTrigger 再次被触发时是否再次校验 */ |
|||
force?: boolean; |
|||
/** 定义 validateFieldsAndScroll 的滚动行为 */ |
|||
scroll?: DomScrollIntoViewConfig; |
|||
}; |
|||
|
|||
// function create
|
|||
export type WrappedFormUtils<V = any> = { |
|||
/** 获取一组输入控件的值,如不传入参数,则获取全部组件的值 */ |
|||
getFieldsValue(fieldNames?: Array<string>): { [field: string]: any }; |
|||
/** 获取一个输入控件的值 */ |
|||
getFieldValue(fieldName: string): any; |
|||
/** 设置一组输入控件的值 */ |
|||
setFieldsValue(obj: Object): void; |
|||
/** 设置一组输入控件的值 */ |
|||
setFields(obj: Object): void; |
|||
/** 校验并获取一组输入域的值与 Error */ |
|||
validateFields( |
|||
fieldNames: Array<string>, |
|||
options: ValidateFieldsOptions, |
|||
callback: ValidateCallback<V>, |
|||
): void; |
|||
validateFields(options: ValidateFieldsOptions, callback: ValidateCallback<V>): void; |
|||
validateFields(fieldNames: Array<string>, callback: ValidateCallback<V>): void; |
|||
validateFields(fieldNames: Array<string>, options: ValidateFieldsOptions): void; |
|||
validateFields(fieldNames: Array<string>): void; |
|||
validateFields(callback: ValidateCallback<V>): void; |
|||
validateFields(options: ValidateFieldsOptions): void; |
|||
validateFields(): void; |
|||
/** 与 `validateFields` 相似,但校验完后,如果校验不通过的菜单域不在可见范围内,则自动滚动进可见范围 */ |
|||
validateFieldsAndScroll( |
|||
fieldNames: Array<string>, |
|||
options: ValidateFieldsOptions, |
|||
callback: ValidateCallback<V>, |
|||
): void; |
|||
validateFieldsAndScroll(options: ValidateFieldsOptions, callback: ValidateCallback<V>): void; |
|||
validateFieldsAndScroll(fieldNames: Array<string>, callback: ValidateCallback<V>): void; |
|||
validateFieldsAndScroll(fieldNames: Array<string>, options: ValidateFieldsOptions): void; |
|||
validateFieldsAndScroll(fieldNames: Array<string>): void; |
|||
validateFieldsAndScroll(callback: ValidateCallback<V>): void; |
|||
validateFieldsAndScroll(options: ValidateFieldsOptions): void; |
|||
validateFieldsAndScroll(): void; |
|||
/** 获取某个输入控件的 Error */ |
|||
getFieldError(name: string): string[] | undefined; |
|||
getFieldsError(names?: Array<string>): Record<string, string[] | undefined>; |
|||
/** 判断一个输入控件是否在校验状态 */ |
|||
isFieldValidating(name: string): boolean; |
|||
isFieldTouched(name: string): boolean; |
|||
isFieldsTouched(names?: Array<string>): boolean; |
|||
/** 重置一组输入控件的值与状态,如不传入参数,则重置所有组件 */ |
|||
resetFields(names?: Array<string>): void; |
|||
// tslint:disable-next-line:max-line-length
|
|||
getFieldDecorator<T extends Object = {}>( |
|||
id: keyof T, |
|||
options?: GetFieldDecoratorOptions, |
|||
): (node: React.ReactNode) => React.ReactNode; |
|||
}; |
|||
|
|||
export interface WrappedFormInternalProps<V = any> { |
|||
form: WrappedFormUtils<V>; |
|||
} |
|||
|
|||
export interface RcBaseFormProps { |
|||
wrappedComponentRef?: any; |
|||
} |
|||
|
|||
export interface FormComponentProps<V = any> extends WrappedFormInternalProps<V>, RcBaseFormProps { |
|||
form: WrappedFormUtils<V>; |
|||
} |
|||
|
|||
export default class Form extends React.Component<FormProps, any> { |
|||
static defaultProps = { |
|||
colon: true, |
|||
layout: 'horizontal' as FormLayout, |
|||
hideRequiredMark: false, |
|||
onSubmit(e: React.FormEvent<HTMLFormElement>) { |
|||
e.preventDefault(); |
|||
}, |
|||
}; |
|||
|
|||
static propTypes = { |
|||
prefixCls: PropTypes.string, |
|||
layout: PropTypes.oneOf(FormLayouts), |
|||
children: PropTypes.any, |
|||
onSubmit: PropTypes.func, |
|||
hideRequiredMark: PropTypes.bool, |
|||
colon: PropTypes.bool, |
|||
}; |
|||
|
|||
static Item = FormItem; |
|||
|
|||
static createFormField = createFormField; |
|||
|
|||
static create = function<TOwnProps extends FormComponentProps>( |
|||
options: FormCreateOption<TOwnProps> = {}, |
|||
): FormWrappedProps<TOwnProps> { |
|||
return createDOMForm({ |
|||
fieldNameProp: 'id', |
|||
...options, |
|||
fieldMetaProp: FIELD_META_PROP, |
|||
fieldDataProp: FIELD_DATA_PROP, |
|||
}); |
|||
}; |
|||
|
|||
constructor(props: FormProps) { |
|||
super(props); |
|||
|
|||
warning(!props.form, 'Form', 'It is unnecessary to pass `form` to `Form` after antd@1.7.0.'); |
|||
} |
|||
|
|||
renderForm = ({ getPrefixCls }: ConfigConsumerProps) => { |
|||
const { prefixCls: customizePrefixCls, hideRequiredMark, className = '', layout } = this.props; |
|||
const prefixCls = getPrefixCls('form', customizePrefixCls); |
|||
const formClassName = classNames( |
|||
prefixCls, |
|||
{ |
|||
[`${prefixCls}-horizontal`]: layout === 'horizontal', |
|||
[`${prefixCls}-vertical`]: layout === 'vertical', |
|||
[`${prefixCls}-inline`]: layout === 'inline', |
|||
[`${prefixCls}-hide-required-mark`]: hideRequiredMark, |
|||
}, |
|||
className, |
|||
); |
|||
|
|||
const formProps = omit(this.props, [ |
|||
'prefixCls', |
|||
'className', |
|||
'layout', |
|||
'form', |
|||
'hideRequiredMark', |
|||
'wrapperCol', |
|||
'labelAlign', |
|||
'labelCol', |
|||
'colon', |
|||
]); |
|||
|
|||
return <form {...formProps} className={formClassName} />; |
|||
}; |
|||
|
|||
render() { |
|||
const { wrapperCol, labelAlign, labelCol, layout, colon } = this.props; |
|||
return ( |
|||
<FormContext.Provider |
|||
value={{ wrapperCol, labelAlign, labelCol, vertical: layout === 'vertical', colon }} |
|||
> |
|||
<ConfigConsumer>{this.renderForm}</ConfigConsumer> |
|||
</FormContext.Provider> |
|||
); |
|||
} |
|||
} |
@ -1,421 +0,0 @@ |
|||
import * as React from 'react'; |
|||
import * as ReactDOM from 'react-dom'; |
|||
import * as PropTypes from 'prop-types'; |
|||
import classNames from 'classnames'; |
|||
import Animate from 'rc-animate'; |
|||
import Row from '../grid/row'; |
|||
import Col, { ColProps } from '../grid/col'; |
|||
import Icon from '../icon'; |
|||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; |
|||
import warning from '../_util/warning'; |
|||
import { tuple } from '../_util/type'; |
|||
import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants'; |
|||
import { FormContext, FormContextProps } from './context'; |
|||
|
|||
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', ''); |
|||
|
|||
export type FormLabelAlign = 'left' | 'right'; |
|||
|
|||
export interface FormItemProps { |
|||
prefixCls?: string; |
|||
className?: string; |
|||
id?: string; |
|||
htmlFor?: string; |
|||
label?: React.ReactNode; |
|||
labelAlign?: FormLabelAlign; |
|||
labelCol?: ColProps; |
|||
wrapperCol?: ColProps; |
|||
help?: React.ReactNode; |
|||
extra?: React.ReactNode; |
|||
validateStatus?: (typeof ValidateStatuses)[number]; |
|||
hasFeedback?: boolean; |
|||
required?: boolean; |
|||
style?: React.CSSProperties; |
|||
colon?: boolean; |
|||
} |
|||
|
|||
function intersperseSpace<T>(list: Array<T>): Array<T | string> { |
|||
return list.reduce((current, item) => [...current, ' ', item], []).slice(1); |
|||
} |
|||
|
|||
export default class FormItem extends React.Component<FormItemProps, any> { |
|||
static defaultProps = { |
|||
hasFeedback: false, |
|||
}; |
|||
|
|||
static propTypes = { |
|||
prefixCls: PropTypes.string, |
|||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), |
|||
labelAlign: PropTypes.string, |
|||
labelCol: PropTypes.object, |
|||
help: PropTypes.oneOfType([PropTypes.node, PropTypes.bool]), |
|||
validateStatus: PropTypes.oneOf(ValidateStatuses), |
|||
hasFeedback: PropTypes.bool, |
|||
wrapperCol: PropTypes.object, |
|||
className: PropTypes.string, |
|||
id: PropTypes.string, |
|||
children: PropTypes.node, |
|||
colon: PropTypes.bool, |
|||
}; |
|||
|
|||
helpShow = false; |
|||
|
|||
componentDidMount() { |
|||
const { children, help, validateStatus, id } = this.props; |
|||
warning( |
|||
this.getControls(children, true).length <= 1 || |
|||
(help !== undefined || validateStatus !== undefined), |
|||
'Form.Item', |
|||
'Cannot generate `validateStatus` and `help` automatically, ' + |
|||
'while there are more than one `getFieldDecorator` in it.', |
|||
); |
|||
|
|||
warning( |
|||
!id, |
|||
'Form.Item', |
|||
'`id` is deprecated for its label `htmlFor`. Please use `htmlFor` directly.', |
|||
); |
|||
} |
|||
|
|||
getHelpMessage() { |
|||
const { help } = this.props; |
|||
if (help === undefined && this.getOnlyControl()) { |
|||
const { errors } = this.getField(); |
|||
if (errors) { |
|||
return intersperseSpace( |
|||
errors.map((e: any, index: number) => { |
|||
let node: React.ReactElement<any> | null = null; |
|||
|
|||
if (React.isValidElement(e)) { |
|||
node = e; |
|||
} else if (React.isValidElement(e.message)) { |
|||
node = e.message; |
|||
} |
|||
|
|||
return node ? React.cloneElement(node, { key: index }) : e.message; |
|||
}), |
|||
); |
|||
} |
|||
return ''; |
|||
} |
|||
return help; |
|||
} |
|||
|
|||
getControls(children: React.ReactNode, recursively: boolean) { |
|||
let controls: React.ReactElement<any>[] = []; |
|||
const childrenArray = React.Children.toArray(children); |
|||
for (let i = 0; i < childrenArray.length; i++) { |
|||
if (!recursively && controls.length > 0) { |
|||
break; |
|||
} |
|||
|
|||
const child = childrenArray[i] as React.ReactElement<any>; |
|||
if ( |
|||
child.type && |
|||
((child.type as any) === FormItem || (child.type as any).displayName === 'FormItem') |
|||
) { |
|||
continue; |
|||
} |
|||
if (!child.props) { |
|||
continue; |
|||
} |
|||
if (FIELD_META_PROP in child.props) { |
|||
// And means FIELD_DATA_PROP in child.props, too.
|
|||
controls.push(child); |
|||
} else if (child.props.children) { |
|||
controls = controls.concat(this.getControls(child.props.children, recursively)); |
|||
} |
|||
} |
|||
return controls; |
|||
} |
|||
|
|||
getOnlyControl() { |
|||
const child = this.getControls(this.props.children, false)[0]; |
|||
return child !== undefined ? child : null; |
|||
} |
|||
|
|||
getChildProp(prop: string) { |
|||
const child = this.getOnlyControl() as React.ReactElement<any>; |
|||
return child && child.props && child.props[prop]; |
|||
} |
|||
|
|||
getId() { |
|||
return this.getChildProp('id'); |
|||
} |
|||
|
|||
getMeta() { |
|||
return this.getChildProp(FIELD_META_PROP); |
|||
} |
|||
|
|||
getField() { |
|||
return this.getChildProp(FIELD_DATA_PROP); |
|||
} |
|||
|
|||
onHelpAnimEnd = (_key: string, helpShow: boolean) => { |
|||
this.helpShow = helpShow; |
|||
if (!helpShow) { |
|||
this.setState({}); |
|||
} |
|||
}; |
|||
|
|||
renderHelp(prefixCls: string) { |
|||
const help = this.getHelpMessage(); |
|||
const children = help ? ( |
|||
<div className={`${prefixCls}-explain`} key="help"> |
|||
{help} |
|||
</div> |
|||
) : null; |
|||
if (children) { |
|||
this.helpShow = !!children; |
|||
} |
|||
return ( |
|||
<Animate |
|||
transitionName="show-help" |
|||
component="" |
|||
transitionAppear |
|||
key="help" |
|||
onEnd={this.onHelpAnimEnd} |
|||
> |
|||
{children} |
|||
</Animate> |
|||
); |
|||
} |
|||
|
|||
renderExtra(prefixCls: string) { |
|||
const { extra } = this.props; |
|||
return extra ? <div className={`${prefixCls}-extra`}>{extra}</div> : 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( |
|||
prefixCls: string, |
|||
c1: React.ReactNode, |
|||
c2: React.ReactNode, |
|||
c3: React.ReactNode, |
|||
) { |
|||
const { props } = this; |
|||
const onlyControl = this.getOnlyControl; |
|||
const validateStatus = |
|||
props.validateStatus === undefined && onlyControl |
|||
? this.getValidateStatus() |
|||
: props.validateStatus; |
|||
|
|||
let classes = `${prefixCls}-item-control`; |
|||
if (validateStatus) { |
|||
classes = classNames(`${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', |
|||
}); |
|||
} |
|||
|
|||
let iconType = ''; |
|||
switch (validateStatus) { |
|||
case 'success': |
|||
iconType = 'check-circle'; |
|||
break; |
|||
case 'warning': |
|||
iconType = 'exclamation-circle'; |
|||
break; |
|||
case 'error': |
|||
iconType = 'close-circle'; |
|||
break; |
|||
case 'validating': |
|||
iconType = 'loading'; |
|||
break; |
|||
default: |
|||
iconType = ''; |
|||
break; |
|||
} |
|||
|
|||
const icon = |
|||
props.hasFeedback && iconType ? ( |
|||
<span className={`${prefixCls}-item-children-icon`}> |
|||
<Icon type={iconType} theme={iconType === 'loading' ? 'outlined' : 'filled'} /> |
|||
</span> |
|||
) : null; |
|||
|
|||
return ( |
|||
<div className={classes}> |
|||
<span className={`${prefixCls}-item-children`}> |
|||
{c1} |
|||
{icon} |
|||
</span> |
|||
{c2} |
|||
{c3} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
renderWrapper(prefixCls: string, children: React.ReactNode) { |
|||
return ( |
|||
<FormContext.Consumer key="wrapper"> |
|||
{({ wrapperCol: contextWrapperCol, vertical }: FormContextProps) => { |
|||
const { wrapperCol } = this.props; |
|||
const mergedWrapperCol: ColProps = |
|||
('wrapperCol' in this.props ? wrapperCol : contextWrapperCol) || {}; |
|||
|
|||
const className = classNames( |
|||
`${prefixCls}-item-control-wrapper`, |
|||
mergedWrapperCol.className, |
|||
); |
|||
|
|||
// No pass FormContext since it's useless
|
|||
return ( |
|||
<FormContext.Provider value={{ vertical }}> |
|||
<Col {...mergedWrapperCol} className={className}> |
|||
{children} |
|||
</Col> |
|||
</FormContext.Provider> |
|||
); |
|||
}} |
|||
</FormContext.Consumer> |
|||
); |
|||
} |
|||
|
|||
isRequired() { |
|||
const { required } = this.props; |
|||
if (required !== undefined) { |
|||
return required; |
|||
} |
|||
if (this.getOnlyControl()) { |
|||
const meta = this.getMeta() || {}; |
|||
const validate = meta.validate || []; |
|||
|
|||
return validate |
|||
.filter((item: any) => !!item.rules) |
|||
.some((item: any) => { |
|||
return item.rules.some((rule: any) => rule.required); |
|||
}); |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
// Resolve duplicated ids bug between different forms
|
|||
// https://github.com/ant-design/ant-design/issues/7351
|
|||
onLabelClick = () => { |
|||
const id = this.props.id || this.getId(); |
|||
if (!id) { |
|||
return; |
|||
} |
|||
|
|||
const formItemNode = ReactDOM.findDOMNode(this) as Element; |
|||
const control = formItemNode.querySelector(`[id="${id}"]`) as HTMLElement; |
|||
if (control && control.focus) { |
|||
control.focus(); |
|||
} |
|||
}; |
|||
|
|||
renderLabel(prefixCls: string) { |
|||
return ( |
|||
<FormContext.Consumer key="label"> |
|||
{({ |
|||
vertical, |
|||
labelAlign: contextLabelAlign, |
|||
labelCol: contextLabelCol, |
|||
colon: contextColon, |
|||
}: FormContextProps) => { |
|||
const { label, labelCol, labelAlign, colon, id, htmlFor } = this.props; |
|||
const required = this.isRequired(); |
|||
|
|||
const mergedLabelCol: ColProps = |
|||
('labelCol' in this.props ? labelCol : contextLabelCol) || {}; |
|||
|
|||
const mergedLabelAlign: FormLabelAlign | undefined = |
|||
'labelAlign' in this.props ? labelAlign : contextLabelAlign; |
|||
|
|||
const labelClsBasic = `${prefixCls}-item-label`; |
|||
const labelColClassName = classNames( |
|||
labelClsBasic, |
|||
mergedLabelAlign === 'left' && `${labelClsBasic}-left`, |
|||
mergedLabelCol.className, |
|||
); |
|||
|
|||
let labelChildren = label; |
|||
// Keep label is original where there should have no colon
|
|||
const computedColon = colon === true || (contextColon !== false && colon !== false); |
|||
const haveColon = computedColon && !vertical; |
|||
// Remove duplicated user input colon
|
|||
if (haveColon && typeof label === 'string' && (label as string).trim() !== '') { |
|||
labelChildren = (label as string).replace(/[:|:]\s*$/, ''); |
|||
} |
|||
|
|||
const labelClassName = classNames({ |
|||
[`${prefixCls}-item-required`]: required, |
|||
[`${prefixCls}-item-no-colon`]: !computedColon, |
|||
}); |
|||
|
|||
return label ? ( |
|||
<Col {...mergedLabelCol} className={labelColClassName}> |
|||
<label |
|||
htmlFor={htmlFor || id || this.getId()} |
|||
className={labelClassName} |
|||
title={typeof label === 'string' ? label : ''} |
|||
onClick={this.onLabelClick} |
|||
> |
|||
{labelChildren} |
|||
</label> |
|||
</Col> |
|||
) : null; |
|||
}} |
|||
</FormContext.Consumer> |
|||
); |
|||
} |
|||
|
|||
renderChildren(prefixCls: string) { |
|||
const { children } = this.props; |
|||
return [ |
|||
this.renderLabel(prefixCls), |
|||
this.renderWrapper( |
|||
prefixCls, |
|||
this.renderValidateWrapper( |
|||
prefixCls, |
|||
children, |
|||
this.renderHelp(prefixCls), |
|||
this.renderExtra(prefixCls), |
|||
), |
|||
), |
|||
]; |
|||
} |
|||
|
|||
renderFormItem = ({ getPrefixCls }: ConfigConsumerProps) => { |
|||
const { prefixCls: customizePrefixCls, style, className } = this.props; |
|||
const prefixCls = getPrefixCls('form', customizePrefixCls); |
|||
const children = this.renderChildren(prefixCls); |
|||
const itemClassName = { |
|||
[`${prefixCls}-item`]: true, |
|||
[`${prefixCls}-item-with-help`]: this.helpShow, |
|||
[`${className}`]: !!className, |
|||
}; |
|||
|
|||
return ( |
|||
<Row className={classNames(itemClassName)} style={style} key="row"> |
|||
{children} |
|||
</Row> |
|||
); |
|||
}; |
|||
|
|||
render() { |
|||
return <ConfigConsumer>{this.renderFormItem}</ConfigConsumer>; |
|||
} |
|||
} |
File diff suppressed because it is too large
@ -1,149 +0,0 @@ |
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
|||
|
|||
exports[`Form should \`labelAlign\` work in FormItem 1`] = ` |
|||
<div |
|||
class="ant-row ant-form-item" |
|||
> |
|||
<div |
|||
class="ant-col ant-form-item-label ant-form-item-label-left" |
|||
> |
|||
<label |
|||
class="" |
|||
title="test" |
|||
> |
|||
test |
|||
</label> |
|||
</div> |
|||
<div |
|||
class="ant-col ant-form-item-control-wrapper" |
|||
> |
|||
<div |
|||
class="ant-form-item-control" |
|||
> |
|||
<span |
|||
class="ant-form-item-children" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
`; |
|||
|
|||
exports[`Form should \`labelAlign\` work in FormItem 2`] = ` |
|||
<form |
|||
class="ant-form ant-form-horizontal" |
|||
> |
|||
<div |
|||
class="ant-row ant-form-item" |
|||
> |
|||
<div |
|||
class="ant-col ant-form-item-label ant-form-item-label-left" |
|||
> |
|||
<label |
|||
class="" |
|||
title="test" |
|||
> |
|||
test |
|||
</label> |
|||
</div> |
|||
<div |
|||
class="ant-col ant-form-item-control-wrapper" |
|||
> |
|||
<div |
|||
class="ant-form-item-control" |
|||
> |
|||
<span |
|||
class="ant-form-item-children" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
`; |
|||
|
|||
exports[`Form should props colon of Form.Item override the props colon of Form. 1`] = ` |
|||
<form |
|||
class="ant-form ant-form-horizontal" |
|||
> |
|||
<div |
|||
class="ant-row ant-form-item" |
|||
> |
|||
<div |
|||
class="ant-col ant-form-item-label" |
|||
> |
|||
<label |
|||
class="ant-form-item-no-colon" |
|||
title="label" |
|||
> |
|||
label |
|||
</label> |
|||
</div> |
|||
<div |
|||
class="ant-col ant-form-item-control-wrapper" |
|||
> |
|||
<div |
|||
class="ant-form-item-control" |
|||
> |
|||
<span |
|||
class="ant-form-item-children" |
|||
> |
|||
input |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div |
|||
class="ant-row ant-form-item" |
|||
> |
|||
<div |
|||
class="ant-col ant-form-item-label" |
|||
> |
|||
<label |
|||
class="" |
|||
title="label" |
|||
> |
|||
label |
|||
</label> |
|||
</div> |
|||
<div |
|||
class="ant-col ant-form-item-control-wrapper" |
|||
> |
|||
<div |
|||
class="ant-form-item-control" |
|||
> |
|||
<span |
|||
class="ant-form-item-children" |
|||
> |
|||
input |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div |
|||
class="ant-row ant-form-item" |
|||
> |
|||
<div |
|||
class="ant-col ant-form-item-label" |
|||
> |
|||
<label |
|||
class="ant-form-item-no-colon" |
|||
title="label" |
|||
> |
|||
label |
|||
</label> |
|||
</div> |
|||
<div |
|||
class="ant-col ant-form-item-control-wrapper" |
|||
> |
|||
<div |
|||
class="ant-form-item-control" |
|||
> |
|||
<span |
|||
class="ant-form-item-children" |
|||
> |
|||
input |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
`; |
@ -1,151 +0,0 @@ |
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
|||
|
|||
exports[`Form should display custom message 1`] = ` |
|||
<form |
|||
class="ant-form ant-form-horizontal" |
|||
> |
|||
<div |
|||
class="ant-row ant-form-item ant-form-item-with-help" |
|||
> |
|||
<div |
|||
class="ant-col ant-form-item-label" |
|||
> |
|||
<label |
|||
class="" |
|||
for="account" |
|||
title="Account" |
|||
> |
|||
Account |
|||
</label> |
|||
</div> |
|||
<div |
|||
class="ant-col ant-form-item-control-wrapper" |
|||
> |
|||
<div |
|||
class="ant-form-item-control has-error" |
|||
> |
|||
<span |
|||
class="ant-form-item-children" |
|||
> |
|||
<input |
|||
data-__field="[object Object]" |
|||
data-__meta="[object Object]" |
|||
id="account" |
|||
value="antd" |
|||
/> |
|||
</span> |
|||
<div |
|||
class="ant-form-explain show-help-enter" |
|||
> |
|||
<span> |
|||
Account does not exist, |
|||
<a |
|||
href="https://www.alipay.com/" |
|||
rel="noopener noreferrer" |
|||
target="_blank" |
|||
> |
|||
Forgot account? |
|||
</a> |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
`; |
|||
|
|||
exports[`Form should display two message 1`] = ` |
|||
<form |
|||
class="ant-form ant-form-horizontal" |
|||
> |
|||
<div |
|||
class="ant-row ant-form-item ant-form-item-with-help" |
|||
> |
|||
<div |
|||
class="ant-col ant-form-item-label" |
|||
> |
|||
<label |
|||
class="" |
|||
for="account" |
|||
title="Account" |
|||
> |
|||
Account |
|||
</label> |
|||
</div> |
|||
<div |
|||
class="ant-col ant-form-item-control-wrapper" |
|||
> |
|||
<div |
|||
class="ant-form-item-control has-error" |
|||
> |
|||
<span |
|||
class="ant-form-item-children" |
|||
> |
|||
<input |
|||
data-__field="[object Object]" |
|||
data-__meta="[object Object]" |
|||
id="account" |
|||
value="+=-/" |
|||
/> |
|||
</span> |
|||
<div |
|||
class="ant-form-explain show-help-enter" |
|||
> |
|||
Error message 1 Error message 2 |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
`; |
|||
|
|||
exports[`Form support error message with reactNode 1`] = ` |
|||
<form |
|||
class="ant-form ant-form-horizontal" |
|||
> |
|||
<div |
|||
class="ant-row ant-form-item ant-form-item-with-help" |
|||
> |
|||
<div |
|||
class="ant-col ant-form-item-label" |
|||
> |
|||
<label |
|||
class="" |
|||
for="account" |
|||
title="Account" |
|||
> |
|||
Account |
|||
</label> |
|||
</div> |
|||
<div |
|||
class="ant-col ant-form-item-control-wrapper" |
|||
> |
|||
<div |
|||
class="ant-form-item-control has-error" |
|||
> |
|||
<span |
|||
class="ant-form-item-children" |
|||
> |
|||
<input |
|||
data-__field="[object Object]" |
|||
data-__meta="[object Object]" |
|||
id="account" |
|||
value="" |
|||
/> |
|||
</span> |
|||
<div |
|||
class="ant-form-explain show-help-enter" |
|||
> |
|||
<div> |
|||
Error 1 |
|||
</div> |
|||
|
|||
<div> |
|||
Error 2 |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
`; |
@ -1,3 +0,0 @@ |
|||
import demoTest from '../../../tests/shared/demoTest'; |
|||
|
|||
demoTest('form'); |
@ -1,45 +0,0 @@ |
|||
/* eslint-disable react/prefer-stateless-function */ |
|||
import React from 'react'; |
|||
import { mount } from 'enzyme'; |
|||
import Form from '..'; |
|||
|
|||
describe('Form', () => { |
|||
it('hideRequiredMark', () => { |
|||
const wrapper = mount(<Form hideRequiredMark />); |
|||
expect(wrapper.find('form').hasClass('ant-form-hide-required-mark')).toBe(true); |
|||
}); |
|||
|
|||
describe('wrappedComponentRef', () => { |
|||
it('warns on functional component', () => { |
|||
if (process.env.REACT === '15') { |
|||
return; |
|||
} |
|||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); |
|||
const TestForm = () => <Form />; |
|||
const Wrapped = Form.create()(TestForm); |
|||
mount(<Wrapped wrappedComponentRef={() => {}} />); |
|||
expect(spy).toHaveBeenCalled(); |
|||
spy.mockReset(); |
|||
spy.mockRestore(); |
|||
}); |
|||
|
|||
it('get component ref', () => { |
|||
class TestForm extends React.Component { |
|||
// eslint-disable-line
|
|||
render() { |
|||
return <Form />; |
|||
} |
|||
} |
|||
const Wrapped = Form.create()(TestForm); |
|||
let form; |
|||
mount( |
|||
<Wrapped |
|||
wrappedComponentRef={node => { |
|||
form = node; |
|||
}} |
|||
/>, |
|||
); |
|||
expect(form).toBeInstanceOf(TestForm); |
|||
}); |
|||
}); |
|||
}); |
@ -1,265 +0,0 @@ |
|||
import React from 'react'; |
|||
import { mount, render } from 'enzyme'; |
|||
import Form from '..'; |
|||
|
|||
describe('Form', () => { |
|||
// Mock of `querySelector`
|
|||
const originQuerySelector = HTMLElement.prototype.querySelector; |
|||
HTMLElement.prototype.querySelector = function querySelector(str) { |
|||
const match = str.match(/^\[id=('|")(.*)('|")]$/); |
|||
const id = match && match[2]; |
|||
|
|||
// Use origin logic
|
|||
if (id) { |
|||
const [input] = this.getElementsByTagName('input'); |
|||
if (input && input.id === id) { |
|||
return input; |
|||
} |
|||
} |
|||
|
|||
return originQuerySelector.call(this, str); |
|||
}; |
|||
|
|||
afterAll(() => { |
|||
HTMLElement.prototype.querySelector = originQuerySelector; |
|||
}); |
|||
|
|||
it('should remove duplicated user input colon', () => { |
|||
const wrapper = mount( |
|||
<Form> |
|||
<Form.Item label="label:">input</Form.Item> |
|||
<Form.Item label="label:">input</Form.Item> |
|||
</Form>, |
|||
); |
|||
|
|||
expect( |
|||
wrapper |
|||
.find('.ant-form-item-label label') |
|||
.at(0) |
|||
.text(), |
|||
).not.toContain(':'); |
|||
expect( |
|||
wrapper |
|||
.find('.ant-form-item-label label') |
|||
.at(1) |
|||
.text(), |
|||
).not.toContain(':'); |
|||
}); |
|||
|
|||
it('should disable colon when props colon Form is false', () => { |
|||
const wrapper = mount( |
|||
<Form colon={false}> |
|||
<Form.Item label="label">input</Form.Item> |
|||
</Form>, |
|||
); |
|||
expect( |
|||
wrapper |
|||
.find('.ant-form-item-label label') |
|||
.at(0) |
|||
.hasClass('ant-form-item-no-colon'), |
|||
).toBe(true); |
|||
}); |
|||
|
|||
it('should props colon of Form.Item override the props colon of Form.', () => { |
|||
const wrapper = mount( |
|||
<Form colon={false}> |
|||
<Form.Item label="label">input</Form.Item> |
|||
<Form.Item label="label" colon> |
|||
input |
|||
</Form.Item> |
|||
<Form.Item label="label" colon={false}> |
|||
input |
|||
</Form.Item> |
|||
</Form>, |
|||
); |
|||
expect(wrapper.render()).toMatchSnapshot(); |
|||
|
|||
const testLabel = mount( |
|||
<Form colon={false}> |
|||
<Form.Item label="label:" colon> |
|||
input |
|||
</Form.Item> |
|||
<Form.Item label="label:" colon> |
|||
input |
|||
</Form.Item> |
|||
</Form>, |
|||
); |
|||
expect( |
|||
testLabel |
|||
.find('.ant-form-item-label label') |
|||
.at(0) |
|||
.text(), |
|||
).not.toContain(':'); |
|||
expect( |
|||
testLabel |
|||
.find('.ant-form-item-label label') |
|||
.at(1) |
|||
.text(), |
|||
).not.toContain(':'); |
|||
}); |
|||
|
|||
it('should not remove duplicated user input colon when props colon is false', () => { |
|||
const wrapper = mount( |
|||
<Form> |
|||
<Form.Item label="label:" colon={false}> |
|||
input |
|||
</Form.Item> |
|||
<Form.Item label="label:" colon={false}> |
|||
input |
|||
</Form.Item> |
|||
</Form>, |
|||
); |
|||
expect( |
|||
wrapper |
|||
.find('.ant-form-item-label label') |
|||
.at(0) |
|||
.text(), |
|||
).toContain(':'); |
|||
expect( |
|||
wrapper |
|||
.find('.ant-form-item-label label') |
|||
.at(1) |
|||
.text(), |
|||
).toContain(':'); |
|||
}); |
|||
|
|||
it('should not remove duplicated user input colon when layout is vertical', () => { |
|||
const wrapper = mount( |
|||
<Form layout="vertical"> |
|||
<Form.Item label="label:">input</Form.Item> |
|||
<Form.Item label="label:">input</Form.Item> |
|||
</Form>, |
|||
); |
|||
expect( |
|||
wrapper |
|||
.find('.ant-form-item-label label') |
|||
.at(0) |
|||
.text(), |
|||
).toContain(':'); |
|||
expect( |
|||
wrapper |
|||
.find('.ant-form-item-label label') |
|||
.at(1) |
|||
.text(), |
|||
).toContain(':'); |
|||
}); |
|||
|
|||
it('should has dom with .ant-form-item-control-wrapper', () => { |
|||
const formItemLayout = { |
|||
labelCol: { span: 6 }, |
|||
wrapperCol: { span: 14 }, |
|||
}; |
|||
const wrapper = mount( |
|||
<Form> |
|||
<Form.Item {...formItemLayout}>input</Form.Item> |
|||
<Form.Item>input</Form.Item> |
|||
</Form>, |
|||
); |
|||
expect(wrapper.find('.ant-form-item-control-wrapper').hostNodes().length).toBe(2); |
|||
expect(wrapper.find('.ant-form-item-control-wrapper.ant-col-14').length).toBe(1); |
|||
}); |
|||
|
|||
// https://github.com/ant-design/ant-design/issues/7351
|
|||
it('focus correct input when click label', () => { |
|||
const Form1 = Form.create()(({ form }) => ( |
|||
<Form> |
|||
<Form.Item label="label 1">{form.getFieldDecorator('test')(<input />)}</Form.Item> |
|||
</Form> |
|||
)); |
|||
const Form2 = Form.create()(({ form }) => ( |
|||
<Form> |
|||
<Form.Item label="label 2">{form.getFieldDecorator('test2')(<input />)}</Form.Item> |
|||
</Form> |
|||
)); |
|||
const wrapper = mount( |
|||
<div> |
|||
<Form1 /> |
|||
<Form2 /> |
|||
</div>, |
|||
); |
|||
wrapper |
|||
.find('Form label') |
|||
.at(0) |
|||
.simulate('click'); |
|||
expect( |
|||
wrapper |
|||
.find('Form input') |
|||
.at(0) |
|||
.getDOMNode(), |
|||
).toBe(document.activeElement); |
|||
wrapper |
|||
.find('Form label') |
|||
.at(1) |
|||
.simulate('click'); |
|||
expect( |
|||
wrapper |
|||
.find('Form input') |
|||
.at(1) |
|||
.getDOMNode(), |
|||
).toBe(document.activeElement); |
|||
}); |
|||
|
|||
// https://github.com/ant-design/ant-design/issues/7693
|
|||
it('should not throw error when is not a valid id', () => { |
|||
const Form1 = Form.create()(({ form }) => ( |
|||
<Form> |
|||
<Form.Item label="label 1"> |
|||
{form.getFieldDecorator('member[0].name.firstname')(<input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
)); |
|||
const wrapper = mount(<Form1 />); |
|||
expect(() => { |
|||
wrapper |
|||
.find('Form label') |
|||
.at(0) |
|||
.simulate('click'); |
|||
}).not.toThrow(); |
|||
expect( |
|||
wrapper |
|||
.find('Form input') |
|||
.at(0) |
|||
.getDOMNode(), |
|||
).toBe(document.activeElement); |
|||
}); |
|||
|
|||
it('should `labelAlign` work in FormItem', () => { |
|||
// Works in Form.Item
|
|||
expect(render(<Form.Item label="test" labelAlign="left" />)).toMatchSnapshot(); |
|||
|
|||
// Use Form.Item first
|
|||
expect( |
|||
render( |
|||
<Form labelAlign="right"> |
|||
<Form.Item label="test" labelAlign="left" /> |
|||
</Form>, |
|||
), |
|||
).toMatchSnapshot(); |
|||
}); |
|||
|
|||
describe('should `htmlFor` work', () => { |
|||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); |
|||
|
|||
afterEach(() => { |
|||
errorSpy.mockReset(); |
|||
}); |
|||
|
|||
afterAll(() => { |
|||
errorSpy.mockRestore(); |
|||
}); |
|||
|
|||
it('should warning when use `id`', () => { |
|||
mount(<Form.Item id="bamboo" label="bamboo" />); |
|||
|
|||
expect(errorSpy).toHaveBeenCalledWith( |
|||
'Warning: [antd: Form.Item] `id` is deprecated for its label `htmlFor`. Please use `htmlFor` directly.', |
|||
); |
|||
}); |
|||
|
|||
it('use `htmlFor`', () => { |
|||
const wrapper = mount(<Form.Item htmlFor="bamboo" label="bamboo" />); |
|||
|
|||
expect(wrapper.find('label').props().htmlFor).toBe('bamboo'); |
|||
}); |
|||
}); |
|||
}); |
@ -1,129 +0,0 @@ |
|||
import React from 'react'; |
|||
import { mount } from 'enzyme'; |
|||
import Form from '..'; |
|||
|
|||
describe('Form', () => { |
|||
it('should display two message', () => { |
|||
const rules = [ |
|||
{ |
|||
pattern: /^\w+$/, |
|||
message: 'Error message 1', |
|||
}, |
|||
{ |
|||
pattern: /^\w+$/, |
|||
message: 'Error message 2', |
|||
}, |
|||
]; |
|||
let myForm; |
|||
const Form1 = Form.create()(({ form }) => { |
|||
myForm = form; |
|||
return ( |
|||
<Form> |
|||
<Form.Item label="Account"> |
|||
{form.getFieldDecorator('account', { initialValue: '+=-/', rules })(<input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}); |
|||
|
|||
const wrapper = mount(<Form1 />); |
|||
myForm.validateFields(); |
|||
|
|||
wrapper.update(); |
|||
expect(wrapper.render()).toMatchSnapshot(); |
|||
}); |
|||
|
|||
it('should display custom message', () => { |
|||
const rules = [ |
|||
{ |
|||
pattern: /^$/, |
|||
message: ( |
|||
<span> |
|||
Account does not exist,{' '} |
|||
<a rel="noopener noreferrer" href="https://www.alipay.com/" target="_blank"> |
|||
Forgot account? |
|||
</a> |
|||
</span> |
|||
), |
|||
}, |
|||
]; |
|||
let myForm; |
|||
const Form1 = Form.create()(({ form }) => { |
|||
myForm = form; |
|||
return ( |
|||
<Form> |
|||
<Form.Item label="Account"> |
|||
{form.getFieldDecorator('account', { initialValue: 'antd', rules })(<input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}); |
|||
|
|||
const wrapper = mount(<Form1 />); |
|||
myForm.validateFields(); |
|||
|
|||
wrapper.update(); |
|||
expect(wrapper.render()).toMatchSnapshot(); |
|||
}); |
|||
|
|||
it('support error message with reactNode', () => { |
|||
let myForm; |
|||
const Form1 = Form.create()(({ form }) => { |
|||
myForm = form; |
|||
return ( |
|||
<Form> |
|||
<Form.Item label="Account">{form.getFieldDecorator('account')(<input />)}</Form.Item> |
|||
</Form> |
|||
); |
|||
}); |
|||
|
|||
const wrapper = mount(<Form1 />); |
|||
|
|||
myForm.setFields({ |
|||
account: { |
|||
errors: [<div>Error 1</div>, <div>Error 2</div>], |
|||
}, |
|||
}); |
|||
|
|||
expect(wrapper.render()).toMatchSnapshot(); |
|||
}); |
|||
|
|||
it('should print warning for not generating help and validateStatus automatically', () => { |
|||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); |
|||
const Form1 = Form.create()(({ form }) => { |
|||
return ( |
|||
<Form> |
|||
<Form.Item label="Account"> |
|||
{form.getFieldDecorator('account')(<input />)} |
|||
{form.getFieldDecorator('account')(<input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}); |
|||
|
|||
mount(<Form1 />); |
|||
expect(errorSpy).toHaveBeenCalledWith( |
|||
'Warning: [antd: Form.Item] Cannot generate `validateStatus` and `help` automatically, while there are more than one `getFieldDecorator` in it.', |
|||
); |
|||
errorSpy.mockRestore(); |
|||
}); |
|||
|
|||
// https://github.com/ant-design/ant-design/issues/14911
|
|||
it('should not print warning for not generating help and validateStatus automatically when help or validateStatus is specified', () => { |
|||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); |
|||
const Form1 = Form.create()(({ form }) => { |
|||
return ( |
|||
<Form> |
|||
<Form.Item label="Account" help="custom help information"> |
|||
{form.getFieldDecorator('account')(<input />)} |
|||
{form.getFieldDecorator('account')(<input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}); |
|||
|
|||
mount(<Form1 />); |
|||
expect(errorSpy).not.toHaveBeenCalled(); |
|||
errorSpy.mockRestore(); |
|||
}); |
|||
}); |
@ -1,91 +0,0 @@ |
|||
/* tslint:disable */ |
|||
import * as React from 'react'; |
|||
import Form, { FormComponentProps, FormCreateOption } from '../Form'; |
|||
|
|||
// test Form.create on component without own props
|
|||
class WithoutOwnProps extends React.Component<any, any> { |
|||
state = { |
|||
foo: 'bar', |
|||
}; |
|||
render() { |
|||
return <div>foo</div>; |
|||
} |
|||
} |
|||
|
|||
const WithoutOwnPropsForm = Form.create()(WithoutOwnProps); |
|||
|
|||
<WithoutOwnPropsForm />; |
|||
|
|||
// test Form.create on component with own props
|
|||
interface WithOwnPropsProps extends FormComponentProps { |
|||
name: string; |
|||
} |
|||
|
|||
class WithOwnProps extends React.Component<WithOwnPropsProps, any> { |
|||
state = { |
|||
foo: 'bar', |
|||
}; |
|||
|
|||
render() { |
|||
return <div>foo</div>; |
|||
} |
|||
} |
|||
|
|||
const WithOwnPropsForm = Form.create<WithOwnPropsProps>()(WithOwnProps); |
|||
|
|||
<WithOwnPropsForm name="foo" />; |
|||
|
|||
// test Form.create with options
|
|||
interface WithCreateOptionsProps extends FormComponentProps { |
|||
username: string; |
|||
} |
|||
|
|||
class WithCreateOptions extends React.Component<WithCreateOptionsProps, {}> { |
|||
render() { |
|||
return <div>foo</div>; |
|||
} |
|||
} |
|||
|
|||
const mapPropsToFields = (props: WithCreateOptionsProps) => { |
|||
const { username } = props; |
|||
|
|||
return { |
|||
username: Form.createFormField({ value: username }), |
|||
}; |
|||
}; |
|||
|
|||
const formOptions: FormCreateOption<WithCreateOptionsProps> = { mapPropsToFields }; |
|||
|
|||
const WithCreateOptionsForm = Form.create(formOptions)(WithCreateOptions); |
|||
|
|||
<WithCreateOptionsForm username="foo" />; |
|||
|
|||
// Should work with forwardRef & wrappedComponentRef
|
|||
// https://github.com/ant-design/ant-design/issues/16229
|
|||
if (React.forwardRef) { |
|||
interface ForwardProps extends FormComponentProps { |
|||
str: string; |
|||
} |
|||
|
|||
const ForwardDemo = React.forwardRef(({ str }: ForwardProps, ref: React.Ref<HTMLDivElement>) => { |
|||
return <div ref={ref}>{str || ''}</div>; |
|||
}); |
|||
const WrappedForwardDemo = Form.create<ForwardProps>()(ForwardDemo); |
|||
<WrappedForwardDemo str="" />; |
|||
} |
|||
|
|||
interface WrappedRefProps extends FormComponentProps { |
|||
str: string; |
|||
test?: () => void; |
|||
} |
|||
class WrapRefDemo extends React.Component<WrappedRefProps> { |
|||
public getForm() { |
|||
return this.props.form; |
|||
} |
|||
public render() { |
|||
return <div>{this.props.str || ''}</div>; |
|||
} |
|||
} |
|||
|
|||
const WrappedWrapRefDemo = Form.create<WrappedRefProps>()(WrapRefDemo); |
|||
<WrappedWrapRefDemo str="" wrappedComponentRef={() => null} />; |
@ -1,2 +0,0 @@ |
|||
export const FIELD_META_PROP = 'data-__meta'; |
|||
export const FIELD_DATA_PROP = 'data-__field'; |
@ -1,16 +0,0 @@ |
|||
import createReactContext from '@ant-design/create-react-context'; |
|||
import { ColProps } from '../grid/col'; |
|||
import { FormLabelAlign } from './FormItem'; |
|||
|
|||
export interface FormContextProps { |
|||
vertical: boolean; |
|||
colon?: boolean; |
|||
labelAlign?: FormLabelAlign; |
|||
labelCol?: ColProps; |
|||
wrapperCol?: ColProps; |
|||
} |
|||
|
|||
export const FormContext = createReactContext<FormContextProps>({ |
|||
labelAlign: 'right', |
|||
vertical: false, |
|||
}); |
@ -1,130 +0,0 @@ |
|||
--- |
|||
order: 3 |
|||
title: |
|||
zh-CN: 高级搜索 |
|||
en-US: Advanced search |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
三列栅格式的表单排列方式,常用于数据表格的高级搜索。 |
|||
|
|||
有部分定制的样式代码,由于输入标签长度不确定,需要根据具体情况自行调整。 |
|||
|
|||
## en-US |
|||
|
|||
Three columns layout is often used for advanced searching of data table. |
|||
|
|||
Because the width of label is not fixed, you may need to adjust it by customizing its style. |
|||
|
|||
```jsx |
|||
import { Form, Row, Col, Input, Button, Icon } from 'antd'; |
|||
|
|||
class AdvancedSearchForm extends React.Component { |
|||
state = { |
|||
expand: false, |
|||
}; |
|||
|
|||
// 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' }}> |
|||
<Form.Item label={`Field ${i}`}> |
|||
{getFieldDecorator(`field-${i}`, { |
|||
rules: [ |
|||
{ |
|||
required: true, |
|||
message: 'Input something!', |
|||
}, |
|||
], |
|||
})(<Input placeholder="placeholder" />)} |
|||
</Form.Item> |
|||
</Col>, |
|||
); |
|||
} |
|||
return children; |
|||
} |
|||
|
|||
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 }); |
|||
}; |
|||
|
|||
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({ name: 'advanced_search' })(AdvancedSearchForm); |
|||
ReactDOM.render( |
|||
<div> |
|||
<WrappedAdvancedSearchForm /> |
|||
<div className="search-result-list">Search Result List</div> |
|||
</div>, |
|||
mountNode, |
|||
); |
|||
``` |
|||
|
|||
```css |
|||
.ant-advanced-search-form { |
|||
padding: 24px; |
|||
background: #fbfbfb; |
|||
border: 1px solid #d9d9d9; |
|||
border-radius: 6px; |
|||
} |
|||
|
|||
.ant-advanced-search-form .ant-form-item { |
|||
display: flex; |
|||
} |
|||
|
|||
.ant-advanced-search-form .ant-form-item-control-wrapper { |
|||
flex: 1; |
|||
} |
|||
``` |
|||
|
|||
<style> |
|||
#components-form-demo-advanced-search .ant-form { |
|||
max-width: none; |
|||
} |
|||
#components-form-demo-advanced-search .search-result-list { |
|||
margin-top: 16px; |
|||
border: 1px dashed #e9e9e9; |
|||
border-radius: 6px; |
|||
background-color: #fafafa; |
|||
min-height: 200px; |
|||
text-align: center; |
|||
padding-top: 80px; |
|||
} |
|||
</style> |
@ -1,73 +0,0 @@ |
|||
--- |
|||
order: 11 |
|||
title: |
|||
zh-CN: 表单联动 |
|||
en-US: Coordinated Controls |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
使用 `setFieldsValue` 来动态设置其他控件的值。 |
|||
|
|||
## en-US |
|||
|
|||
Use `setFieldsValue` to set other control's value programmaticly. |
|||
|
|||
```jsx |
|||
import { Form, Select, Input, Button } from 'antd'; |
|||
|
|||
const { Option } = Select; |
|||
|
|||
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 labelCol={{ span: 5 }} wrapperCol={{ span: 12 }} onSubmit={this.handleSubmit}> |
|||
<Form.Item label="Note"> |
|||
{getFieldDecorator('note', { |
|||
rules: [{ required: true, message: 'Please input your note!' }], |
|||
})(<Input />)} |
|||
</Form.Item> |
|||
<Form.Item label="Gender"> |
|||
{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>, |
|||
)} |
|||
</Form.Item> |
|||
<Form.Item wrapperCol={{ span: 12, offset: 5 }}> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const WrappedApp = Form.create({ name: 'coordinated' })(App); |
|||
|
|||
ReactDOM.render(<WrappedApp />, mountNode); |
|||
``` |
@ -1,147 +0,0 @@ |
|||
--- |
|||
order: 7 |
|||
title: |
|||
zh-CN: 自定义表单控件 |
|||
en-US: Customized Form Controls |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
自定义或第三方的表单控件,也可以与 Form 组件一起使用。只要该组件遵循以下的约定: |
|||
|
|||
> - 提供受控属性 `value` 或其它与 [`valuePropName`](http://ant.design/components/form/#getFieldDecorator-参数) 的值同名的属性。 |
|||
> - 提供 `onChange` 事件或 [`trigger`](http://ant.design/components/form/#getFieldDecorator-参数) 的值同名的事件。 |
|||
> - 支持 ref: |
|||
> - React@16.3.0 之前只有 Class 组件支持。 |
|||
> - React@16.3.0 及之后可以通过 [forwardRef](https://reactjs.org/docs/forwarding-refs.html) 添加 ref 支持。([示例](https://codesandbox.io/s/7wj199900x)) |
|||
|
|||
## en-US |
|||
|
|||
Customized or third-party form controls can be used in Form, too. Controls must follow these conventions: |
|||
|
|||
> - It has a controlled property `value` or other name which is equal to the value of [`valuePropName`](http://ant.design/components/form/?locale=en-US#getFieldDecorator's-parameters). |
|||
> - It has event `onChange` or an event which name is equal to the value of [`trigger`](http://ant.design/components/form/?locale=en-US#getFieldDecorator's-parameters). |
|||
> - Support ref: |
|||
> - Can only use class component before React@16.3.0. |
|||
> - Can use [forwardRef](https://reactjs.org/docs/forwarding-refs.html) to add ref support after React@16.3.0. ([Sample](https://codesandbox.io/s/7wj199900x)) |
|||
|
|||
```jsx |
|||
import { Form, Input, Select, Button } from 'antd'; |
|||
|
|||
const { Option } = Select; |
|||
|
|||
class PriceInput extends React.Component { |
|||
static getDerivedStateFromProps(nextProps) { |
|||
// Should be a controlled component. |
|||
if ('value' in nextProps) { |
|||
return { |
|||
...(nextProps.value || {}), |
|||
}; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
|
|||
const value = props.value || {}; |
|||
this.state = { |
|||
number: value.number || 0, |
|||
currency: value.currency || 'rmb', |
|||
}; |
|||
} |
|||
|
|||
handleNumberChange = e => { |
|||
const number = parseInt(e.target.value || 0, 10); |
|||
if (Number.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; |
|||
if (onChange) { |
|||
onChange(Object.assign({}, this.state, changedValue)); |
|||
} |
|||
}; |
|||
|
|||
render() { |
|||
const { size } = this.props; |
|||
const { state } = this; |
|||
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}> |
|||
<Form.Item label="Price"> |
|||
{getFieldDecorator('price', { |
|||
initialValue: { number: 0, currency: 'rmb' }, |
|||
rules: [{ validator: this.checkPrice }], |
|||
})(<PriceInput />)} |
|||
</Form.Item> |
|||
<Form.Item> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const WrappedDemo = Form.create({ name: 'customized_form_controls' })(Demo); |
|||
|
|||
ReactDOM.render(<WrappedDemo />, mountNode); |
|||
``` |
@ -1,144 +0,0 @@ |
|||
--- |
|||
order: 5 |
|||
title: |
|||
zh-CN: 动态增减表单项 |
|||
en-US: Dynamic Form Item |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
动态增加、减少表单项。 |
|||
|
|||
## en-US |
|||
|
|||
Add or remove form items dynamically. |
|||
|
|||
```jsx |
|||
import { Form, Input, Icon, Button } from 'antd'; |
|||
|
|||
let id = 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(id++); |
|||
// 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) { |
|||
const { keys, names } = values; |
|||
console.log('Received values of form: ', values); |
|||
console.log('Merged values:', keys.map(key => names[key])); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
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) => ( |
|||
<Form.Item |
|||
{...(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" |
|||
onClick={() => this.remove(k)} |
|||
/> |
|||
) : null} |
|||
</Form.Item> |
|||
)); |
|||
return ( |
|||
<Form onSubmit={this.handleSubmit}> |
|||
{formItems} |
|||
<Form.Item {...formItemLayoutWithOutLabel}> |
|||
<Button type="dashed" onClick={this.add} style={{ width: '60%' }}> |
|||
<Icon type="plus" /> Add field |
|||
</Button> |
|||
</Form.Item> |
|||
<Form.Item {...formItemLayoutWithOutLabel}> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const WrappedDynamicFieldSet = Form.create({ name: 'dynamic_form_item' })(DynamicFieldSet); |
|||
ReactDOM.render(<WrappedDynamicFieldSet />, mountNode); |
|||
``` |
|||
|
|||
```css |
|||
.dynamic-delete-button { |
|||
cursor: pointer; |
|||
position: relative; |
|||
top: 4px; |
|||
font-size: 24px; |
|||
color: #999; |
|||
transition: all 0.3s; |
|||
} |
|||
.dynamic-delete-button:hover { |
|||
color: #777; |
|||
} |
|||
.dynamic-delete-button[disabled] { |
|||
cursor: not-allowed; |
|||
opacity: 0.5; |
|||
} |
|||
``` |
@ -1,92 +0,0 @@ |
|||
--- |
|||
order: 13 |
|||
title: |
|||
zh-CN: 动态校验规则 |
|||
en-US: Dynamic Rules |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
根据不同情况执行不同的校验规则。 |
|||
|
|||
## en-US |
|||
|
|||
Perform different check rules according to different situations. |
|||
|
|||
```jsx |
|||
import { Form, Input, Button, Checkbox } from 'antd'; |
|||
|
|||
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> |
|||
<Form.Item {...formItemLayout} label="Name"> |
|||
{getFieldDecorator('username', { |
|||
rules: [ |
|||
{ |
|||
required: true, |
|||
message: 'Please input your name', |
|||
}, |
|||
], |
|||
})(<Input placeholder="Please input your name" />)} |
|||
</Form.Item> |
|||
<Form.Item {...formItemLayout} label="Nickname"> |
|||
{getFieldDecorator('nickname', { |
|||
rules: [ |
|||
{ |
|||
required: this.state.checkNick, |
|||
message: 'Please input your nickname', |
|||
}, |
|||
], |
|||
})(<Input placeholder="Please input your nickname" />)} |
|||
</Form.Item> |
|||
<Form.Item {...formTailLayout}> |
|||
<Checkbox checked={this.state.checkNick} onChange={this.handleChange}> |
|||
Nickname is required |
|||
</Checkbox> |
|||
</Form.Item> |
|||
<Form.Item {...formTailLayout}> |
|||
<Button type="primary" onClick={this.check}> |
|||
Check |
|||
</Button> |
|||
</Form.Item> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const WrappedDynamicRule = Form.create({ name: 'dynamic_rule' })(DynamicRule); |
|||
ReactDOM.render(<WrappedDynamicRule />, mountNode); |
|||
``` |
@ -1,113 +0,0 @@ |
|||
--- |
|||
order: 4 |
|||
title: |
|||
zh-CN: 弹出层中的新建表单 |
|||
en-US: Form in Modal to Create |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
当用户访问一个展示了某个列表的页面,想新建一项但又不想跳转页面时,可以用 Modal 弹出一个表单,用户填写必要信息后创建新的项。 |
|||
|
|||
## en-US |
|||
|
|||
When user visit a page with a list of items, and want to create a new item. The page can popup a form in Modal, then let user fill in the form to create an item. |
|||
|
|||
```jsx |
|||
import { Button, Modal, Form, Input, Radio } from 'antd'; |
|||
|
|||
const CollectionCreateForm = Form.create({ name: 'form_in_modal' })( |
|||
// eslint-disable-next-line |
|||
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"> |
|||
<Form.Item label="Title"> |
|||
{getFieldDecorator('title', { |
|||
rules: [{ required: true, message: 'Please input the title of collection!' }], |
|||
})(<Input />)} |
|||
</Form.Item> |
|||
<Form.Item label="Description"> |
|||
{getFieldDecorator('description')(<Input type="textarea" />)} |
|||
</Form.Item> |
|||
<Form.Item 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>, |
|||
)} |
|||
</Form.Item> |
|||
</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.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); |
|||
``` |
|||
|
|||
```css |
|||
.collection-create-form_last-form-item { |
|||
margin-bottom: 0; |
|||
} |
|||
``` |
@ -1,89 +0,0 @@ |
|||
--- |
|||
order: 8 |
|||
title: |
|||
zh-CN: 表单数据存储于上层组件 |
|||
en-US: Store Form Data into Upper Component |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
通过使用 `onFieldsChange` 与 `mapPropsToFields`,可以把表单的数据存储到上层组件或者 [Redux](https://github.com/reactjs/redux)、[dva](https://github.com/dvajs/dva) 中,更多可参考 [rc-form 示例](http://react-component.github.io/form/examples/redux.html)。 |
|||
|
|||
**注意:**`mapPropsToFields` 里面返回的表单域数据必须使用 `Form.createFormField` 包装。 |
|||
|
|||
## en-US |
|||
|
|||
We can store form data into upper component or [Redux](https://github.com/reactjs/redux) or [dva](https://github.com/dvajs/dva) by using `onFieldsChange` and `mapPropsToFields`, see more at this [rc-form demo](http://react-component.github.io/form/examples/redux.html). |
|||
|
|||
**Note:** You must wrap field data with `Form.createFormField` in `mapPropsToFields`. |
|||
|
|||
**Note:** Here, errors are passed to higher order component in `onFieldsChange` and passed back in `mapPropsToFields`. |
|||
|
|||
```jsx |
|||
import { Form, Input } from 'antd'; |
|||
|
|||
const CustomizedForm = Form.create({ |
|||
name: 'global_state', |
|||
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"> |
|||
<Form.Item label="Username"> |
|||
{getFieldDecorator('username', { |
|||
rules: [{ required: true, message: 'Username is required!' }], |
|||
})(<Input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}); |
|||
|
|||
class Demo extends React.Component { |
|||
state = { |
|||
fields: { |
|||
username: { |
|||
value: 'benjycui', |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
handleFormChange = changedFields => { |
|||
this.setState(({ fields }) => ({ |
|||
fields: { ...fields, ...changedFields }, |
|||
})); |
|||
}; |
|||
|
|||
render() { |
|||
const { fields } = this.state; |
|||
return ( |
|||
<div> |
|||
<CustomizedForm {...fields} onChange={this.handleFormChange} /> |
|||
<pre className="language-bash">{JSON.stringify(fields, null, 2)}</pre> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
ReactDOM.render(<Demo />, mountNode); |
|||
``` |
|||
|
|||
<style> |
|||
#components-form-demo-global-state .language-bash { |
|||
max-width: 400px; |
|||
border-radius: 6px; |
|||
margin-top: 24px; |
|||
} |
|||
</style> |
@ -1,80 +0,0 @@ |
|||
--- |
|||
order: 0 |
|||
title: |
|||
zh-CN: 水平登录栏 |
|||
en-US: Horizontal Login Form |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
水平登录栏,常用在顶部导航栏中。 |
|||
|
|||
## en-US |
|||
|
|||
Horizontal login form is often used in navigation bar. |
|||
|
|||
```jsx |
|||
import { Form, Icon, Input, Button } from 'antd'; |
|||
|
|||
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}> |
|||
<Form.Item validateStatus={usernameError ? 'error' : ''} help={usernameError || ''}> |
|||
{getFieldDecorator('username', { |
|||
rules: [{ required: true, message: 'Please input your username!' }], |
|||
})( |
|||
<Input |
|||
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} |
|||
placeholder="Username" |
|||
/>, |
|||
)} |
|||
</Form.Item> |
|||
<Form.Item validateStatus={passwordError ? 'error' : ''} help={passwordError || ''}> |
|||
{getFieldDecorator('password', { |
|||
rules: [{ required: true, message: 'Please input your Password!' }], |
|||
})( |
|||
<Input |
|||
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} |
|||
type="password" |
|||
placeholder="Password" |
|||
/>, |
|||
)} |
|||
</Form.Item> |
|||
<Form.Item> |
|||
<Button type="primary" htmlType="submit" disabled={hasErrors(getFieldsError())}> |
|||
Log in |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const WrappedHorizontalLoginForm = Form.create({ name: 'horizontal_login' })(HorizontalLoginForm); |
|||
|
|||
ReactDOM.render(<WrappedHorizontalLoginForm />, mountNode); |
|||
``` |
@ -1,72 +0,0 @@ |
|||
--- |
|||
order: 12 |
|||
title: |
|||
zh-CN: 表单布局 |
|||
en-US: Form Layout |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
表单有三种布局。 |
|||
|
|||
## en-US |
|||
|
|||
There are three layout for form: `horizontal`, `vertical`, `inline`. |
|||
|
|||
```jsx |
|||
import { Form, Input, Button, Radio } from 'antd'; |
|||
|
|||
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}> |
|||
<Form.Item 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> |
|||
</Form.Item> |
|||
<Form.Item label="Field A" {...formItemLayout}> |
|||
<Input placeholder="input placeholder" /> |
|||
</Form.Item> |
|||
<Form.Item label="Field B" {...formItemLayout}> |
|||
<Input placeholder="input placeholder" /> |
|||
</Form.Item> |
|||
<Form.Item {...buttonItemLayout}> |
|||
<Button type="primary">Submit</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
ReactDOM.render(<FormLayoutDemo />, mountNode); |
|||
``` |
@ -1,87 +0,0 @@ |
|||
--- |
|||
order: 1 |
|||
title: |
|||
zh-CN: 登录框 |
|||
en-US: Login Form |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
普通的登录框,可以容纳更多的元素。 |
|||
|
|||
## en-US |
|||
|
|||
Normal login form which can contain more elements. |
|||
|
|||
```jsx |
|||
import { Form, Icon, Input, Button, Checkbox } from 'antd'; |
|||
|
|||
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"> |
|||
<Form.Item> |
|||
{getFieldDecorator('username', { |
|||
rules: [{ required: true, message: 'Please input your username!' }], |
|||
})( |
|||
<Input |
|||
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} |
|||
placeholder="Username" |
|||
/>, |
|||
)} |
|||
</Form.Item> |
|||
<Form.Item> |
|||
{getFieldDecorator('password', { |
|||
rules: [{ required: true, message: 'Please input your Password!' }], |
|||
})( |
|||
<Input |
|||
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} |
|||
type="password" |
|||
placeholder="Password" |
|||
/>, |
|||
)} |
|||
</Form.Item> |
|||
<Form.Item> |
|||
{getFieldDecorator('remember', { |
|||
valuePropName: 'checked', |
|||
initialValue: true, |
|||
})(<Checkbox>Remember me</Checkbox>)} |
|||
<a className="login-form-forgot" href=""> |
|||
Forgot password |
|||
</a> |
|||
<Button type="primary" htmlType="submit" className="login-form-button"> |
|||
Log in |
|||
</Button> |
|||
Or <a href="">register now!</a> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const WrappedNormalLoginForm = Form.create({ name: 'normal_login' })(NormalLoginForm); |
|||
|
|||
ReactDOM.render(<WrappedNormalLoginForm />, mountNode); |
|||
``` |
|||
|
|||
```css |
|||
#components-form-demo-normal-login .login-form { |
|||
max-width: 300px; |
|||
} |
|||
#components-form-demo-normal-login .login-form-forgot { |
|||
float: right; |
|||
} |
|||
#components-form-demo-normal-login .login-form-button { |
|||
width: 100%; |
|||
} |
|||
``` |
@ -1,271 +0,0 @@ |
|||
--- |
|||
order: 2 |
|||
title: |
|||
zh-CN: 注册新用户 |
|||
en-US: Registration |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
用户填写必须的信息以注册新用户。 |
|||
|
|||
## en-US |
|||
|
|||
Fill in this form to create a new account for you. |
|||
|
|||
```jsx |
|||
import { |
|||
Form, |
|||
Input, |
|||
Tooltip, |
|||
Icon, |
|||
Cascader, |
|||
Select, |
|||
Row, |
|||
Col, |
|||
Checkbox, |
|||
Button, |
|||
AutoComplete, |
|||
} from 'antd'; |
|||
|
|||
const { Option } = Select; |
|||
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, |
|||
autoCompleteResult: [], |
|||
}; |
|||
|
|||
handleSubmit = e => { |
|||
e.preventDefault(); |
|||
this.props.form.validateFieldsAndScroll((err, values) => { |
|||
if (!err) { |
|||
console.log('Received values of form: ', values); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
handleConfirmBlur = e => { |
|||
const { value } = e.target; |
|||
this.setState({ confirmDirty: this.state.confirmDirty || !!value }); |
|||
}; |
|||
|
|||
compareToFirstPassword = (rule, value, callback) => { |
|||
const { form } = this.props; |
|||
if (value && value !== form.getFieldValue('password')) { |
|||
callback('Two passwords that you enter is inconsistent!'); |
|||
} else { |
|||
callback(); |
|||
} |
|||
}; |
|||
|
|||
validateToNextPassword = (rule, value, callback) => { |
|||
const { form } = this.props; |
|||
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 }); |
|||
}; |
|||
|
|||
render() { |
|||
const { getFieldDecorator } = this.props.form; |
|||
const { autoCompleteResult } = 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 prefixSelector = getFieldDecorator('prefix', { |
|||
initialValue: '86', |
|||
})( |
|||
<Select style={{ width: 70 }}> |
|||
<Option value="86">+86</Option> |
|||
<Option value="87">+87</Option> |
|||
</Select>, |
|||
); |
|||
|
|||
const websiteOptions = autoCompleteResult.map(website => ( |
|||
<AutoCompleteOption key={website}>{website}</AutoCompleteOption> |
|||
)); |
|||
|
|||
return ( |
|||
<Form {...formItemLayout} onSubmit={this.handleSubmit}> |
|||
<Form.Item 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 />)} |
|||
</Form.Item> |
|||
<Form.Item label="Password" hasFeedback> |
|||
{getFieldDecorator('password', { |
|||
rules: [ |
|||
{ |
|||
required: true, |
|||
message: 'Please input your password!', |
|||
}, |
|||
{ |
|||
validator: this.validateToNextPassword, |
|||
}, |
|||
], |
|||
})(<Input.Password />)} |
|||
</Form.Item> |
|||
<Form.Item label="Confirm Password" hasFeedback> |
|||
{getFieldDecorator('confirm', { |
|||
rules: [ |
|||
{ |
|||
required: true, |
|||
message: 'Please confirm your password!', |
|||
}, |
|||
{ |
|||
validator: this.compareToFirstPassword, |
|||
}, |
|||
], |
|||
})(<Input.Password onBlur={this.handleConfirmBlur} />)} |
|||
</Form.Item> |
|||
<Form.Item |
|||
label={ |
|||
<span> |
|||
Nickname |
|||
<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 />)} |
|||
</Form.Item> |
|||
<Form.Item label="Habitual Residence"> |
|||
{getFieldDecorator('residence', { |
|||
initialValue: ['zhejiang', 'hangzhou', 'xihu'], |
|||
rules: [ |
|||
{ type: 'array', required: true, message: 'Please select your habitual residence!' }, |
|||
], |
|||
})(<Cascader options={residences} />)} |
|||
</Form.Item> |
|||
<Form.Item label="Phone Number"> |
|||
{getFieldDecorator('phone', { |
|||
rules: [{ required: true, message: 'Please input your phone number!' }], |
|||
})(<Input addonBefore={prefixSelector} style={{ width: '100%' }} />)} |
|||
</Form.Item> |
|||
<Form.Item label="Website"> |
|||
{getFieldDecorator('website', { |
|||
rules: [{ required: true, message: 'Please input website!' }], |
|||
})( |
|||
<AutoComplete |
|||
dataSource={websiteOptions} |
|||
onChange={this.handleWebsiteChange} |
|||
placeholder="website" |
|||
> |
|||
<Input /> |
|||
</AutoComplete>, |
|||
)} |
|||
</Form.Item> |
|||
<Form.Item label="Captcha" 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 />)} |
|||
</Col> |
|||
<Col span={12}> |
|||
<Button>Get captcha</Button> |
|||
</Col> |
|||
</Row> |
|||
</Form.Item> |
|||
<Form.Item {...tailFormItemLayout}> |
|||
{getFieldDecorator('agreement', { |
|||
valuePropName: 'checked', |
|||
})( |
|||
<Checkbox> |
|||
I have read the <a href="">agreement</a> |
|||
</Checkbox>, |
|||
)} |
|||
</Form.Item> |
|||
<Form.Item {...tailFormItemLayout}> |
|||
<Button type="primary" htmlType="submit"> |
|||
Register |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const WrappedRegistrationForm = Form.create({ name: 'register' })(RegistrationForm); |
|||
|
|||
ReactDOM.render(<WrappedRegistrationForm />, mountNode); |
|||
``` |
@ -1,158 +0,0 @@ |
|||
--- |
|||
order: 99 |
|||
title: |
|||
zh-CN: 提交修改前看看这个对不对 |
|||
en-US: Please check this before commit |
|||
debug: true |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
提交修改前看看这个对不对。 |
|||
|
|||
## en-US |
|||
|
|||
Please check this before commit. |
|||
|
|||
```jsx |
|||
import { Button, Modal, Form, Row, Col, Input, Select, InputNumber, Radio, DatePicker } from 'antd'; |
|||
|
|||
const ColSpan = { lg: 12, md: 24 }; |
|||
|
|||
class App extends React.Component { |
|||
constructor() { |
|||
super(); |
|||
this.state = { |
|||
visible: false, |
|||
}; |
|||
} |
|||
|
|||
handleClick = () => { |
|||
this.setState({ |
|||
visible: true, |
|||
}); |
|||
}; |
|||
|
|||
handleCancel = () => { |
|||
this.setState({ |
|||
visible: false, |
|||
}); |
|||
}; |
|||
|
|||
handleSubmit = e => { |
|||
e.preventDefault(); |
|||
const { form } = this.props; |
|||
form.validateFields((error, values) => { |
|||
console.log(error, values); |
|||
}); |
|||
}; |
|||
|
|||
render() { |
|||
const { |
|||
form: { getFieldDecorator }, |
|||
} = this.props; |
|||
const { Item } = Form; |
|||
const itemLayout = { |
|||
labelCol: { span: 6 }, |
|||
wrapperCol: { span: 16 }, |
|||
}; |
|||
const span = 12; |
|||
|
|||
return ( |
|||
<div> |
|||
{/* Case 1: Form in modal */} |
|||
<Button onClick={this.handleClick}>打开</Button> |
|||
<Modal |
|||
onOk={this.handleSubmit} |
|||
onCancel={this.handleCancel} |
|||
title="弹出层" |
|||
visible={this.state.visible} |
|||
> |
|||
<Form layout="horizontal" onSubmit={this.handleSubmit}> |
|||
<Row> |
|||
<Col span={span}> |
|||
<Item colon={false} {...itemLayout} label="测试字段"> |
|||
{getFieldDecorator('item1', { |
|||
rules: [{ required: true, message: '请必须填写此字段' }], |
|||
})(<Input />)} |
|||
</Item> |
|||
</Col> |
|||
<Col span={span}> |
|||
<Item {...itemLayout} label="测试字段"> |
|||
{getFieldDecorator('item2', { |
|||
rules: [{ required: true, message: '请必须填写此字段' }], |
|||
})(<Input />)} |
|||
</Item> |
|||
</Col> |
|||
<Col span={span}> |
|||
<Item {...itemLayout} label="测试字段"> |
|||
{getFieldDecorator('item3')(<Input />)} |
|||
</Item> |
|||
</Col> |
|||
<Col span={span}> |
|||
<Item {...itemLayout} label="测试字段"> |
|||
{getFieldDecorator('item4', { |
|||
rules: [{ required: true, message: '请必须填写此字段' }], |
|||
})(<Input />)} |
|||
</Item> |
|||
</Col> |
|||
<Col span={span}> |
|||
<Item {...itemLayout} label="测试字段"> |
|||
{getFieldDecorator('item5', { |
|||
rules: [{ required: true, message: '请必须填写此字段' }], |
|||
})(<Input />)} |
|||
</Item> |
|||
</Col> |
|||
<Col span={span}> |
|||
<Item {...itemLayout} label="测试字段"> |
|||
{getFieldDecorator('item6', { |
|||
rules: [{ required: true, message: '请必须填写此字段' }], |
|||
})(<Input />)} |
|||
</Item> |
|||
</Col> |
|||
</Row> |
|||
</Form> |
|||
</Modal> |
|||
|
|||
{/* case 2: Form different item */} |
|||
<Form> |
|||
<Row gutter={16}> |
|||
<Col {...ColSpan}> |
|||
<Item colon={false} label="input:64.5px"> |
|||
<Input /> |
|||
</Item> |
|||
</Col> |
|||
<Col {...ColSpan}> |
|||
<Item label="select:64px"> |
|||
<Select /> |
|||
</Item> |
|||
</Col> |
|||
<Col {...ColSpan}> |
|||
<Item label="InputNumber:64px"> |
|||
<InputNumber /> |
|||
</Item> |
|||
</Col> |
|||
<Col {...ColSpan}> |
|||
<Item label="DatePicker: 64.5px"> |
|||
<DatePicker /> |
|||
</Item> |
|||
</Col> |
|||
<Col {...ColSpan}> |
|||
<Item label="RadioGroup: 64px"> |
|||
<Radio.Group> |
|||
<Radio value={0}>男</Radio> |
|||
<Radio value={1}>女</Radio> |
|||
</Radio.Group> |
|||
</Item> |
|||
</Col> |
|||
</Row> |
|||
</Form> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const WrapApp = Form.create()(App); |
|||
|
|||
ReactDOM.render(<WrapApp />, mountNode); |
|||
``` |
@ -1,109 +0,0 @@ |
|||
--- |
|||
order: 6 |
|||
title: |
|||
zh-CN: 时间类控件 |
|||
en-US: Time-related Controls |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
时间类组件的 `value` 类型为 `moment` 对象,所以在提交服务器前需要预处理。 |
|||
|
|||
## en-US |
|||
|
|||
The `value` of time-related components is a `moment` object, which we need to pre-process it before we submit to server. |
|||
|
|||
```jsx |
|||
import { Form, DatePicker, TimePicker, Button } from 'antd'; |
|||
|
|||
const { MonthPicker, RangePicker } = DatePicker; |
|||
|
|||
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 {...formItemLayout} onSubmit={this.handleSubmit}> |
|||
<Form.Item label="DatePicker"> |
|||
{getFieldDecorator('date-picker', config)(<DatePicker />)} |
|||
</Form.Item> |
|||
<Form.Item label="DatePicker[showTime]"> |
|||
{getFieldDecorator('date-time-picker', config)( |
|||
<DatePicker showTime format="YYYY-MM-DD HH:mm:ss" />, |
|||
)} |
|||
</Form.Item> |
|||
<Form.Item label="MonthPicker"> |
|||
{getFieldDecorator('month-picker', config)(<MonthPicker />)} |
|||
</Form.Item> |
|||
<Form.Item label="RangePicker"> |
|||
{getFieldDecorator('range-picker', rangeConfig)(<RangePicker />)} |
|||
</Form.Item> |
|||
<Form.Item label="RangePicker[showTime]"> |
|||
{getFieldDecorator('range-time-picker', rangeConfig)( |
|||
<RangePicker showTime format="YYYY-MM-DD HH:mm:ss" />, |
|||
)} |
|||
</Form.Item> |
|||
<Form.Item label="TimePicker"> |
|||
{getFieldDecorator('time-picker', config)(<TimePicker />)} |
|||
</Form.Item> |
|||
<Form.Item |
|||
wrapperCol={{ |
|||
xs: { span: 24, offset: 0 }, |
|||
sm: { span: 16, offset: 8 }, |
|||
}} |
|||
> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const WrappedTimeRelatedForm = Form.create({ name: 'time_related_controls' })(TimeRelatedForm); |
|||
|
|||
ReactDOM.render(<WrappedTimeRelatedForm />, mountNode); |
|||
``` |
@ -1,217 +0,0 @@ |
|||
--- |
|||
order: 14 |
|||
title: |
|||
zh-CN: 校验其他组件 |
|||
en-US: Other Form Controls |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
以上演示没有出现的表单控件对应的校验演示。 |
|||
|
|||
## en-US |
|||
|
|||
Demonstration of validation configuration for form controls which are not shown in the demos above. |
|||
|
|||
```jsx |
|||
import { |
|||
Form, |
|||
Select, |
|||
InputNumber, |
|||
Switch, |
|||
Radio, |
|||
Slider, |
|||
Button, |
|||
Upload, |
|||
Icon, |
|||
Rate, |
|||
Checkbox, |
|||
Row, |
|||
Col, |
|||
} from 'antd'; |
|||
|
|||
const { Option } = Select; |
|||
|
|||
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 {...formItemLayout} onSubmit={this.handleSubmit}> |
|||
<Form.Item label="Plain Text"> |
|||
<span className="ant-form-text">China</span> |
|||
</Form.Item> |
|||
<Form.Item 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="usa">U.S.A</Option> |
|||
</Select>, |
|||
)} |
|||
</Form.Item> |
|||
|
|||
<Form.Item 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>, |
|||
)} |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="InputNumber"> |
|||
{getFieldDecorator('input-number', { initialValue: 3 })(<InputNumber min={1} max={10} />)} |
|||
<span className="ant-form-text"> machines</span> |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="Switch"> |
|||
{getFieldDecorator('switch', { valuePropName: 'checked' })(<Switch />)} |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="Slider"> |
|||
{getFieldDecorator('slider')( |
|||
<Slider |
|||
marks={{ |
|||
0: 'A', |
|||
20: 'B', |
|||
40: 'C', |
|||
60: 'D', |
|||
80: 'E', |
|||
100: 'F', |
|||
}} |
|||
/>, |
|||
)} |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="Radio.Group"> |
|||
{getFieldDecorator('radio-group')( |
|||
<Radio.Group> |
|||
<Radio value="a">item 1</Radio> |
|||
<Radio value="b">item 2</Radio> |
|||
<Radio value="c">item 3</Radio> |
|||
</Radio.Group>, |
|||
)} |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="Radio.Button"> |
|||
{getFieldDecorator('radio-button')( |
|||
<Radio.Group> |
|||
<Radio.Button value="a">item 1</Radio.Button> |
|||
<Radio.Button value="b">item 2</Radio.Button> |
|||
<Radio.Button value="c">item 3</Radio.Button> |
|||
</Radio.Group>, |
|||
)} |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="Checkbox.Group"> |
|||
{getFieldDecorator('checkbox-group', { |
|||
initialValue: ['A', 'B'], |
|||
})( |
|||
<Checkbox.Group style={{ width: '100%' }}> |
|||
<Row> |
|||
<Col span={8}> |
|||
<Checkbox value="A">A</Checkbox> |
|||
</Col> |
|||
<Col span={8}> |
|||
<Checkbox disabled value="B"> |
|||
B |
|||
</Checkbox> |
|||
</Col> |
|||
<Col span={8}> |
|||
<Checkbox value="C">C</Checkbox> |
|||
</Col> |
|||
<Col span={8}> |
|||
<Checkbox value="D">D</Checkbox> |
|||
</Col> |
|||
<Col span={8}> |
|||
<Checkbox value="E">E</Checkbox> |
|||
</Col> |
|||
</Row> |
|||
</Checkbox.Group>, |
|||
)} |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="Rate"> |
|||
{getFieldDecorator('rate', { |
|||
initialValue: 3.5, |
|||
})(<Rate />)} |
|||
</Form.Item> |
|||
|
|||
<Form.Item 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>, |
|||
)} |
|||
</Form.Item> |
|||
|
|||
<Form.Item 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> |
|||
</Form.Item> |
|||
|
|||
<Form.Item wrapperCol={{ span: 12, offset: 6 }}> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const WrappedDemo = Form.create({ name: 'validate_other' })(Demo); |
|||
|
|||
ReactDOM.render(<WrappedDemo />, mountNode); |
|||
``` |
|||
|
|||
```css |
|||
#components-form-demo-validate-other .dropbox { |
|||
height: 180px; |
|||
line-height: 1.5; |
|||
} |
|||
``` |
@ -1,72 +0,0 @@ |
|||
--- |
|||
order: 9 |
|||
title: |
|||
zh-CN: 自行处理表单数据 |
|||
en-US: Handle Form Data Manually |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
使用 `Form.create` 处理后的表单具有自动收集数据并校验的功能,但如果您不需要这个功能,或者默认的行为无法满足业务需求,可以选择不使用 `Form.create` 并自行处理数据。 |
|||
|
|||
## en-US |
|||
|
|||
`Form.create` will collect and validate form data automatically. But if you don't need this feature or the default behaviour cannot satisfy your business, you can drop `Form.create` and handle form data manually. |
|||
|
|||
```jsx |
|||
import { Form, InputNumber } from 'antd'; |
|||
|
|||
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; |
|||
const tips = |
|||
'A prime is a natural number greater than 1 that has no positive divisors other than 1 and itself.'; |
|||
return ( |
|||
<Form> |
|||
<Form.Item |
|||
{...formItemLayout} |
|||
label="Prime between 8 & 12" |
|||
validateStatus={number.validateStatus} |
|||
help={number.errorMsg || tips} |
|||
> |
|||
<InputNumber min={8} max={12} value={number.value} onChange={this.handleNumberChange} /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
} |
|||
} |
|||
|
|||
ReactDOM.render(<RawForm />, mountNode); |
|||
``` |
@ -1,98 +0,0 @@ |
|||
# 表单实现原理 |
|||
|
|||
--- |
|||
|
|||
antd 中的 [Form](https://github.com/ant-design/ant-design/blob/master/components/form/index.zh-CN.md) 组件基于 [rc-form](https://github.com/react-component/form) 实现。本文第一部分将介绍 rc-form 库;第二部分再介绍 antd 中的 Form 组件;第三部分将结合表单组件的使用,回顾前两部分的内容。 |
|||
|
|||
--- |
|||
|
|||
### rc-form |
|||
|
|||
常规收集表单数据并作校验,只需以 store 实时记录表单数据,校验后重绘表单。这样的思路以业务代码为例,就是,以数据模型 model 集成数据处理操作,再通过 setState 将 model 中的实时数据注入组件中,并驱动组件重绘(除了 setState 方法以外,也可以使用 forceUpdate 方法重绘组件,并在 render 阶段重新访问 model 中的实时数据)。 |
|||
|
|||
#### FieldsStore |
|||
|
|||
在 rc-form 中,上述数据模型的具体实现为 FieldsStore 类。如前所述,FieldsStore 实例与视图层的交互逻辑为,在用户行为驱动字段变更时,即时存储字段的值及校验信息(本文以校验信息代指校验文案和校验状态),继而调用表单组件实例的 forceUpdate 方法强制重绘;在绘制过程中,再从 FieldsStore 实例读取实时数据和校验信息。 |
|||
|
|||
建模方面,FieldsStore 实例以 fields 属性存储表单的实时数据,即由用户行为或开发者显式更新的数据。本文把实时数据已存入 fields 中的字段称为已收集字段;反之,称为未收集字段。以下是 fields\[name\] 中成员属性的意义(name 为字段名,下同)。 |
|||
|
|||
- value 字段的值。 |
|||
- errors 校验文案,数组形式。 |
|||
- validating 校验状态。 |
|||
- dirty 脏值标识。真值时意味着字段的值已作变更,但未作校验。 |
|||
- touched 用户行为标识。通常情况下,真值时意味着用户行为已促使字段的值发生了变更。 |
|||
|
|||
FieldsStore 实例又以 fieldsMeta 属性存储字段的元数据。元数据作为配置项,通常是指定后不再变更的数据,用于操控表单数据转换等行为。元数据通过下文中 getFieldProps, getFieldDecorator 方法的次参 fieldOption 配置。以下是 fieldsMeta\[name\] 中部分成员属性的意义(如不作特殊说明,该成员属性即 fieldOption 中的同名属性)。 |
|||
|
|||
- validate 校验规则和触发事件,用于约定在何种事件下以何种方式校验字段的值,\[{ rules, trigger }\] 形式。通过 fieldOption.validate, fieldOption.rules, fieldOption.validateTrigger 配置。 |
|||
- hidden 设置为 true 时,validateFields 将不会校验该字段,需要开发者手动校验,并且,getFieldsValue, getFieldsError 等方法也将无法获取该字段的数据及校验信息等实时数据。适用场景如勾选协议复选框,当其已勾选时,提交按钮才可点击,该复选框的值不会出现在全量表单数据中。本文假定配置了 hidden 为 true 的字段为虚拟隐藏项。 |
|||
- getValueFromEvent(event) 用于从 event 对象中获取字段的值,适用场景如处理 input, radio 等原生组件。特别的,当字段组件输出的首参不是 event 对象时,getValueFromEvent 将对首参进行数据转化,以满足特定的场景,比如由开发者指定具体首参内容的自定义字段组件。 |
|||
- initialValue 字段的初始值。当字段的值未作收集时,初始值用于作为字段的值。 |
|||
- valuePropName 约定字段的值以何种 props 属性注入字段组件中,适用场景如将字段的值以 props.checked 属性注入 radio 组件中。注入字段组件的值数据 props 由 getFieldValuePropValue 方法生成,下同。 |
|||
- getValueProps(value) 用于转化字段的值,输出 props 以注入字段组件中。适用场景如当 get, post 请求的数据结构不同时,且字段组件以 post 请求的数据结构输出字段的值、以 get 请求的数据结构接受字段的值,那就可以使用 getValueProps 将 post 请求的数据结构转换为 get 请求的数据结构。 |
|||
- normalize(newValue, oldValue, values) 用于转化存入 FieldsStore 实例的字段的值。使用案例如[全选按钮](https://codepen.io/afc163/pen/JJVXzG?editors=001)。 |
|||
|
|||
在此基础上,FieldsStore 提供了一些便捷的访问器操作。需要说明的是,rc-form 借助 [lodash](https://github.com/lodash/lodash),支持以嵌套结构定义字段名,即使用 '.', '\[|\]' 作为分割符,如 'a.b' 意味着 a 对象下的 b 属性;'c\[0\]' 意味着 c 数组的首项。本文约定匹配字段指,所有以指定字符串起始或等值的字段列表。为此,FieldsStore 提供 isValidNestedFieldName 用于校验字段名不能作为表单中另一个字段名的成员;flattenRegisteredFields 用于传参数据作扁平化处理;getValidFieldsFullName 用于获取匹配字段列表,但不包含虚拟隐藏项。除此以外,FieldsStore 提供了对实时数据和元数据的读取操作,特别的,部分 api 可用于获取匹配字段的实时数据,参见[文档](https://ant.design/components/form-cn/)。 |
|||
|
|||
#### BaseForm |
|||
|
|||
与业务 model 不同的是,FieldsStore 仅作为表单实时数据和元数据的存储器,校验数据等方法由 BaseForm 提供。BaseForm 既作为 HOC 容器,能为开发者自定义表单组件(下文用自定义表单替代)注入表单操作函数集,通常是 props.form;又用于装饰字段组件或其 props,以收集字段的元数据、通过绑定函数收集或校验字段的实时数据。因此,可以部分认为,BaseForm 即 FieldsStore 和视图层桥接的控制器。其机制为: |
|||
|
|||
首先,通过 createBaseForm(option, mixins) 创建装饰函数。装饰函数可以为自定义表单包裹上 BaseForm 容器。参数 option 用于配置表单层面的绑定函数、校验文案以及部分字段组件的 props 属性名;mixins 将作为实例方法混入 BaseForm,特别的,createDOMForm 函数即通过这一机制混入了 validateFieldsAndScroll 方法。 |
|||
|
|||
其次,在 BaseForm 的 getInitialState 阶段,将创建 FieldsStore 实例,并初始化 clearedFieldMetaCache 等缓存。这些缓存的意义包含缓存字段组件实例,缓存内置的 ref 引用及绑定函数,缓存渲染状态等。特别的,clearedFieldMetaCache 用于在 ref 引用函数执行时缓存字段的实时数据和元数据,以便于在字段重绘过程中的第两次执行 ref 引用时,恢复 FieldsStore 存储的实时数据和元数据(参见[源码](https://github.com/react-component/form/blob/master/src/createBaseForm.js)中的 saveRef 方法)。不然,元数据的丢失将导致数据校验无法正常工作。 |
|||
|
|||
其次,执行 render 方法,将表单操作函数集通过 props 注入自定义表单。介于此,在自定义表单中,开发者可以获取到表单的实时数据,或者更新表单数据,或者校验表单,以完成特定的处理逻辑。 |
|||
|
|||
其次,由 BaseForm 提供的 getFieldProps 或 getFieldDecorator 实例方法完成字段组件的渲染。以下是 getFieldProps, getFieldDecorator 的意义。 |
|||
|
|||
- getFieldProps(name, fieldOption) 用于为 FieldsStore 实例收集字段的元数据,如经转化后的校验规则等;指定字段组件的 ref 引用函数;为字段组件绑定 onCollect, onCollectValidate 实例方法,以在指定事件触发时收集或校验字段的值;最终将输出注入字段组件的 props,包含全量的元数据和实时数据、以及字段的值。 |
|||
- getFieldDecorator(name, fieldOption) 基于 getFieldProps 方法,直接装饰字段组件,这样就可以操控添加在字段组件上的 ref 引用函数及 props.onChange 等绑定函数。 |
|||
|
|||
其次,当用户行为促使字段的值发生变更时,将执行 BaseForm 实例的 onCollect, onCollectValidate 方法,以收集或校验该字段的实时数据,并重绘表单。其中,rc-form 中的数据校验通过 [async-validate](https://github.com/yiminghe/async-validator) 库实现,具体实现为 BaseForm 实例的 validateFieldsInternal 方法。校验字段时,默认将沿用上一次的校验信息;当设置 force 为 true 时,将强制重新校验。 |
|||
|
|||
详细的工作流程参见下方的时序图: <img class="preview-img no-padding" src="http://xzfyu.com/2018/11/04/ant%20design/antd-Form%20%E7%BB%84%E4%BB%B6/rc-form%E6%97%B6%E5%BA%8F%E5%9B%BE.png"> |
|||
|
|||
### Form 表单 |
|||
|
|||
#### 表单 |
|||
|
|||
Form 组件本身并不承载逻辑,而是通过 props.className, props.prefixCls, props.layout, props.hideRequiredMark, props.onSubmit 设定注入 form 原生节点的样式类及绑定函数,以影响表单内部节点渲染时的样式。同时,Form 组件将为子组件传入 context.vertical 以区分是水平布局,还是垂直布局。 |
|||
|
|||
#### 表单域 |
|||
|
|||
FormItem 组件用于设定表单项的布局。如同受控组件和非受控组件,FormItem 组件提供两种使用方式:其一,当未设定校验信息相关的 props 属性时,FormItem 组件将自动根据内部字段组件实例的状况渲染校验文案及校验状态;其二,当设定校验信息相关的 props 属性时,FormItem 组件将根据开发者传入的 props 渲染校验文案及校验状态。在第一种使用方式下,FormItem 组件只可以包含一个字段组件;在第二种使用方式下,FormItem 组件中可以包含多个字段组件,布局也更为灵活。这里说的相关 props 属性包含:校验文案 help, 校验状态 validateStatus(用于绘制反馈图标), 必填标识 required, 字段名 id(影响点击 label 时聚焦哪个字段元素)。 |
|||
|
|||
那么,FormItem 又是怎样自动收集字段组件的校验数据呢?因为在 BaseForm 组件提供的 getFieldProp 方法,字段名、元数据和实时数据都将作为特殊的 props 属性传入到字段组件中,所以作为字段组件容器的 FormItem,就可以通过这些特殊的 props 属性判断子组件实例是不是一个字段组件实例。当其为字段组件实例时,进一步收集实时的校验信息,从校验规则中获取是否必填标识,以完成表单域的渲染。 |
|||
|
|||
此外,FormItem 可以使用 props.labelCol, props.wrapperCol 属性栅格化布局标签组件和字段组件,其实现借助于 antd 提供的 [Row, Col 组件](https://ant.design/components/grid-cn/)。当点击标签 label 时,FormItem 提供的绑定函数也能自动为字段组件获得焦点。这里不再多加介绍。 |
|||
|
|||
### 使用与回顾 |
|||
|
|||
#### 创建 HOC 容器 |
|||
|
|||
结合上文,antd 使用 Form.create(options)(CustomizedForm) 形式为用户自定义表单包裹上 BaseForm 高阶组件,以此植入 props.form 表单操作函数集。 |
|||
|
|||
高阶组件影响对自定义表单设置 ref 引用。默认可使用 BaseForm 实例的 refs.wrappedComponent 属性访问 CustomizedForm 实例,其次也可以通过 props.wrappedComponentRef 为 CustomizedForm 实例配置 ref 引用(参考案例 —— 弹出层中的新建表单)。当自定义表单可切换或者需要对外交互时,设置 ref 引用通常是不可避免的。 |
|||
|
|||
高阶组件影响 props 传递。antd 既支持使用 options.mapPropsToFields 将 BaseForm 实例获得的 props 转化成表单的实时数据(需要结合 Form.createFormField 方法),又支持在 CustomizedForm 实例中调用 props.form.setFields 方法更新实时数据。当 options.mapPropsToFields, options.onFieldsChange 方法结合使用时,可用于完成自定义表单和上层组件、或者 view 层和 store 层的交互(参考案例 —— 表单数据存储于上层组件)。 |
|||
|
|||
一般认为,对整张表单的控制,需要借助于 Form.create 方法的首参 options 配置实现。 |
|||
|
|||
#### 操作函数集 |
|||
|
|||
通常情况下,CustomizedForm 实例可通过 props.form 获取到表单操作函数集。当然,开发者也可以通过 options.formPropName 指定操作函数集的 props 属性名。 |
|||
|
|||
操作函数集包含 getFieldProps, getFieldDecorator, getFieldInstance, setFields, setFieldsValue, setFieldsInitialValue, resetFields, validateFields, getFieldsValue, getFieldValue, getFieldsError, getFieldError, isFieldsValidating, isFieldValidating, isFieldsTouched, isFieldTouched 方法,即用于装饰字段组件(或其 props)、获取字段组件实例、设置或获取实时数据、重置或校验字段。 |
|||
|
|||
如上文所说,getFieldProps, getFieldDecorator 方法即用于自动为字段组件绑定监听函数,这样就可以在指定事件触发时,收集和校验字段的值。同时,可以通过这两个方法将视图中未加显示的字段存入 FieldsStore 中(这时,可以将 FieldsStore 视为一个内置于表单组件的状态管理器)。比如在包含其他选项的单选框场景中,就可以使用 getFieldDecorator 创建虚拟字段,通过绑定函数将单选框和输入框的值赋值到该虚拟字段中,并使用该虚拟字段的校验信息绘制 FormItem。参考案例 —— 动态增减表单项。 |
|||
|
|||
上述单选框场景,也可以使用自定义字段组件 CustomizedField 实现。参考案例 —— 自定义字段组件。当使用自定义字段组件时,通过 getFieldInstance 获取 CustomizedField 的实例可能是必不可少的,这样可以把略显复杂的数据校验方法集成在 CustomizedField 中。此外,在字段组件中,既可以通过 props.value 属性获得字段的值,也可以通过 props\['data-**meta'\], props\['data-**field'\] 获得字段的全量元数据和实时数据。 |
|||
|
|||
在 CustomizedForm 中,使用 getFieldValue 可以取得字段的实时更新值,这样就能根据指定字段的值控制另一个字段的显隐。此外,通过在字段组件的 onChange 绑定函数中调用 setFieldValue,也能对另一个字段组件加以赋值,这样就可以实现表单的联动效果,参考案例 —— 表单联动。若在字段组件的 onChange 绑定函数中调用 validateFields 方法,就可以实现关联字段的校验,比如表单中存在设置最大最小值的两个输入框,参考案例 —— 动态校验规则。 |
|||
|
|||
使用 getFieldError, isFieldValidating 获取到的校验信息可用于直接绘制 FormItem,这样就能更加细微地操控 FormItem 下字段组件的布局,比如放置多个字段组件。当然,对于多个字段组件公用校验信息的场景,也可以使用包含多个字段的 CustomizedField 实现。isFieldTouched 可判断用户是否对字段组件有触发数据收集和校验的行为,参考案例 —— 水平登录栏。介于 BaseForm 默认在 onChange 事件中收集并校验字段的值,在这种情形下,也可以通过 isFieldTouched 判断字段的值是否已作更新,而不需要在 CustomizedForm 中设置特殊的更新标识。 |
|||
|
|||
#### 其他 |
|||
|
|||
当不使用 Form.create 为字段组件自动绑定校验方法时,可以使用 Form, FormItem 组件设定表单的布局、校验信息的绘制,参考案例 —— 表单布局、自行处理表单数据、自定义校验。 |
@ -1,254 +0,0 @@ |
|||
--- |
|||
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. |
|||
|
|||
## When to use |
|||
|
|||
- When you need to create a instance or collect information. |
|||
- When you need to validate fields in certain rules. |
|||
|
|||
## Form Component |
|||
|
|||
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 Item Component |
|||
|
|||
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 `<Form.Item />`. |
|||
|
|||
```jsx |
|||
<Form.Item {...props}>{children}</Form.Item> |
|||
``` |
|||
|
|||
## 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 | object | n/a | |
|||
| hideRequiredMark | Hide required mark of all form items | Boolean | false | |
|||
| labelAlign | Label text align | 'left' \| 'right' | 'right' | |
|||
| labelCol | (Added in 3.14.0. Previous version can only set on FormItem.) 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 `<Col>` | [object](https://ant.design/components/grid/#Col) | | |
|||
| layout | Define form layout | 'horizontal'\|'vertical'\|'inline' | 'horizontal' | |
|||
| onSubmit | Defines a function will be called if form data validation is successful. | Function(e:Event) | | |
|||
| wrapperCol | (Added in 3.14.0. Previous version can only set on FormItem.) The layout for input controls, same as `labelCol` | [object](https://ant.design/components/grid/#Col) | | |
|||
| colon | change default props colon value of Form.Item | boolean | true | |
|||
|
|||
### 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). Please note that the form fields will become controlled components. Properties like errors will not be automatically mapped and need to be manually passed in. | (props) => ({ \[fieldName\]: FormField { value } }) | |
|||
| name | Set the id prefix of fields under form | - | |
|||
| 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 fields (including errors) of a `Form.Item` gets changed. Usage example: saving the field's value to Redux store. | Function(props, changedFields, allFields) | |
|||
| onValuesChange | A handler while value of any field is changed | (props, changedValues, allValues) => void | |
|||
|
|||
If you want to get `ref` after `Form.create`, you can use `wrappedComponentRef` provided by `rc-form`, [details can be viewed here](https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140). |
|||
|
|||
```jsx |
|||
class CustomizedForm extends React.Component { ... } |
|||
|
|||
// use wrappedComponentRef |
|||
const EnhancedForm = Form.create()(CustomizedForm); |
|||
<EnhancedForm wrappedComponentRef={(form) => this.form = form} /> |
|||
this.form // => The instance of CustomizedForm |
|||
``` |
|||
|
|||
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 value and error state of fields. [Code Sample](https://github.com/react-component/form/blob/3b9959b57ab30b41d8890ff30c79a7e7c383cad3/examples/server-validate.js#L74-L79) | ({<br /> \[fieldName\]: {value: any, errors: \[Error\] }<br />}) => void | |
|||
| setFieldsValue | Set the value of a field. (Note: please don't use it in `componentWillReceiveProps`, otherwise, it will cause an endless loop, [reason](https://github.com/ant-design/ant-design/issues/2985)) | ({ \[fieldName\]: value }) => void | |
|||
| validateFields | Validate the specified fields and get theirs values and errors. If you don't specify the parameter of fieldNames, you will validate all fields. | (<br /> \[fieldNames: string\[]],<br /> \[options: object\],<br /> callback(errors, values)<br />) => void | |
|||
| 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` | |
|||
|
|||
### validateFields/validateFieldsAndScroll |
|||
|
|||
```jsx |
|||
const { |
|||
form: { validateFields }, |
|||
} = this.props; |
|||
validateFields((errors, values) => { |
|||
// ... |
|||
}); |
|||
validateFields(['field1', 'field2'], (errors, values) => { |
|||
// ... |
|||
}); |
|||
validateFields(['field1', 'field2'], options, (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 | {} | |
|||
|
|||
#### Callback arguments example of validateFields |
|||
|
|||
- `errors`: |
|||
|
|||
```js |
|||
{ |
|||
"username": { |
|||
"errors": [ |
|||
{ |
|||
"message": "Please input your username!", |
|||
"field": "username" |
|||
} |
|||
] |
|||
}, |
|||
"password": { |
|||
"errors": [ |
|||
{ |
|||
"message": "Please input your Password!", |
|||
"field": "password" |
|||
} |
|||
] |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- `values`: |
|||
|
|||
```js |
|||
{ |
|||
"username": "username", |
|||
"password": "password", |
|||
} |
|||
``` |
|||
|
|||
### 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 cannot 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 |
|||
|
|||
If you use `react@<15.3.0`, then, you can't use `getFieldDecorator` in stateless component: <https://github.com/facebook/react/pull/6534> |
|||
|
|||
#### 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.getValueProps | Get the component props according to field value. | function(value): any | [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 `===` internally, we recommend to use variable 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.preserve | Keep the field even if field removed | boolean | - | |
|||
| 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. |
|||
|
|||
| Property | Description | Type | Default Value | Version | |
|||
| --- | --- | --- | --- | --- | |
|||
| 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 | | | |
|||
| htmlFor | Set sub label `htmlFor`. | string | | 3.17.0 | |
|||
| 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 `<Col>`. You can set on Form one time after 3.14.0. Will take FormItem's prop when both set with Form. | [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`. You can set on Form one time after 3.14.0. Will take FormItem's prop when both set with Form. | [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\|ReactNode | - | |
|||
| 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 |
|||
|
|||
```tsx |
|||
import { Form } from 'antd'; |
|||
import { FormComponentProps } from 'antd/lib/form'; |
|||
|
|||
interface UserFormProps extends FormComponentProps { |
|||
age: number; |
|||
name: string; |
|||
} |
|||
|
|||
class UserForm extends React.Component<UserFormProps, any> { |
|||
// ... |
|||
} |
|||
|
|||
const App = Form.create<UserFormProps>({ |
|||
// ... |
|||
})(UserForm); |
|||
``` |
|||
|
|||
<style> |
|||
.code-box-demo .ant-form:not(.ant-form-inline):not(.ant-form-vertical) { |
|||
max-width: 600px; |
|||
} |
|||
.markdown.api-container table td:last-child { |
|||
white-space: nowrap; |
|||
word-wrap: break-word; |
|||
} |
|||
</style> |
@ -1,12 +0,0 @@ |
|||
import Form from './Form'; |
|||
|
|||
export { |
|||
FormProps, |
|||
FormComponentProps, |
|||
FormCreateOption, |
|||
ValidateCallback, |
|||
ValidationRule, |
|||
} from './Form'; |
|||
export { FormItemProps } from './FormItem'; |
|||
|
|||
export default Form; |
@ -1,256 +0,0 @@ |
|||
--- |
|||
category: Components |
|||
subtitle: 表单 |
|||
type: 数据录入 |
|||
cols: 1 |
|||
title: Form |
|||
--- |
|||
|
|||
具有数据收集、校验和提交功能的表单,包含复选框、单选框、输入框、下拉选择框等元素。 |
|||
|
|||
## 何时使用 |
|||
|
|||
- 用于创建一个实体或收集信息。 |
|||
- 需要对输入的数据类型进行校验时。 |
|||
|
|||
## 表单 |
|||
|
|||
我们为 `form` 提供了以下三种排列方式: |
|||
|
|||
- 水平排列:标签和表单控件水平排列;(默认) |
|||
- 垂直排列:标签和表单控件上下垂直排列; |
|||
- 行内排列:表单项水平行内排列。 |
|||
|
|||
## 表单域 |
|||
|
|||
表单一定会包含表单域,表单域可以是输入控件,标准表单域,标签,下拉菜单,文本域等。 |
|||
|
|||
这里我们封装了表单域 `<Form.Item />` 。 |
|||
|
|||
```jsx |
|||
<Form.Item {...props}>{children}</Form.Item> |
|||
``` |
|||
|
|||
## API |
|||
|
|||
### Form |
|||
|
|||
**更多示例参考 [rc-form](http://react-component.github.io/form/)**。 |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
| --- | --- | --- | --- | |
|||
| form | 经 `Form.create()` 包装过的组件会自带 `this.props.form` 属性 | object | - | |
|||
| hideRequiredMark | 隐藏所有表单项的必选标记 | Boolean | false | |
|||
| labelAlign | label 标签的文本对齐方式 | 'left' \| 'right' | 'right' | |
|||
| labelCol | (3.14.0 新增,之前的版本只能设置到 FormItem 上。)label 标签布局,同 `<Col>` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}` 或 `sm: {span: 3, offset: 12}` | [object](https://ant.design/components/grid/#Col) | | |
|||
| layout | 表单布局 | 'horizontal'\|'vertical'\|'inline' | 'horizontal' | |
|||
| onSubmit | 数据验证成功后回调事件 | Function(e:Event) | | |
|||
| wrapperCol | (3.14.0 新增,之前的版本只能设置到 FormItem 上。)需要为输入控件设置布局样式时,使用该属性,用法同 labelCol | [object](https://ant.design/components/grid/#Col) | | |
|||
| colon | 配置 Form.Item 的 colon 的默认值 | boolean | true | |
|||
|
|||
### Form.create(options) |
|||
|
|||
使用方式如下: |
|||
|
|||
```jsx |
|||
class CustomizedForm extends React.Component {} |
|||
|
|||
CustomizedForm = Form.create({})(CustomizedForm); |
|||
``` |
|||
|
|||
`options` 的配置项如下。 |
|||
|
|||
| 参数 | 说明 | 类型 | |
|||
| --- | --- | --- | |
|||
| mapPropsToFields | 把父组件的属性映射到表单项上(如:把 Redux store 中的值读出),需要对返回值中的表单域数据用 [`Form.createFormField`](#Form.createFormField) 标记,注意表单项将变成受控组件, error 等也需要一并手动传入 | (props) => ({ \[fieldName\]: FormField { value } }) | |
|||
| name | 设置表单域内字段 id 的前缀 | - | |
|||
| validateMessages | 默认校验信息,可用于把默认错误信息改为中文等,格式与 [newMessages](https://github.com/yiminghe/async-validator/blob/master/src/messages.js) 返回值一致 | Object { \[nested.path]: String } | |
|||
| onFieldsChange | 当 `Form.Item` 子节点的值(包括 error)发生改变时触发,可以把对应的值转存到 Redux store | Function(props, changedFields, allFields) | |
|||
| onValuesChange | 任一表单域的值发生改变时的回调 | (props, changedValues, allValues) => void | |
|||
|
|||
经过 `Form.create` 之后如果要拿到 `ref`,可以使用 `rc-form` 提供的 `wrappedComponentRef`,[详细内容可以查看这里](https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140)。 |
|||
|
|||
```jsx |
|||
class CustomizedForm extends React.Component { ... } |
|||
|
|||
// use wrappedComponentRef |
|||
const EnhancedForm = Form.create()(CustomizedForm); |
|||
<EnhancedForm wrappedComponentRef={(form) => this.form = form} /> |
|||
this.form // => The instance of CustomizedForm |
|||
``` |
|||
|
|||
经过 `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 | 设置一组输入控件的值与错误状态:[代码](https://github.com/react-component/form/blob/3b9959b57ab30b41d8890ff30c79a7e7c383cad3/examples/server-validate.js#L74-L79) | ({<br /> \[fieldName\]: {value: any, errors: \[Error\] }<br />}) => void | |
|||
| setFieldsValue | 设置一组输入控件的值(注意:不要在 `componentWillReceiveProps` 内使用,否则会导致死循环,[原因](https://github.com/ant-design/ant-design/issues/2985)) | ({ \[fieldName\]: value }) => void | |
|||
| validateFields | 校验并获取一组输入域的值与 Error,若 fieldNames 参数为空,则校验全部组件 | (<br /> \[fieldNames: string\[]],<br /> \[options: object\],<br /> callback(errors, values)<br />) => void | |
|||
| validateFieldsAndScroll | 与 `validateFields` 相似,但校验完后,如果校验不通过的菜单域不在可见范围内,则自动滚动进可见范围 | 参考 `validateFields` | |
|||
|
|||
### validateFields/validateFieldsAndScroll |
|||
|
|||
```jsx |
|||
const { |
|||
form: { validateFields }, |
|||
} = this.props; |
|||
validateFields((errors, values) => { |
|||
// ... |
|||
}); |
|||
validateFields(['field1', 'field2'], (errors, values) => { |
|||
// ... |
|||
}); |
|||
validateFields(['field1', 'field2'], options, (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 | {} | |
|||
|
|||
#### validateFields 的 callback 参数示例 |
|||
|
|||
- `errors`: |
|||
|
|||
```js |
|||
{ |
|||
"username": { |
|||
"errors": [ |
|||
{ |
|||
"message": "Please input your username!", |
|||
"field": "username" |
|||
} |
|||
] |
|||
}, |
|||
"password": { |
|||
"errors": [ |
|||
{ |
|||
"message": "Please input your Password!", |
|||
"field": "password" |
|||
} |
|||
] |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- `values`: |
|||
|
|||
```js |
|||
{ |
|||
"username": "username", |
|||
"password": "password", |
|||
} |
|||
``` |
|||
|
|||
### 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` 来动态改变表单值。 |
|||
|
|||
#### 特别注意 |
|||
|
|||
如果使用的是 `react@<15.3.0`,则 `getFieldDecorator` 调用不能位于纯函数组件中: <https://github.com/facebook/react/pull/6534> |
|||
|
|||
#### 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.preserve | 即便字段不再使用,也保留该字段的值 | boolean | - | |
|||
| 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` 无法自动生成。 |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | 版本 | |
|||
| --- | --- | --- | --- | --- | |
|||
| colon | 配合 label 属性使用,表示是否显示 label 后面的冒号 | boolean | true | | |
|||
| extra | 额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 | string\|ReactNode | | | |
|||
| hasFeedback | 配合 validateStatus 属性使用,展示校验状态图标,建议只配合 Input 组件使用 | boolean | false | | |
|||
| help | 提示信息,如不设置,则会根据校验规则自动生成 | string\|ReactNode | | | |
|||
| htmlFor | 设置子元素 label `htmlFor` 属性 | string | | 3.17.0 | |
|||
| label | label 标签的文本 | string\|ReactNode | | | |
|||
| labelCol | label 标签布局,同 `<Col>` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}` 或 `sm: {span: 3, offset: 12}`。在 3.14.0 之后,你可以通过 Form 的 labelCol 进行统一设置。当和 Form 同时设置时,以 FormItem 为准。 | [object](https://ant.design/components/grid/#Col) | | | |
|||
| required | 是否必填,如不设置,则会根据校验规则自动生成 | boolean | false | | |
|||
| validateStatus | 校验状态,如不设置,则会根据校验规则自动生成,可选:'success' 'warning' 'error' 'validating' | string | | | |
|||
| wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol。在 3.14.0 之后,你可以通过 Form 的 labelCol 进行统一设置。当和 Form 同时设置时,以 FormItem 为准。 | [object](https://ant.design/components/grid/#Col) | | | |
|||
|
|||
### 校验规则 |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
| --- | --- | --- | --- | |
|||
| enum | 枚举类型 | string | - | |
|||
| len | 字段长度 | number | - | |
|||
| max | 最大长度 | number | - | |
|||
| message | 校验文案 | string\|ReactNode | - | |
|||
| 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 中使用 |
|||
|
|||
```tsx |
|||
import { Form } from 'antd'; |
|||
import { FormComponentProps } from 'antd/lib/form'; |
|||
|
|||
interface UserFormProps extends FormComponentProps { |
|||
age: number; |
|||
name: string; |
|||
} |
|||
|
|||
class UserForm extends React.Component<UserFormProps, any> { |
|||
// ... |
|||
} |
|||
|
|||
const App = Form.create<UserFormProps>({ |
|||
// ... |
|||
})(UserForm); |
|||
``` |
|||
|
|||
<style> |
|||
.code-box-demo .ant-form:not(.ant-form-inline):not(.ant-form-vertical) { |
|||
max-width: 600px; |
|||
} |
|||
.markdown.api-container table td:last-child { |
|||
white-space: nowrap; |
|||
word-wrap: break-word; |
|||
} |
|||
</style> |
@ -1,89 +0,0 @@ |
|||
import * as React from 'react'; |
|||
import { Omit } from '../_util/type'; |
|||
import { WrappedFormInternalProps } from './Form'; |
|||
|
|||
// Heavily copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/hoist-non-react-statics/index.d.ts
|
|||
// tslint:disable-next-line:class-name
|
|||
interface REACT_STATICS { |
|||
childContextTypes: true; |
|||
contextType: true; |
|||
contextTypes: true; |
|||
defaultProps: true; |
|||
displayName: true; |
|||
getDefaultProps: true; |
|||
getDerivedStateFromError: true; |
|||
getDerivedStateFromProps: true; |
|||
mixins: true; |
|||
propTypes: true; |
|||
type: true; |
|||
} |
|||
|
|||
// tslint:disable-next-line:class-name
|
|||
interface KNOWN_STATICS { |
|||
name: true; |
|||
length: true; |
|||
prototype: true; |
|||
caller: true; |
|||
callee: true; |
|||
arguments: true; |
|||
arity: true; |
|||
} |
|||
|
|||
// tslint:disable-next-line:class-name
|
|||
interface MEMO_STATICS { |
|||
$$typeof: true; |
|||
compare: true; |
|||
defaultProps: true; |
|||
displayName: true; |
|||
propTypes: true; |
|||
type: true; |
|||
} |
|||
|
|||
// tslint:disable-next-line:class-name
|
|||
interface FORWARD_REF_STATICS { |
|||
$$typeof: true; |
|||
render: true; |
|||
defaultProps: true; |
|||
displayName: true; |
|||
propTypes: true; |
|||
} |
|||
|
|||
type NonReactStatics< |
|||
S extends React.ComponentType<any>, |
|||
C extends { |
|||
[key: string]: true; |
|||
} = {} |
|||
> = { |
|||
[key in Exclude< |
|||
keyof S, |
|||
S extends React.MemoExoticComponent<any> |
|||
? keyof MEMO_STATICS | keyof C |
|||
: S extends React.ForwardRefExoticComponent<any> |
|||
? keyof FORWARD_REF_STATICS | keyof C |
|||
: keyof REACT_STATICS | keyof KNOWN_STATICS | keyof C |
|||
>]: S[key] |
|||
}; |
|||
|
|||
// Copy from @types/react-redux https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-redux/index.d.ts
|
|||
export type Matching<InjectedProps, DecorationTargetProps> = { |
|||
[P in keyof DecorationTargetProps]: P extends keyof InjectedProps |
|||
? InjectedProps[P] extends DecorationTargetProps[P] |
|||
? DecorationTargetProps[P] |
|||
: InjectedProps[P] |
|||
: DecorationTargetProps[P] |
|||
}; |
|||
|
|||
export type GetProps<C> = C extends React.ComponentType<infer P> ? P : never; |
|||
|
|||
export type ConnectedComponentClass<C extends React.ComponentType<any>, P> = React.ComponentClass< |
|||
JSX.LibraryManagedAttributes<C, P> |
|||
> & |
|||
NonReactStatics<C> & { |
|||
WrappedComponent: C; |
|||
}; |
|||
|
|||
export type FormWrappedProps<TOwnProps extends WrappedFormInternalProps> = < |
|||
C extends React.ComponentType<Matching<TOwnProps, GetProps<C>>> |
|||
>( |
|||
component: C, |
|||
) => ConnectedComponentClass<C, Omit<TOwnProps, keyof WrappedFormInternalProps>>; |
@ -1,681 +0,0 @@ |
|||
@import '../../style/themes/index'; |
|||
@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: @font-size-base; |
|||
@form-help-margin-top: (@form-component-height - @form-component-max-height) / 2 + 2px; |
|||
@form-explain-font-size: @font-size-base; |
|||
// Extends additional 1px to fix precision issue. |
|||
// https://github.com/ant-design/ant-design/issues/12803 |
|||
// https://github.com/ant-design/ant-design/issues/8220 |
|||
@form-explain-precision: 1px; |
|||
@form-explain-height: floor(@form-explain-font-size * @line-height-base); |
|||
|
|||
.@{form-prefix-cls} { |
|||
.reset-component; |
|||
.reset-form; |
|||
} |
|||
|
|||
.@{form-prefix-cls}-item-required::before { |
|||
display: inline-block; |
|||
margin-right: 4px; |
|||
color: @label-required-color; |
|||
font-size: @font-size-base; |
|||
font-family: SimSun, sans-serif; |
|||
line-height: 1; |
|||
content: '*'; |
|||
.@{form-prefix-cls}-hide-required-mark & { |
|||
display: none; |
|||
} |
|||
} |
|||
|
|||
.@{form-prefix-cls}-item-label > label { |
|||
color: @label-color; |
|||
|
|||
&::after { |
|||
& when (@form-item-trailing-colon=true) { |
|||
content: ':'; |
|||
} |
|||
& when not (@form-item-trailing-colon=true) { |
|||
content: ' '; |
|||
} |
|||
|
|||
position: relative; |
|||
top: -0.5px; |
|||
margin: 0 8px 0 2px; |
|||
} |
|||
|
|||
&.@{form-prefix-cls}-item-no-colon::after { |
|||
content: ' '; |
|||
} |
|||
} |
|||
|
|||
// Radio && Checkbox |
|||
input[type='radio'], |
|||
input[type='checkbox'] { |
|||
&[disabled], |
|||
&.disabled { |
|||
cursor: not-allowed; |
|||
} |
|||
} |
|||
|
|||
// These classes are used directly on <label>s |
|||
.@{ant-prefix}-radio-inline, |
|||
.@{ant-prefix}-radio-vertical, |
|||
.@{ant-prefix}-checkbox-inline, |
|||
.@{ant-prefix}-checkbox-vertical { |
|||
&.disabled { |
|||
cursor: not-allowed; |
|||
} |
|||
} |
|||
|
|||
// These classes are used on elements with <label> descendants |
|||
.@{ant-prefix}-radio, |
|||
.@{ant-prefix}-checkbox { |
|||
&.disabled { |
|||
label { |
|||
cursor: not-allowed; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Form items |
|||
// You should wrap labels and controls in .@{form-prefix-cls}-item for optimum spacing |
|||
.@{form-prefix-cls}-item { |
|||
label { |
|||
position: relative; |
|||
|
|||
> .@{iconfont-css-prefix} { |
|||
font-size: @font-size-base; |
|||
vertical-align: top; |
|||
} |
|||
} |
|||
|
|||
.reset-component; |
|||
|
|||
margin-bottom: @form-item-margin-bottom; |
|||
vertical-align: top; |
|||
|
|||
&-control { |
|||
position: relative; |
|||
line-height: @form-component-max-height; |
|||
.clearfix; |
|||
} |
|||
|
|||
&-children { |
|||
position: relative; |
|||
} |
|||
|
|||
&-with-help { |
|||
margin-bottom: max(0, @form-item-margin-bottom - @form-explain-height - @form-help-margin-top); |
|||
} |
|||
|
|||
&-label { |
|||
display: inline-block; |
|||
overflow: hidden; |
|||
line-height: @form-component-max-height - 0.0001px; |
|||
white-space: nowrap; |
|||
text-align: right; |
|||
vertical-align: middle; |
|||
|
|||
&-left { |
|||
text-align: left; |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-switch { |
|||
margin: 2px 0 4px; |
|||
} |
|||
} |
|||
|
|||
.@{form-prefix-cls}-explain, |
|||
.@{form-prefix-cls}-extra { |
|||
clear: both; |
|||
min-height: @form-explain-height + @form-explain-precision; |
|||
margin-top: @form-help-margin-top; |
|||
color: @text-color-secondary; |
|||
font-size: @form-explain-font-size; |
|||
line-height: @line-height-base; |
|||
transition: color 0.3s @ease-out; // sync input color transition |
|||
} |
|||
|
|||
.@{form-prefix-cls}-explain { |
|||
margin-bottom: -@form-explain-precision; |
|||
} |
|||
|
|||
.@{form-prefix-cls}-extra { |
|||
padding-top: 4px; |
|||
} |
|||
|
|||
.@{form-prefix-cls}-text { |
|||
display: inline-block; |
|||
padding-right: 8px; |
|||
} |
|||
|
|||
.@{form-prefix-cls}-split { |
|||
display: block; |
|||
text-align: center; |
|||
} |
|||
|
|||
form { |
|||
.has-feedback { |
|||
.@{ant-prefix}-input { |
|||
padding-right: 24px; |
|||
} |
|||
|
|||
.@{ant-prefix}-input-password-icon { |
|||
margin-right: 18px; |
|||
} |
|||
|
|||
// Fix overlapping between feedback icon and <Select>'s arrow. |
|||
// https://github.com/ant-design/ant-design/issues/4431 |
|||
> .@{ant-prefix}-select .@{ant-prefix}-select-arrow, |
|||
> .@{ant-prefix}-select .@{ant-prefix}-select-selection__clear, |
|||
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow, |
|||
:not(.@{ant-prefix}-input-group-addon) |
|||
> .@{ant-prefix}-select |
|||
.@{ant-prefix}-select-selection__clear { |
|||
right: 28px; |
|||
} |
|||
> .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value, |
|||
:not(.@{ant-prefix}-input-group-addon) |
|||
> .@{ant-prefix}-select |
|||
.@{ant-prefix}-select-selection-selected-value { |
|||
padding-right: 42px; |
|||
} |
|||
|
|||
.@{ant-prefix}-cascader-picker { |
|||
&-arrow { |
|||
margin-right: 17px; |
|||
} |
|||
&-clear { |
|||
right: 28px; |
|||
} |
|||
} |
|||
|
|||
// Fix issue: https://github.com/ant-design/ant-design/issues/7854 |
|||
.@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) { |
|||
.@{ant-prefix}-input-suffix { |
|||
right: 28px; |
|||
} |
|||
} |
|||
|
|||
// Fix issue: https://github.com/ant-design/ant-design/issues/4783 |
|||
.@{ant-prefix}-calendar-picker, |
|||
.@{ant-prefix}-time-picker { |
|||
&-icon, |
|||
&-clear { |
|||
right: 28px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-mentions, |
|||
textarea.@{ant-prefix}-input { |
|||
height: auto; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
// input[type=file] |
|||
.@{ant-prefix}-upload { |
|||
background: transparent; |
|||
} |
|||
|
|||
input[type='radio'], |
|||
input[type='checkbox'] { |
|||
width: 14px; |
|||
height: 14px; |
|||
} |
|||
|
|||
// Radios and checkboxes on same line |
|||
.@{ant-prefix}-radio-inline, |
|||
.@{ant-prefix}-checkbox-inline { |
|||
display: inline-block; |
|||
margin-left: 8px; |
|||
font-weight: normal; |
|||
vertical-align: middle; |
|||
cursor: pointer; |
|||
|
|||
&:first-child { |
|||
margin-left: 0; |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-checkbox-vertical, |
|||
.@{ant-prefix}-radio-vertical { |
|||
display: block; |
|||
} |
|||
|
|||
.@{ant-prefix}-checkbox-vertical + .@{ant-prefix}-checkbox-vertical, |
|||
.@{ant-prefix}-radio-vertical + .@{ant-prefix}-radio-vertical { |
|||
margin-left: 0; |
|||
} |
|||
|
|||
.@{ant-prefix}-input-number { |
|||
+ .@{form-prefix-cls}-text { |
|||
margin-left: 8px; |
|||
} |
|||
&-handler-wrap { |
|||
z-index: 2; // https://github.com/ant-design/ant-design/issues/6289 |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-select, |
|||
.@{ant-prefix}-cascader-picker { |
|||
width: 100%; |
|||
} |
|||
|
|||
// Don't impact select inside input group |
|||
.@{ant-prefix}-input-group .@{ant-prefix}-select, |
|||
.@{ant-prefix}-input-group .@{ant-prefix}-cascader-picker { |
|||
width: auto; |
|||
} |
|||
|
|||
// fix input with addon position. https://github.com/ant-design/ant-design/issues/8243 |
|||
:not(.@{ant-prefix}-input-group-wrapper) > .@{ant-prefix}-input-group, |
|||
.@{ant-prefix}-input-group-wrapper { |
|||
position: relative; |
|||
top: -1px; |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
} |
|||
} |
|||
|
|||
// Input combined with select |
|||
.@{ant-prefix}-input-group-wrap { |
|||
.@{ant-prefix}-select-selection { |
|||
border-top-left-radius: 0; |
|||
border-bottom-left-radius: 0; |
|||
&:hover { |
|||
border-color: @border-color-base; |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-select-selection--single { |
|||
height: @input-height-lg; |
|||
margin-left: -1px; |
|||
background-color: fade(@black, 7%); |
|||
.@{ant-prefix}-select-selection__rendered { |
|||
padding-right: 25px; |
|||
padding-left: 8px; |
|||
line-height: 30px; |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-select-open .@{ant-prefix}-select-selection { |
|||
border-color: @border-color-base; |
|||
box-shadow: none; |
|||
} |
|||
} |
|||
|
|||
// Form layout |
|||
//== Vertical Form |
|||
.make-vertical-layout-label() { |
|||
display: block; |
|||
margin: @form-vertical-label-margin; |
|||
padding: @form-vertical-label-padding; |
|||
line-height: @line-height-base; |
|||
white-space: initial; |
|||
text-align: left; |
|||
|
|||
label::after { |
|||
display: none; |
|||
} |
|||
} |
|||
|
|||
.make-vertical-layout() { |
|||
.@{form-prefix-cls}-item-label, |
|||
.@{form-prefix-cls}-item-control-wrapper { |
|||
display: block; |
|||
width: 100%; |
|||
} |
|||
.@{form-prefix-cls}-item-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
} |
|||
|
|||
.@{form-prefix-cls}-vertical .@{form-prefix-cls}-item-label, |
|||
// when labelCol is 24, it is a vertical form |
|||
.@{ant-prefix}-col-24.@{form-prefix-cls}-item-label, |
|||
.@{ant-prefix}-col-xl-24.@{form-prefix-cls}-item-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
|
|||
.@{form-prefix-cls}-vertical { |
|||
.@{form-prefix-cls}-item { |
|||
padding-bottom: 8px; |
|||
} |
|||
.@{form-prefix-cls}-item-control { |
|||
line-height: @line-height-base; |
|||
} |
|||
.@{form-prefix-cls}-explain { |
|||
margin-top: 2px; |
|||
margin-bottom: -4px - @form-explain-precision; |
|||
} |
|||
.@{form-prefix-cls}-extra { |
|||
margin-top: 2px; |
|||
margin-bottom: -4px; |
|||
} |
|||
} |
|||
|
|||
@media (max-width: @screen-xs-max) { |
|||
.make-vertical-layout(); |
|||
.@{ant-prefix}-col-xs-24.@{form-prefix-cls}-item-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
} |
|||
|
|||
@media (max-width: @screen-sm-max) { |
|||
.@{ant-prefix}-col-sm-24.@{form-prefix-cls}-item-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
} |
|||
|
|||
@media (max-width: @screen-md-max) { |
|||
.@{ant-prefix}-col-md-24.@{form-prefix-cls}-item-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
} |
|||
|
|||
@media (max-width: @screen-lg-max) { |
|||
.@{ant-prefix}-col-lg-24.@{form-prefix-cls}-item-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
} |
|||
|
|||
@media (max-width: @screen-xl-max) { |
|||
.@{ant-prefix}-col-xl-24.@{form-prefix-cls}-item-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
} |
|||
|
|||
//== Inline Form |
|||
.@{form-prefix-cls}-inline { |
|||
.@{form-prefix-cls}-item { |
|||
display: inline-block; |
|||
margin-right: 16px; |
|||
margin-bottom: 0; |
|||
|
|||
&-with-help { |
|||
margin-bottom: @form-item-margin-bottom; |
|||
} |
|||
|
|||
> .@{form-prefix-cls}-item-control-wrapper, |
|||
> .@{form-prefix-cls}-item-label { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
} |
|||
} |
|||
|
|||
.@{form-prefix-cls}-text { |
|||
display: inline-block; |
|||
} |
|||
|
|||
.has-feedback { |
|||
display: inline-block; |
|||
} |
|||
} |
|||
|
|||
// Validation state |
|||
.has-success, |
|||
.has-warning, |
|||
.has-error, |
|||
.is-validating { |
|||
&.has-feedback .@{form-prefix-cls}-item-children-icon { |
|||
position: absolute; |
|||
top: 50%; |
|||
right: 0; |
|||
z-index: 1; |
|||
width: @form-component-height; |
|||
height: 20px; |
|||
margin-top: -10px; |
|||
font-size: @form-feedback-icon-size; |
|||
line-height: 20px; |
|||
text-align: center; |
|||
visibility: visible; |
|||
animation: zoomIn 0.3s @ease-out-back; |
|||
pointer-events: none; |
|||
|
|||
& svg { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
margin: auto; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.has-success { |
|||
&.has-feedback .@{form-prefix-cls}-item-children-icon { |
|||
color: @success-color; |
|||
animation-name: diffZoomIn1 !important; |
|||
} |
|||
} |
|||
|
|||
.has-warning { |
|||
.form-control-validation(@warning-color; @warning-color; @form-warning-input-bg;); |
|||
|
|||
&.has-feedback .@{form-prefix-cls}-item-children-icon { |
|||
color: @warning-color; |
|||
animation-name: diffZoomIn3 !important; |
|||
} |
|||
|
|||
//select |
|||
.@{ant-prefix}-select { |
|||
&-selection { |
|||
border-color: @warning-color; |
|||
&:hover { |
|||
border-color: @warning-color; |
|||
} |
|||
} |
|||
&-open .@{ant-prefix}-select-selection, |
|||
&-focused .@{ant-prefix}-select-selection { |
|||
.active(@warning-color); |
|||
} |
|||
} |
|||
|
|||
// arrow and icon |
|||
.@{ant-prefix}-calendar-picker-icon::after, |
|||
.@{ant-prefix}-time-picker-icon::after, |
|||
.@{ant-prefix}-picker-icon::after, |
|||
.@{ant-prefix}-select-arrow, |
|||
.@{ant-prefix}-cascader-picker-arrow { |
|||
color: @warning-color; |
|||
} |
|||
|
|||
//input-number, timepicker |
|||
.@{ant-prefix}-input-number, |
|||
.@{ant-prefix}-time-picker-input { |
|||
border-color: @warning-color; |
|||
&-focused, |
|||
&:focus { |
|||
.active(@warning-color); |
|||
} |
|||
&:not([disabled]):hover { |
|||
border-color: @warning-color; |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input { |
|||
.active(@warning-color); |
|||
} |
|||
} |
|||
|
|||
.has-error { |
|||
.form-control-validation(@error-color; @error-color; @form-error-input-bg;); |
|||
|
|||
&.has-feedback .@{form-prefix-cls}-item-children-icon { |
|||
color: @error-color; |
|||
animation-name: diffZoomIn2 !important; |
|||
} |
|||
|
|||
//select |
|||
.@{ant-prefix}-select { |
|||
&-selection { |
|||
border-color: @error-color; |
|||
&:hover { |
|||
border-color: @error-color; |
|||
} |
|||
} |
|||
&-open .@{ant-prefix}-select-selection, |
|||
&-focused .@{ant-prefix}-select-selection { |
|||
.active(@error-color); |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-select.@{ant-prefix}-select-auto-complete { |
|||
.@{ant-prefix}-input:focus { |
|||
border-color: @error-color; |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-input-group-addon .@{ant-prefix}-select { |
|||
&-selection { |
|||
border-color: transparent; |
|||
box-shadow: none; |
|||
} |
|||
} |
|||
|
|||
// arrow and icon |
|||
.@{ant-prefix}-calendar-picker-icon::after, |
|||
.@{ant-prefix}-time-picker-icon::after, |
|||
.@{ant-prefix}-picker-icon::after, |
|||
.@{ant-prefix}-select-arrow, |
|||
.@{ant-prefix}-cascader-picker-arrow { |
|||
color: @error-color; |
|||
} |
|||
|
|||
//input-number, timepicker |
|||
.@{ant-prefix}-input-number, |
|||
.@{ant-prefix}-time-picker-input { |
|||
border-color: @error-color; |
|||
&-focused, |
|||
&:focus { |
|||
.active(@error-color); |
|||
} |
|||
&:not([disabled]):hover { |
|||
border-color: @error-color; |
|||
} |
|||
} |
|||
.@{ant-prefix}-mention-wrapper { |
|||
.@{ant-prefix}-mention-editor { |
|||
&, |
|||
&:not([disabled]):hover { |
|||
border-color: @error-color; |
|||
} |
|||
} |
|||
&.@{ant-prefix}-mention-active:not([disabled]) .@{ant-prefix}-mention-editor, |
|||
.@{ant-prefix}-mention-editor:not([disabled]):focus { |
|||
.active(@error-color); |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input { |
|||
.active(@error-color); |
|||
} |
|||
|
|||
// transfer |
|||
.@{ant-prefix}-transfer { |
|||
&-list { |
|||
border-color: @error-color; |
|||
|
|||
&-search:not([disabled]) { |
|||
border-color: @input-border-color; |
|||
|
|||
&:hover { |
|||
.hover(); |
|||
} |
|||
|
|||
&:focus { |
|||
.active(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.is-validating { |
|||
&.has-feedback .@{form-prefix-cls}-item-children-icon { |
|||
display: inline-block; |
|||
color: @primary-color; |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-advanced-search-form { |
|||
.@{form-prefix-cls}-item { |
|||
margin-bottom: @form-item-margin-bottom; |
|||
|
|||
&-with-help { |
|||
margin-bottom: @form-item-margin-bottom - @form-explain-height - @form-help-margin-top; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.show-help-motion(@className, @keyframeName, @duration: @animation-duration-slow) { |
|||
.make-motion(@className, @keyframeName, @duration); |
|||
.@{className}-enter, |
|||
.@{className}-appear { |
|||
opacity: 0; |
|||
animation-timing-function: @ease-in-out; |
|||
} |
|||
.@{className}-leave { |
|||
animation-timing-function: @ease-in-out; |
|||
} |
|||
} |
|||
|
|||
.show-help-motion(show-help, antShowHelp, 0.3s); |
|||
|
|||
@keyframes antShowHelpIn { |
|||
0% { |
|||
transform: translateY(-5px); |
|||
opacity: 0; |
|||
} |
|||
100% { |
|||
transform: translateY(0); |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
@keyframes antShowHelpOut { |
|||
to { |
|||
transform: translateY(-5px); |
|||
opacity: 0; |
|||
} |
|||
} |
|||
|
|||
// need there different zoom animation |
|||
// otherwise won't trigger anim |
|||
@keyframes diffZoomIn1 { |
|||
0% { |
|||
transform: scale(0); |
|||
} |
|||
100% { |
|||
transform: scale(1); |
|||
} |
|||
} |
|||
|
|||
@keyframes diffZoomIn2 { |
|||
0% { |
|||
transform: scale(0); |
|||
} |
|||
100% { |
|||
transform: scale(1); |
|||
} |
|||
} |
|||
|
|||
@keyframes diffZoomIn3 { |
|||
0% { |
|||
transform: scale(0); |
|||
} |
|||
100% { |
|||
transform: scale(1); |
|||
} |
|||
} |
@ -1,5 +0,0 @@ |
|||
import '../../style/index.less'; |
|||
import './index.less'; |
|||
|
|||
// style dependencies
|
|||
import '../../grid/style'; |
@ -1,125 +0,0 @@ |
|||
@import '../../input/style/mixin'; |
|||
|
|||
.form-control-validation(@text-color: @input-color; @border-color: @input-border-color; @background-color: @input-bg) { |
|||
.@{ant-prefix}-form-explain, |
|||
.@{ant-prefix}-form-split { |
|||
color: @text-color; |
|||
} |
|||
// 输入框的不同校验状态 |
|||
.@{ant-prefix}-input { |
|||
&, |
|||
&:hover { |
|||
border-color: @border-color; |
|||
} |
|||
|
|||
&:focus { |
|||
.active(@border-color); |
|||
} |
|||
|
|||
&:not([disabled]):hover { |
|||
border-color: @border-color; |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-calendar-picker-open .@{ant-prefix}-calendar-picker-input { |
|||
.active(@border-color); |
|||
} |
|||
|
|||
// Input prefix |
|||
.@{ant-prefix}-input-affix-wrapper { |
|||
.@{ant-prefix}-input { |
|||
&, |
|||
&:hover { |
|||
background-color: @background-color; |
|||
border-color: @border-color; |
|||
} |
|||
|
|||
&:focus { |
|||
.active(@border-color); |
|||
} |
|||
} |
|||
|
|||
&:hover .@{ant-prefix}-input:not(.@{ant-prefix}-input-disabled) { |
|||
border-color: @border-color; |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-input-prefix { |
|||
color: @text-color; |
|||
} |
|||
|
|||
.@{ant-prefix}-input-group-addon { |
|||
color: @text-color; |
|||
background-color: @background-color; |
|||
border-color: @border-color; |
|||
} |
|||
|
|||
.has-feedback { |
|||
color: @text-color; |
|||
} |
|||
} |
|||
|
|||
// Reset form styles |
|||
// ----------------------------- |
|||
// Based on Bootstrap framework |
|||
.reset-form() { |
|||
legend { |
|||
display: block; |
|||
width: 100%; |
|||
margin-bottom: 20px; |
|||
padding: 0; |
|||
color: @text-color-secondary; |
|||
font-size: @font-size-lg; |
|||
line-height: inherit; |
|||
border: 0; |
|||
border-bottom: @border-width-base @border-style-base @border-color-base; |
|||
} |
|||
|
|||
label { |
|||
font-size: @font-size-base; |
|||
} |
|||
|
|||
input[type='search'] { |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
// Position radios and checkboxes better |
|||
input[type='radio'], |
|||
input[type='checkbox'] { |
|||
line-height: normal; |
|||
} |
|||
|
|||
input[type='file'] { |
|||
display: block; |
|||
} |
|||
|
|||
// Make range inputs behave like textual form controls |
|||
input[type='range'] { |
|||
display: block; |
|||
width: 100%; |
|||
} |
|||
|
|||
// Make multiple select elements height not fixed |
|||
select[multiple], |
|||
select[size] { |
|||
height: auto; |
|||
} |
|||
|
|||
// Focus for file, radio, and checkbox |
|||
input[type='file']:focus, |
|||
input[type='radio']:focus, |
|||
input[type='checkbox']:focus { |
|||
outline: thin dotted; |
|||
outline: 5px auto -webkit-focus-ring-color; |
|||
outline-offset: -2px; |
|||
} |
|||
|
|||
// Adjust output element |
|||
output { |
|||
display: block; |
|||
padding-top: 15px; |
|||
color: @input-color; |
|||
font-size: @font-size-base; |
|||
line-height: @line-height-base; |
|||
} |
|||
} |
@ -1,3 +1,80 @@ |
|||
import Form from 'rc-field-form'; |
|||
import * as React from 'react'; |
|||
import omit from 'omit.js'; |
|||
import classNames from 'classnames'; |
|||
import FieldForm, { FormInstance, useForm, List } from 'rc-field-form'; |
|||
import { FormProps as RcFormProps } from 'rc-field-form/lib/Form'; |
|||
import { ColProps } from '../grid/col'; |
|||
import { ConfigContext, ConfigConsumerProps } from '../config-provider'; |
|||
import { FormContext } from './context'; |
|||
import { FormLabelAlign } from './interface'; |
|||
|
|||
export type FormLayout = 'horizontal' | 'inline' | 'vertical'; |
|||
|
|||
interface FormProps extends RcFormProps { |
|||
prefixCls?: string; |
|||
hideRequiredMark?: boolean; |
|||
colon?: boolean; |
|||
name?: string; |
|||
layout?: FormLayout; |
|||
labelAlign?: FormLabelAlign; |
|||
labelCol?: ColProps; |
|||
wrapperCol?: ColProps; |
|||
} |
|||
|
|||
const InternalForm: React.FC<FormProps> = (props, ref) => { |
|||
const { getPrefixCls }: ConfigConsumerProps = React.useContext(ConfigContext); |
|||
|
|||
const { |
|||
colon, |
|||
name, |
|||
labelAlign, |
|||
labelCol, |
|||
wrapperCol, |
|||
prefixCls: customizePrefixCls, |
|||
hideRequiredMark, |
|||
className = '', |
|||
layout = 'horizontal', |
|||
} = props; |
|||
const prefixCls = getPrefixCls('form', customizePrefixCls); |
|||
|
|||
const formClassName = classNames( |
|||
prefixCls, |
|||
{ |
|||
[`${prefixCls}-${layout}`]: true, |
|||
[`${prefixCls}-hide-required-mark`]: hideRequiredMark, |
|||
}, |
|||
className, |
|||
); |
|||
|
|||
const formProps = omit(props, [ |
|||
'prefixCls', |
|||
'className', |
|||
'layout', |
|||
'hideRequiredMark', |
|||
'wrapperCol', |
|||
'labelAlign', |
|||
'labelCol', |
|||
'colon', |
|||
]); |
|||
|
|||
return ( |
|||
<FormContext.Provider |
|||
value={{ |
|||
name, |
|||
labelAlign, |
|||
labelCol, |
|||
wrapperCol, |
|||
vertical: layout === 'vertical', |
|||
colon, |
|||
}} |
|||
> |
|||
<FieldForm id={name} {...formProps} ref={ref} className={formClassName} /> |
|||
</FormContext.Provider> |
|||
); |
|||
}; |
|||
|
|||
const Form = React.forwardRef<FormInstance>(InternalForm); |
|||
|
|||
export { useForm, List, FormInstance }; |
|||
|
|||
export default Form; |
|||
|
@ -0,0 +1,219 @@ |
|||
import * as React from 'react'; |
|||
import isEqual from 'lodash/isEqual'; |
|||
import classNames from 'classnames'; |
|||
import { Field, FormInstance } from 'rc-field-form'; |
|||
import { FieldProps as RcFieldProps } from 'rc-field-form/lib/Field'; |
|||
import Row from '../grid/row'; |
|||
import { ConfigContext } from '../config-provider'; |
|||
import { tuple } from '../_util/type'; |
|||
import warning from '../_util/warning'; |
|||
import FormItemLabel, { FormItemLabelProps } from './FormItemLabel'; |
|||
import FormItemInput, { FormItemInputProps } from './FormItemInput'; |
|||
import { FormContext, FormItemContext } from './context'; |
|||
import { toArray } from './util'; |
|||
|
|||
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', ''); |
|||
export type ValidateStatus = (typeof ValidateStatuses)[number]; |
|||
|
|||
type RenderChildren = (form: FormInstance) => React.ReactElement; |
|||
|
|||
interface FormItemProps extends FormItemLabelProps, FormItemInputProps, RcFieldProps { |
|||
prefixCls?: string; |
|||
inline?: boolean; |
|||
style?: React.CSSProperties; |
|||
className?: string; |
|||
children: React.ReactElement | RenderChildren; |
|||
id?: string; |
|||
hasFeedback?: boolean; |
|||
validateStatus?: ValidateStatus; |
|||
required?: boolean; |
|||
|
|||
/** Auto passed by List render props. User should not use this. */ |
|||
fieldKey: number; |
|||
} |
|||
|
|||
const FormItem: React.FC<FormItemProps> = (props: FormItemProps) => { |
|||
const { |
|||
name, |
|||
fieldKey, |
|||
inline, |
|||
dependencies, |
|||
prefixCls: customizePrefixCls, |
|||
style, |
|||
className, |
|||
shouldUpdate, |
|||
hasFeedback, |
|||
help, |
|||
rules, |
|||
validateStatus, |
|||
children, |
|||
required, |
|||
trigger = 'onChange', |
|||
validateTrigger = 'onChange', |
|||
} = props; |
|||
const { getPrefixCls } = React.useContext(ConfigContext); |
|||
const formContext = React.useContext(FormContext); |
|||
const { updateItemErrors } = React.useContext(FormItemContext); |
|||
const [domErrorVisible, setDomErrorVisible] = React.useState(false); |
|||
const [inlineErrors, setInlineErrors] = React.useState<Record<string, string[]>>({}); |
|||
|
|||
const { name: formName } = formContext; |
|||
|
|||
// Cache Field NamePath
|
|||
const nameRef = React.useRef<(string | number)[]>([]); |
|||
|
|||
// Should clean up if Field removed
|
|||
React.useEffect(() => { |
|||
return () => { |
|||
updateItemErrors(nameRef.current.join('__SPLIT__'), []); |
|||
}; |
|||
}, []); |
|||
|
|||
const prefixCls = getPrefixCls('form', customizePrefixCls); |
|||
|
|||
return ( |
|||
<Field |
|||
{...props} |
|||
trigger={trigger} |
|||
validateTrigger={validateTrigger} |
|||
onReset={() => { |
|||
setDomErrorVisible(false); |
|||
}} |
|||
> |
|||
{(control, meta, context) => { |
|||
const { errors, name: metaName } = meta; |
|||
const mergedName = toArray(name).length ? metaName : []; |
|||
|
|||
// ======================== Errors ========================
|
|||
// Collect inline Field error to the top FormItem
|
|||
const updateChildItemErrors = inline |
|||
? updateItemErrors |
|||
: (subName: string, subErrors: string[]) => { |
|||
if (!isEqual(inlineErrors[subName], subErrors)) { |
|||
setInlineErrors({ |
|||
...inlineErrors, |
|||
[subName]: subErrors, |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
if (inline) { |
|||
nameRef.current = [...mergedName]; |
|||
if (fieldKey) { |
|||
nameRef.current[nameRef.current.length - 1] = fieldKey; |
|||
} |
|||
updateItemErrors(nameRef.current.join('__SPLIT__'), errors); |
|||
} |
|||
|
|||
let mergedErrors: React.ReactNode[]; |
|||
if (help) { |
|||
mergedErrors = toArray(help); |
|||
} else { |
|||
mergedErrors = errors; |
|||
Object.keys(inlineErrors).forEach(subName => { |
|||
const subErrors = inlineErrors[subName] || []; |
|||
if (subErrors.length) { |
|||
mergedErrors = [...mergedErrors, ...subErrors]; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// ======================== Status ========================
|
|||
let mergedValidateStatus: ValidateStatus = ''; |
|||
if (validateStatus !== undefined) { |
|||
mergedValidateStatus = validateStatus; |
|||
} else if (meta.validating) { |
|||
mergedValidateStatus = 'validating'; |
|||
} else if (!help && mergedErrors.length) { |
|||
mergedValidateStatus = 'error'; |
|||
} else if (meta.touched) { |
|||
mergedValidateStatus = 'success'; |
|||
} |
|||
|
|||
// ====================== Class Name ======================
|
|||
const itemClassName = { |
|||
[`${prefixCls}-item`]: true, |
|||
[`${prefixCls}-item-with-help`]: domErrorVisible, // TODO: handle this
|
|||
[`${className}`]: !!className, |
|||
|
|||
// Status
|
|||
[`${prefixCls}-item-has-feedback`]: |
|||
(mergedValidateStatus && hasFeedback) || mergedValidateStatus === 'validating', |
|||
[`${prefixCls}-item-has-success`]: mergedValidateStatus === 'success', |
|||
[`${prefixCls}-item-has-warning`]: mergedValidateStatus === 'warning', |
|||
[`${prefixCls}-item-has-error`]: mergedValidateStatus === 'error', |
|||
[`${prefixCls}-item-has-error-leave`]: |
|||
!help && domErrorVisible && mergedValidateStatus !== 'error', |
|||
[`${prefixCls}-item-is-validating`]: mergedValidateStatus === 'validating', |
|||
}; |
|||
|
|||
// TODO: Check if user add `required` in RuleRender
|
|||
const isRequired = |
|||
required !== undefined |
|||
? required |
|||
: !!(rules && rules.some(rule => typeof rule === 'object' && rule.required)); |
|||
|
|||
// ======================= Children =======================
|
|||
const mergedId = mergedName.join('_'); |
|||
const mergedControl: typeof control = { |
|||
...control, |
|||
id: formName ? `${formName}_${mergedId}` : mergedId, |
|||
}; |
|||
|
|||
let childNode; |
|||
if (typeof children === 'function' && (!shouldUpdate || !!name)) { |
|||
warning(false, 'Form.Item', '`children` of render props only work with `shouldUpdate`.'); |
|||
} else if (!mergedName.length && !shouldUpdate && !dependencies) { |
|||
childNode = children; |
|||
} else if (React.isValidElement(children)) { |
|||
const childProps = { ...children.props, ...mergedControl }; |
|||
|
|||
// We should keep user origin event handler
|
|||
const triggers = new Set<string>(); |
|||
[...toArray(trigger), ...toArray(validateTrigger)].forEach(eventName => { |
|||
triggers.add(eventName); |
|||
}); |
|||
|
|||
triggers.forEach(eventName => { |
|||
if (eventName in mergedControl && eventName in children.props) { |
|||
childProps[eventName] = (...args: any[]) => { |
|||
mergedControl[eventName](...args); |
|||
children.props[eventName](...args); |
|||
}; |
|||
} |
|||
}); |
|||
|
|||
childNode = React.cloneElement(children, childProps); |
|||
} else if (typeof children === 'function' && shouldUpdate && !name) { |
|||
childNode = children(context); |
|||
} |
|||
|
|||
if (inline) { |
|||
return childNode; |
|||
} |
|||
|
|||
return ( |
|||
<Row type="flex" className={classNames(itemClassName)} style={style} key="row"> |
|||
{/* Label */} |
|||
<FormItemLabel {...props} required={isRequired} prefixCls={prefixCls} /> |
|||
{/* Input Group */} |
|||
<FormItemInput |
|||
{...props} |
|||
{...meta} |
|||
errors={mergedErrors} |
|||
prefixCls={prefixCls} |
|||
onDomErrorVisibleChange={setDomErrorVisible} |
|||
validateStatus={mergedValidateStatus} |
|||
> |
|||
<FormItemContext.Provider value={{ updateItemErrors: updateChildItemErrors }}> |
|||
{childNode} |
|||
</FormItemContext.Provider> |
|||
</FormItemInput> |
|||
</Row> |
|||
); |
|||
}} |
|||
</Field> |
|||
); |
|||
}; |
|||
|
|||
export default FormItem; |
@ -0,0 +1,110 @@ |
|||
import * as React from 'react'; |
|||
import classNames from 'classnames'; |
|||
import Icon from '../icon'; |
|||
import CSSMotion from 'rc-animate/lib/CSSMotion'; |
|||
import Col, { ColProps } from '../grid/col'; |
|||
import { ValidateStatus } from './FormItem'; |
|||
import { FormContext } from './context'; |
|||
import { useCacheErrors } from './util'; |
|||
|
|||
interface FormItemInputMiscProps { |
|||
prefixCls: string; |
|||
children: React.ReactNode; |
|||
errors: React.ReactNode[]; |
|||
touched: boolean; |
|||
validating: boolean; |
|||
hasFeedback?: boolean; |
|||
validateStatus?: ValidateStatus; |
|||
onDomErrorVisibleChange: (visible: boolean) => void; |
|||
} |
|||
|
|||
export interface FormItemInputProps { |
|||
wrapperCol?: ColProps; |
|||
help?: React.ReactNode; |
|||
extra?: React.ReactNode; |
|||
} |
|||
|
|||
function getIconType(validateStatus?: ValidateStatus) { |
|||
switch (validateStatus) { |
|||
case 'success': |
|||
return 'check-circle'; |
|||
case 'warning': |
|||
return 'exclamation-circle'; |
|||
case 'error': |
|||
return 'close-circle'; |
|||
case 'validating': |
|||
return 'loading'; |
|||
default: |
|||
return ''; |
|||
} |
|||
} |
|||
|
|||
const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({ |
|||
prefixCls, |
|||
wrapperCol, |
|||
children, |
|||
errors, |
|||
onDomErrorVisibleChange, |
|||
hasFeedback, |
|||
validateStatus, |
|||
extra, |
|||
}) => { |
|||
const baseClassName = `${prefixCls}-item`; |
|||
|
|||
const formContext = React.useContext(FormContext); |
|||
|
|||
const mergedWrapperCol: ColProps = wrapperCol || formContext.wrapperCol || {}; |
|||
|
|||
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className); |
|||
|
|||
const [visible, cacheErrors] = useCacheErrors(errors, changedVisible => { |
|||
if (changedVisible) { |
|||
onDomErrorVisibleChange(true); |
|||
} |
|||
}); |
|||
|
|||
// Should provides additional icon if `hasFeedback`
|
|||
const iconType = getIconType(validateStatus); |
|||
const icon = |
|||
hasFeedback && iconType ? ( |
|||
<span className={`${baseClassName}-children-icon`}> |
|||
<Icon type={iconType} theme={iconType === 'loading' ? 'outlined' : 'filled'} /> |
|||
</span> |
|||
) : null; |
|||
|
|||
// Pass to sub FormItem should not with col info
|
|||
const subFormContext = { ...formContext }; |
|||
delete subFormContext.labelCol; |
|||
delete subFormContext.wrapperCol; |
|||
|
|||
return ( |
|||
<FormContext.Provider value={subFormContext}> |
|||
<Col {...mergedWrapperCol} className={className}> |
|||
<div className={`${baseClassName}-control-input`}> |
|||
{children} |
|||
{icon} |
|||
</div> |
|||
<CSSMotion |
|||
visible={visible} |
|||
motionName="show-help" |
|||
onLeaveEnd={() => { |
|||
onDomErrorVisibleChange(false); |
|||
}} |
|||
motionAppear |
|||
removeOnLeave |
|||
> |
|||
{({ className: motionClassName }: { className: string }) => { |
|||
return ( |
|||
<div className={classNames(`${baseClassName}-explain`, motionClassName)} key="help"> |
|||
{cacheErrors} |
|||
</div> |
|||
); |
|||
}} |
|||
</CSSMotion> |
|||
{extra && <div className={`${baseClassName}-extra`}>{extra}</div>} |
|||
</Col> |
|||
</FormContext.Provider> |
|||
); |
|||
}; |
|||
|
|||
export default FormItemInput; |
@ -0,0 +1,75 @@ |
|||
import * as React from 'react'; |
|||
import classNames from 'classnames'; |
|||
import Col, { ColProps } from '../grid/col'; |
|||
import { FormLabelAlign } from './interface'; |
|||
import { FormContext, FormContextProps } from './context'; |
|||
|
|||
export interface FormItemLabelProps { |
|||
colon?: boolean; |
|||
htmlFor: string; |
|||
label?: React.ReactNode; |
|||
labelAlign?: FormLabelAlign; |
|||
labelCol?: ColProps; |
|||
} |
|||
|
|||
const FormItemLabel: React.FC<FormItemLabelProps & { required: boolean; prefixCls: string }> = ({ |
|||
prefixCls, |
|||
label, |
|||
htmlFor, |
|||
labelCol, |
|||
labelAlign, |
|||
colon, |
|||
required, |
|||
}) => { |
|||
if (!label) return null; |
|||
|
|||
return ( |
|||
<FormContext.Consumer key="label"> |
|||
{({ |
|||
vertical, |
|||
labelAlign: contextLabelAlign, |
|||
labelCol: contextLabelCol, |
|||
colon: contextColon, |
|||
}: FormContextProps) => { |
|||
const mergedLabelCol: ColProps = labelCol || contextLabelCol || {}; |
|||
|
|||
const mergedLabelAlign: FormLabelAlign | undefined = labelAlign || contextLabelAlign; |
|||
|
|||
const labelClsBasic = `${prefixCls}-item-label`; |
|||
const labelColClassName = classNames( |
|||
labelClsBasic, |
|||
mergedLabelAlign === 'left' && `${labelClsBasic}-left`, |
|||
mergedLabelCol.className, |
|||
); |
|||
|
|||
let labelChildren = label; |
|||
// Keep label is original where there should have no colon
|
|||
const computedColon = colon === true || (contextColon !== false && colon !== false); |
|||
const haveColon = computedColon && !vertical; |
|||
// Remove duplicated user input colon
|
|||
if (haveColon && typeof label === 'string' && (label as string).trim() !== '') { |
|||
labelChildren = (label as string).replace(/[:|:]\s*$/, ''); |
|||
} |
|||
|
|||
const labelClassName = classNames({ |
|||
[`${prefixCls}-item-required`]: required, |
|||
[`${prefixCls}-item-no-colon`]: !computedColon, |
|||
}); |
|||
|
|||
return ( |
|||
<Col {...mergedLabelCol} className={labelColClassName}> |
|||
<label |
|||
htmlFor={htmlFor} |
|||
className={labelClassName} |
|||
title={typeof label === 'string' ? label : ''} |
|||
> |
|||
{labelChildren} |
|||
</label> |
|||
</Col> |
|||
); |
|||
}} |
|||
</FormContext.Consumer> |
|||
); |
|||
}; |
|||
|
|||
export default FormItemLabel; |
@ -0,0 +1,33 @@ |
|||
import * as React from 'react'; |
|||
import warning from '../_util/warning'; |
|||
import { List } from 'rc-field-form'; |
|||
|
|||
interface FieldData { |
|||
name: number; |
|||
key: number; |
|||
fieldKey: number; |
|||
} |
|||
|
|||
interface Operation { |
|||
add: () => void; |
|||
remove: (index: number) => void; |
|||
} |
|||
|
|||
interface FormListProps { |
|||
name: string | number | (string | number)[]; |
|||
children: (fields: FieldData[], operation: Operation) => React.ReactNode; |
|||
} |
|||
|
|||
const FormList: React.FC<FormListProps> = ({ children, ...props }) => { |
|||
warning(!!props.name, 'Form.List', 'Miss `name` prop.'); |
|||
|
|||
return ( |
|||
<List {...props}> |
|||
{(fields, operation) => { |
|||
return children(fields.map(field => ({ ...field, fieldKey: field.key })), operation); |
|||
}} |
|||
</List> |
|||
); |
|||
}; |
|||
|
|||
export default FormList; |
File diff suppressed because it is too large
@ -0,0 +1,126 @@ |
|||
import React from 'react'; |
|||
import { mount } from 'enzyme'; |
|||
import Form from '..'; |
|||
import Input from '../../input'; |
|||
import Button from '../../button'; |
|||
|
|||
const delay = () => |
|||
new Promise(resolve => { |
|||
setTimeout(resolve, 0); |
|||
}); |
|||
|
|||
describe('Form', () => { |
|||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); |
|||
|
|||
async function change(wrapper, index, value) { |
|||
wrapper |
|||
.find(Input) |
|||
.at(index) |
|||
.simulate('change', { target: { value } }); |
|||
await delay(); |
|||
wrapper.update(); |
|||
} |
|||
|
|||
afterEach(() => { |
|||
errorSpy.mockReset(); |
|||
}); |
|||
|
|||
afterAll(() => { |
|||
errorSpy.mockRestore(); |
|||
}); |
|||
|
|||
describe('List', () => { |
|||
function testList(name, renderField) { |
|||
it(name, async () => { |
|||
const wrapper = mount( |
|||
<Form> |
|||
<Form.List name="list"> |
|||
{(fields, { add, remove }) => ( |
|||
<React.Fragment> |
|||
{fields.map(field => renderField(field))} |
|||
<Button className="add" onClick={add}> |
|||
Add |
|||
</Button> |
|||
<Button |
|||
className="remove" |
|||
onClick={() => { |
|||
remove(1); |
|||
}} |
|||
> |
|||
Remove |
|||
</Button> |
|||
</React.Fragment> |
|||
)} |
|||
</Form.List> |
|||
</Form>, |
|||
); |
|||
|
|||
async function operate(className) { |
|||
wrapper |
|||
.find(className) |
|||
.last() |
|||
.simulate('click'); |
|||
await delay(); |
|||
wrapper.update(); |
|||
} |
|||
|
|||
await operate('.add'); |
|||
expect(wrapper.find(Input).length).toBe(1); |
|||
|
|||
await operate('.add'); |
|||
expect(wrapper.find(Input).length).toBe(2); |
|||
|
|||
await change(wrapper, 1, ''); |
|||
expect(wrapper.find('.ant-form-item-explain').length).toBe(1); |
|||
|
|||
await operate('.remove'); |
|||
expect(wrapper.find(Input).length).toBe(1); |
|||
expect(wrapper.find('.ant-form-item-explain').length).toBe(0); |
|||
}); |
|||
} |
|||
|
|||
testList('operation correctly', field => ( |
|||
<Form.Item {...field} rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
)); |
|||
|
|||
testList('nest inline', field => ( |
|||
<Form.Item key={field.key}> |
|||
<Form.Item inline {...field} rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form.Item> |
|||
)); |
|||
}); |
|||
|
|||
it('inline Form.Item', async () => { |
|||
const onChange = jest.fn(); |
|||
|
|||
const wrapper = mount( |
|||
<Form> |
|||
<Form.Item> |
|||
<Form.Item name="test" rules={[{ required: true }]}> |
|||
<Input onChange={onChange} /> |
|||
</Form.Item> |
|||
</Form.Item> |
|||
</Form>, |
|||
); |
|||
|
|||
await change(wrapper, 0, ''); |
|||
expect(wrapper.find('.ant-form-item-explain').length).toBe(1); |
|||
|
|||
expect(onChange).toHaveBeenCalled(); |
|||
}); |
|||
|
|||
it('`shouldUpdate` should work with render props', () => { |
|||
mount( |
|||
<Form> |
|||
<Form.Item>{() => null}</Form.Item> |
|||
</Form>, |
|||
); |
|||
expect(errorSpy).toHaveBeenCalledWith( |
|||
'Warning: [antd: Form.Item] `children` of render props only work with `shouldUpdate`.', |
|||
); |
|||
}); |
|||
}); |
@ -1,91 +0,0 @@ |
|||
/* tslint:disable */ |
|||
import * as React from 'react'; |
|||
import Form, { FormComponentProps, FormCreateOption } from '../Form'; |
|||
|
|||
// test Form.create on component without own props
|
|||
class WithoutOwnProps extends React.Component<any, any> { |
|||
state = { |
|||
foo: 'bar', |
|||
}; |
|||
render() { |
|||
return <div>foo</div>; |
|||
} |
|||
} |
|||
|
|||
const WithoutOwnPropsForm = Form.create()(WithoutOwnProps); |
|||
|
|||
<WithoutOwnPropsForm />; |
|||
|
|||
// test Form.create on component with own props
|
|||
interface WithOwnPropsProps extends FormComponentProps { |
|||
name: string; |
|||
} |
|||
|
|||
class WithOwnProps extends React.Component<WithOwnPropsProps, any> { |
|||
state = { |
|||
foo: 'bar', |
|||
}; |
|||
|
|||
render() { |
|||
return <div>foo</div>; |
|||
} |
|||
} |
|||
|
|||
const WithOwnPropsForm = Form.create<WithOwnPropsProps>()(WithOwnProps); |
|||
|
|||
<WithOwnPropsForm name="foo" />; |
|||
|
|||
// test Form.create with options
|
|||
interface WithCreateOptionsProps extends FormComponentProps { |
|||
username: string; |
|||
} |
|||
|
|||
class WithCreateOptions extends React.Component<WithCreateOptionsProps, {}> { |
|||
render() { |
|||
return <div>foo</div>; |
|||
} |
|||
} |
|||
|
|||
const mapPropsToFields = (props: WithCreateOptionsProps) => { |
|||
const { username } = props; |
|||
|
|||
return { |
|||
username: Form.createFormField({ value: username }), |
|||
}; |
|||
}; |
|||
|
|||
const formOptions: FormCreateOption<WithCreateOptionsProps> = { mapPropsToFields }; |
|||
|
|||
const WithCreateOptionsForm = Form.create(formOptions)(WithCreateOptions); |
|||
|
|||
<WithCreateOptionsForm username="foo" />; |
|||
|
|||
// Should work with forwardRef & wrappedComponentRef
|
|||
// https://github.com/ant-design/ant-design/issues/16229
|
|||
if (React.forwardRef) { |
|||
interface ForwardProps extends FormComponentProps { |
|||
str: string; |
|||
} |
|||
|
|||
const ForwardDemo = React.forwardRef(({ str }: ForwardProps, ref: React.Ref<HTMLDivElement>) => { |
|||
return <div ref={ref}>{str || ''}</div>; |
|||
}); |
|||
const WrappedForwardDemo = Form.create<ForwardProps>()(ForwardDemo); |
|||
<WrappedForwardDemo str="" />; |
|||
} |
|||
|
|||
interface WrappedRefProps extends FormComponentProps { |
|||
str: string; |
|||
test?: () => void; |
|||
} |
|||
class WrapRefDemo extends React.Component<WrappedRefProps> { |
|||
public getForm() { |
|||
return this.props.form; |
|||
} |
|||
public render() { |
|||
return <div>{this.props.str || ''}</div>; |
|||
} |
|||
} |
|||
|
|||
const WrappedWrapRefDemo = Form.create<WrappedRefProps>()(WrapRefDemo); |
|||
<WrappedWrapRefDemo str="" wrappedComponentRef={() => null} />; |
@ -0,0 +1,5 @@ |
|||
# Form Dom 变化 |
|||
|
|||
- 状态 className 现在移动到顶层,不再是 input only |
|||
- 去除 `ant-form-item-control-wrapper` 一层 div |
|||
- `.has-success` 等状态样式添加 `ant-form-item` 前缀 |
@ -0,0 +1,47 @@ |
|||
import * as React from 'react'; |
|||
import omit from 'omit.js'; |
|||
import { FormProvider as RcFormProvider } from 'rc-field-form'; |
|||
import { FormProviderProps as RcFormProviderProps } from 'rc-field-form/lib/FormContext'; |
|||
import { ColProps } from '../grid/col'; |
|||
import { FormLabelAlign } from './interface'; |
|||
|
|||
/** |
|||
* Form Context |
|||
* Set top form style and pass to Form Item usage. |
|||
*/ |
|||
export interface FormContextProps { |
|||
vertical: boolean; |
|||
name?: string; |
|||
colon?: boolean; |
|||
labelAlign?: FormLabelAlign; |
|||
labelCol?: ColProps; |
|||
wrapperCol?: ColProps; |
|||
} |
|||
|
|||
export const FormContext = React.createContext<FormContextProps>({ |
|||
labelAlign: 'right', |
|||
vertical: false, |
|||
}); |
|||
|
|||
/** |
|||
* Form Item Context |
|||
* Used for Form inline Item error collection |
|||
*/ |
|||
export interface FormItemContextProps { |
|||
updateItemErrors: (name: string, errors: string[]) => void; |
|||
} |
|||
|
|||
export const FormItemContext = React.createContext<FormItemContextProps>({ |
|||
updateItemErrors: () => {}, |
|||
}); |
|||
|
|||
/** |
|||
* Form Provider |
|||
* |
|||
*/ |
|||
export interface FormProviderProps extends Omit<RcFormProviderProps, 'validateMessages'> {} |
|||
|
|||
export const FormProvider: React.FC<FormProviderProps> = props => { |
|||
const providerProps = omit(props, ['prefixCls']); |
|||
return <RcFormProvider {...providerProps} />; |
|||
}; |
@ -0,0 +1,129 @@ |
|||
--- |
|||
order: 13 |
|||
title: |
|||
zh-CN: 高级搜索 |
|||
en-US: Advanced search |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
三列栅格式的表单排列方式,常用于数据表格的高级搜索。 |
|||
|
|||
有部分定制的样式代码,由于输入标签长度不确定,需要根据具体情况自行调整。 |
|||
|
|||
## en-US |
|||
|
|||
Three columns layout is often used for advanced searching of data table. |
|||
|
|||
Because the width of label is not fixed, you may need to adjust it by customizing its style. |
|||
|
|||
```tsx |
|||
import { Form, Row, Col, Input, Button, Icon } from 'antd'; |
|||
|
|||
const AdvancedSearchForm = () => { |
|||
const [expand, setExpand] = React.useState(false); |
|||
const [form] = Form.useForm(); |
|||
|
|||
const getFields = () => { |
|||
const count = expand ? 10 : 6; |
|||
const children = []; |
|||
for (let i = 0; i < count; i++) { |
|||
children.push( |
|||
<Col span={8} key={i}> |
|||
<Form.Item |
|||
name={`field-${i}`} |
|||
label={`Field ${i}`} |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: 'Input something!', |
|||
}, |
|||
]} |
|||
> |
|||
<Input placeholder="placeholder" /> |
|||
</Form.Item> |
|||
</Col>, |
|||
); |
|||
} |
|||
return children; |
|||
}; |
|||
|
|||
const onFinish = values => { |
|||
console.log('Received values of form: ', values); |
|||
}; |
|||
|
|||
return ( |
|||
<Form |
|||
form={form} |
|||
name="advanced_search" |
|||
className="ant-advanced-search-form" |
|||
onFinish={onFinish} |
|||
> |
|||
<Row gutter={24}>{getFields()}</Row> |
|||
<Row> |
|||
<Col span={24} style={{ textAlign: 'right' }}> |
|||
<Button type="primary" htmlType="submit"> |
|||
Search |
|||
</Button> |
|||
<Button |
|||
style={{ marginLeft: 8 }} |
|||
onClick={() => { |
|||
form.resetFields(); |
|||
}} |
|||
> |
|||
Clear |
|||
</Button> |
|||
<a |
|||
style={{ marginLeft: 8, fontSize: 12 }} |
|||
onClick={() => { |
|||
setExpand(!expand); |
|||
}} |
|||
> |
|||
Collapse <Icon type={expand ? 'up' : 'down'} /> |
|||
</a> |
|||
</Col> |
|||
</Row> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render( |
|||
<div> |
|||
<AdvancedSearchForm /> |
|||
<div className="search-result-list">Search Result List</div> |
|||
</div>, |
|||
mountNode, |
|||
); |
|||
``` |
|||
|
|||
```css |
|||
.ant-advanced-search-form { |
|||
padding: 24px; |
|||
background: #fbfbfb; |
|||
border: 1px solid #d9d9d9; |
|||
border-radius: 6px; |
|||
} |
|||
|
|||
.ant-advanced-search-form .ant-form-item { |
|||
display: flex; |
|||
} |
|||
|
|||
.ant-advanced-search-form .ant-form-item-control-wrapper { |
|||
flex: 1; |
|||
} |
|||
``` |
|||
|
|||
<style> |
|||
#components-form-demo-advanced-search .ant-form { |
|||
max-width: none; |
|||
} |
|||
#components-form-demo-advanced-search .search-result-list { |
|||
margin-top: 16px; |
|||
border: 1px dashed #e9e9e9; |
|||
border-radius: 6px; |
|||
background-color: #fafafa; |
|||
min-height: 200px; |
|||
text-align: center; |
|||
padding-top: 80px; |
|||
} |
|||
</style> |
@ -0,0 +1,66 @@ |
|||
--- |
|||
order: 0 |
|||
title: |
|||
zh-CN: 基本使用 |
|||
en-US: Basic Usage |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
基本的表单数据域控制展示,包含布局、初始化、验证、提交。 |
|||
|
|||
## en-US |
|||
|
|||
Basic Form data control. Includes layout, initial values, validation and submit. |
|||
|
|||
```tsx |
|||
import { Form, Input, Button, Checkbox } from 'antd'; |
|||
|
|||
const layout = { |
|||
labelCol: { span: 8 }, |
|||
wrapperCol: { span: 16 }, |
|||
}; |
|||
const tailLayout = { |
|||
wrapperCol: { offset: 8, span: 16 }, |
|||
}; |
|||
|
|||
const Demo = () => { |
|||
const onFinish = values => { |
|||
console.log('Success:', values); |
|||
}; |
|||
|
|||
const onFinishFailed = errorInfo => { |
|||
console.log('Failed:', errorInfo); |
|||
}; |
|||
|
|||
return ( |
|||
<Form |
|||
{...layout} |
|||
name="basic" |
|||
initialValues={{ remember: true }} |
|||
onFinish={onFinish} |
|||
onFinishFailed={onFinishFailed} |
|||
> |
|||
<Form.Item label="Username" name="username" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="Password" name="password" rules={[{ required: true }]}> |
|||
<Input.Password /> |
|||
</Form.Item> |
|||
|
|||
<Form.Item {...tailLayout} name="remember" valuePropName="checked"> |
|||
<Checkbox>Remember me</Checkbox> |
|||
</Form.Item> |
|||
|
|||
<Form.Item {...tailLayout}> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<Demo />, mountNode); |
|||
``` |
@ -0,0 +1,103 @@ |
|||
--- |
|||
order: 1 |
|||
title: |
|||
zh-CN: 表单方法调用 |
|||
en-US: Form methods |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
通过 `Form.useForm` 对表单数据域进行交互。 |
|||
|
|||
## en-US |
|||
|
|||
Call form method with `Form.useForm`. |
|||
|
|||
```tsx |
|||
import { Form, Input, Button, Select } from 'antd'; |
|||
|
|||
const { Option } = Select; |
|||
|
|||
const layout = { |
|||
labelCol: { span: 8 }, |
|||
wrapperCol: { span: 16 }, |
|||
}; |
|||
const tailLayout = { |
|||
wrapperCol: { offset: 8, span: 16 }, |
|||
}; |
|||
|
|||
const Demo = () => { |
|||
const [form] = Form.useForm(); |
|||
|
|||
const onGenderChange = value => { |
|||
form.setFieldsValue({ |
|||
note: `Hi, ${value === 'male' ? 'man' : 'lady'}!`, |
|||
}); |
|||
}; |
|||
|
|||
const onFinish = values => { |
|||
console.log(values); |
|||
}; |
|||
|
|||
const onReset = () => { |
|||
form.resetFields(); |
|||
}; |
|||
|
|||
const onFill = () => { |
|||
form.setFieldsValue({ |
|||
note: 'Hello world!', |
|||
gender: 'male', |
|||
}); |
|||
}; |
|||
|
|||
return ( |
|||
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}> |
|||
<Form.Item name="note" label="Note" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
<Form.Item name="gender" label="Gender" rules={[{ required: true }]}> |
|||
<Select |
|||
placeholder="Select a option and change input text above" |
|||
onChange={onGenderChange} |
|||
allowClear |
|||
> |
|||
<Option value="male">male</Option> |
|||
<Option value="female">female</Option> |
|||
<Option value="other">other</Option> |
|||
</Select> |
|||
</Form.Item> |
|||
<Form.Item |
|||
inline |
|||
shouldUpdate={(prevValues, currentValues) => prevValues.gender !== currentValues.gender} |
|||
> |
|||
{({ getFieldValue }) => { |
|||
return getFieldValue('gender') === 'other' ? ( |
|||
<Form.Item name="customizeGender" label="Customize Gender" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
) : null; |
|||
}} |
|||
</Form.Item> |
|||
<Form.Item {...tailLayout}> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
<Button htmlType="button" onClick={onReset}> |
|||
Reset |
|||
</Button> |
|||
<Button type="link" htmlType="button" onClick={onFill}> |
|||
Fill form |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<Demo />, mountNode); |
|||
``` |
|||
|
|||
```css |
|||
#components-form-demo-control-hooks .ant-btn { |
|||
margin-right: 8px; |
|||
} |
|||
``` |
@ -0,0 +1,109 @@ |
|||
--- |
|||
order: 2 |
|||
title: |
|||
zh-CN: 表单方法调用(Class component) |
|||
en-US: Form methods (Class component) |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
我们推荐使用 `Form.useForm` 创建表单数据域进行控制。如果是在 class component 下,你也可以通过 `ref` 获取数据域。 |
|||
|
|||
## en-US |
|||
|
|||
We recommend use `Form.useForm` to create data control. If you are using class component, you can get it by `ref`. |
|||
|
|||
```tsx |
|||
import { Form, Input, Button, Select } from 'antd'; |
|||
|
|||
const { Option } = Select; |
|||
|
|||
const layout = { |
|||
labelCol: { span: 8 }, |
|||
wrapperCol: { span: 16 }, |
|||
}; |
|||
const tailLayout = { |
|||
wrapperCol: { offset: 8, span: 16 }, |
|||
}; |
|||
|
|||
class Demo extends React.Component { |
|||
formRef = React.createRef(); |
|||
|
|||
onGenderChange = value => { |
|||
this.formRef.current.setFieldsValue({ |
|||
note: `Hi, ${value === 'male' ? 'man' : 'lady'}!`, |
|||
}); |
|||
}; |
|||
|
|||
onFinish = values => { |
|||
console.log(values); |
|||
}; |
|||
|
|||
onReset = () => { |
|||
this.formRef.current.resetFields(); |
|||
}; |
|||
|
|||
onFill = () => { |
|||
this.formRef.current.setFieldsValue({ |
|||
note: 'Hello world!', |
|||
gender: 'male', |
|||
}); |
|||
}; |
|||
|
|||
render() { |
|||
return ( |
|||
<Form {...layout} ref={this.formRef} name="control-ref" onFinish={this.onFinish}> |
|||
<Form.Item name="note" label="Note" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
<Form.Item name="gender" label="Gender" rules={[{ required: true }]}> |
|||
<Select |
|||
placeholder="Select a option and change input text above" |
|||
onChange={this.onGenderChange} |
|||
allowClear |
|||
> |
|||
<Option value="male">male</Option> |
|||
<Option value="female">female</Option> |
|||
<Option value="other">other</Option> |
|||
</Select> |
|||
</Form.Item> |
|||
<Form.Item |
|||
inline |
|||
shouldUpdate={(prevValues, currentValues) => prevValues.gender !== currentValues.gender} |
|||
> |
|||
{({ getFieldValue }) => { |
|||
return getFieldValue('gender') === 'other' ? ( |
|||
<Form.Item |
|||
name="customizeGender" |
|||
label="Customize Gender" |
|||
rules={[{ required: true }]} |
|||
> |
|||
<Input /> |
|||
</Form.Item> |
|||
) : null; |
|||
}} |
|||
</Form.Item> |
|||
<Form.Item {...tailLayout}> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
<Button htmlType="button" onClick={this.onReset}> |
|||
Reset |
|||
</Button> |
|||
<Button type="link" htmlType="button" onClick={this.onFill}> |
|||
Fill form |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
} |
|||
} |
|||
|
|||
ReactDOM.render(<Demo />, mountNode); |
|||
``` |
|||
|
|||
```css |
|||
#components-form-demo-control-ref .ant-btn { |
|||
margin-right: 8px; |
|||
} |
|||
``` |
@ -0,0 +1,118 @@ |
|||
--- |
|||
order: 6 |
|||
title: |
|||
zh-CN: 自定义表单控件 |
|||
en-US: Customized Form Controls |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
自定义或第三方的表单控件,也可以与 Form 组件一起使用。只要该组件遵循以下的约定: |
|||
|
|||
> - 提供受控属性 `value` 或其它与 [`valuePropName`](http://ant.design/components/form/#getFieldDecorator-参数) 的值同名的属性。 |
|||
> - 提供 `onChange` 事件或 [`trigger`](http://ant.design/components/form/#getFieldDecorator-参数) 的值同名的事件。 |
|||
|
|||
## en-US |
|||
|
|||
Customized or third-party form controls can be used in Form, too. Controls must follow these conventions: |
|||
|
|||
> - It has a controlled property `value` or other name which is equal to the value of [`valuePropName`](http://ant.design/components/form/?locale=en-US#getFieldDecorator's-parameters). |
|||
> - It has event `onChange` or an event which name is equal to the value of [`trigger`](http://ant.design/components/form/?locale=en-US#getFieldDecorator's-parameters). |
|||
|
|||
```tsx |
|||
import { Form, Input, Select, Button } from 'antd'; |
|||
|
|||
const { Option } = Select; |
|||
|
|||
interface PriceValue { |
|||
number?: number; |
|||
currency?: 'rmb' | 'dollar'; |
|||
} |
|||
|
|||
interface PriceInputProps { |
|||
value?: PriceValue; |
|||
onChange?: (value: PriceValue) => void; |
|||
} |
|||
|
|||
const PriceInput: React.FC<PriceInputProps> = ({ value = {}, onChange }) => { |
|||
const [number, setNumber] = React.useState(0); |
|||
const [currency, setCurrency] = React.useState('rmb'); |
|||
|
|||
const triggerChange = changedValue => { |
|||
if (onChange) { |
|||
onChange({ number, currency, ...value, ...changedValue }); |
|||
} |
|||
}; |
|||
|
|||
const onNumberChange = e => { |
|||
const newNumber = parseInt(e.target.value || 0, 10); |
|||
if (Number.isNaN(number)) { |
|||
return; |
|||
} |
|||
if (!('number' in value)) { |
|||
setNumber(newNumber); |
|||
} |
|||
triggerChange({ number: newNumber }); |
|||
}; |
|||
|
|||
const onCurrencyChange = newCurrency => { |
|||
if (!('currency' in value)) { |
|||
setCurrency(newCurrency); |
|||
} |
|||
triggerChange({ currency: newCurrency }); |
|||
}; |
|||
|
|||
return ( |
|||
<span> |
|||
<Input |
|||
type="text" |
|||
value={value.number || number} |
|||
onChange={onNumberChange} |
|||
style={{ width: 100, marginRight: 8 }} |
|||
/> |
|||
<Select value={value.currency || currency} style={{ width: 80 }} onChange={onCurrencyChange}> |
|||
<Option value="rmb">RMB</Option> |
|||
<Option value="dollar">Dollar</Option> |
|||
</Select> |
|||
</span> |
|||
); |
|||
}; |
|||
|
|||
const Demo = () => { |
|||
const onFinish = values => { |
|||
console.log('Received values of form: ', values); |
|||
}; |
|||
|
|||
const checkPrice = (rule, value) => { |
|||
if (value.number > 0) { |
|||
return Promise.resolve(); |
|||
} |
|||
return Promise.reject('Price must greater than zero!'); |
|||
}; |
|||
|
|||
return ( |
|||
<Form |
|||
name="customized_form_controls" |
|||
layout="inline" |
|||
onFinish={onFinish} |
|||
initialValues={{ |
|||
price: { |
|||
number: 0, |
|||
currency: 'rmb', |
|||
}, |
|||
}} |
|||
> |
|||
<Form.Item name="price" label="Price" rules={[{ validator: checkPrice }]}> |
|||
<PriceInput /> |
|||
</Form.Item> |
|||
<Form.Item> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<Demo />, mountNode); |
|||
``` |
@ -0,0 +1,123 @@ |
|||
--- |
|||
order: 4 |
|||
title: |
|||
zh-CN: 动态增减表单项 |
|||
en-US: Dynamic Form Item |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
动态增加、减少表单项。 |
|||
|
|||
## en-US |
|||
|
|||
Add or remove form items dynamically. |
|||
|
|||
```jsx |
|||
import { Form, Input, Icon, Button } from 'antd'; |
|||
|
|||
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 }, |
|||
}, |
|||
}; |
|||
|
|||
const DynamicFieldSet = () => { |
|||
const onFinish = values => { |
|||
console.log('Received values of form:', values); |
|||
}; |
|||
|
|||
return ( |
|||
<Form name="dynamic_form_item" {...formItemLayoutWithOutLabel} onFinish={onFinish}> |
|||
<Form.List name="names"> |
|||
{(fields, { add, remove }) => { |
|||
return ( |
|||
<div> |
|||
{fields.map((field, index) => ( |
|||
<Form.Item |
|||
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)} |
|||
label={index === 0 ? 'Passengers' : ''} |
|||
required={false} |
|||
key={field.key} |
|||
> |
|||
<Form.Item |
|||
{...field} |
|||
validateTrigger={['onChange', 'onBlur']} |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
whitespace: true, |
|||
message: "Please input passenger's name or delete this field.", |
|||
}, |
|||
]} |
|||
inline |
|||
> |
|||
<Input placeholder="passenger name" style={{ width: '60%', marginRight: 8 }} /> |
|||
</Form.Item> |
|||
{fields.length > 1 ? ( |
|||
<Icon |
|||
className="dynamic-delete-button" |
|||
type="minus-circle-o" |
|||
onClick={() => { |
|||
remove(field.name); |
|||
}} |
|||
/> |
|||
) : null} |
|||
</Form.Item> |
|||
))} |
|||
<Form.Item> |
|||
<Button |
|||
type="dashed" |
|||
onClick={() => { |
|||
add(); |
|||
}} |
|||
style={{ width: '60%' }} |
|||
> |
|||
<Icon type="plus" /> Add field |
|||
</Button> |
|||
</Form.Item> |
|||
</div> |
|||
); |
|||
}} |
|||
</Form.List> |
|||
|
|||
<Form.Item> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<DynamicFieldSet />, mountNode); |
|||
``` |
|||
|
|||
```css |
|||
.dynamic-delete-button { |
|||
cursor: pointer; |
|||
position: relative; |
|||
top: 4px; |
|||
font-size: 24px; |
|||
color: #999; |
|||
transition: all 0.3s; |
|||
} |
|||
.dynamic-delete-button:hover { |
|||
color: #777; |
|||
} |
|||
.dynamic-delete-button[disabled] { |
|||
cursor: not-allowed; |
|||
opacity: 0.5; |
|||
} |
|||
``` |
@ -0,0 +1,92 @@ |
|||
--- |
|||
order: 23 |
|||
title: |
|||
zh-CN: 动态校验规则 |
|||
en-US: Dynamic Rules |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
根据不同情况执行不同的校验规则。 |
|||
|
|||
## en-US |
|||
|
|||
Perform different check rules according to different situations. |
|||
|
|||
```tsx |
|||
import { Form, Input, Button, Checkbox } from 'antd'; |
|||
|
|||
const formItemLayout = { |
|||
labelCol: { span: 4 }, |
|||
wrapperCol: { span: 8 }, |
|||
}; |
|||
const formTailLayout = { |
|||
labelCol: { span: 4 }, |
|||
wrapperCol: { span: 8, offset: 4 }, |
|||
}; |
|||
|
|||
const DynamicRule = () => { |
|||
const [form] = Form.useForm(); |
|||
const [checkNick, setCheckNick] = React.useState(false); |
|||
|
|||
React.useEffect(() => { |
|||
form.validateFields(['nickname']); |
|||
}, [checkNick]); |
|||
|
|||
const onCheckboxChange = e => { |
|||
setCheckNick(e.target.checked); |
|||
}; |
|||
|
|||
const onCheck = async () => { |
|||
try { |
|||
const values = await form.validateFields(); |
|||
console.log('Success:', values); |
|||
} catch (errorInfo) { |
|||
console.log('Failed:', errorInfo); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<Form form={form} name="dynamic_rule"> |
|||
<Form.Item |
|||
{...formItemLayout} |
|||
name="username" |
|||
label="Name" |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: 'Please input your name', |
|||
}, |
|||
]} |
|||
> |
|||
<Input placeholder="Please input your name" /> |
|||
</Form.Item> |
|||
<Form.Item |
|||
{...formItemLayout} |
|||
name="nickname" |
|||
label="Nickname" |
|||
rules={[ |
|||
{ |
|||
required: checkNick, |
|||
message: 'Please input your nickname', |
|||
}, |
|||
]} |
|||
> |
|||
<Input placeholder="Please input your nickname" /> |
|||
</Form.Item> |
|||
<Form.Item {...formTailLayout}> |
|||
<Checkbox checked={checkNick} onChange={onCheckboxChange}> |
|||
Nickname is required |
|||
</Checkbox> |
|||
</Form.Item> |
|||
<Form.Item {...formTailLayout}> |
|||
<Button type="primary" onClick={onCheck}> |
|||
Check |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<DynamicRule />, mountNode); |
|||
``` |
@ -0,0 +1,137 @@ |
|||
--- |
|||
order: 8 |
|||
title: |
|||
zh-CN: 多表单联动 |
|||
en-US: Control between forms |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
通过 `Form.Provider` 在表单间处理数据。本例子中,Modal 的确认按钮在 Form 之外,通过 `form.submit` 方法调用表单提交功能。反之,则推荐使用 `<Button htmlType="submit" />` 调用 web 原生提交逻辑。 |
|||
|
|||
## en-US |
|||
|
|||
Use `Form.Provider` to process data between forms. In this case, submit button is in the Modal which is out of Form. You can use `form.submit` to submit form. Besides, we recommend native `<Button htmlType="submit" />` to submit a form. |
|||
|
|||
```tsx |
|||
import { Form, Input, InputNumber, Modal, Icon, Button, Avatar, Typography } from 'antd'; |
|||
|
|||
const layout = { |
|||
labelCol: { span: 8 }, |
|||
wrapperCol: { span: 16 }, |
|||
}; |
|||
const tailLayout = { |
|||
wrapperCol: { offset: 8, span: 16 }, |
|||
}; |
|||
|
|||
interface ModalFormProps { |
|||
visible: boolean; |
|||
onCancel: () => void; |
|||
} |
|||
|
|||
const ModalForm: React.FC<ModalFormProps> = ({ visible, onCancel }) => { |
|||
const [form] = Form.useForm(); |
|||
|
|||
React.useEffect(() => { |
|||
form.resetFields(); |
|||
}, [visible]); |
|||
|
|||
const onOk = () => { |
|||
form.submit(); |
|||
}; |
|||
|
|||
return ( |
|||
<Modal title="Basic Drawer" visible={visible} onOk={onOk} onCancel={onCancel}> |
|||
<Form form={form} layout="vertical" name="userForm"> |
|||
<Form.Item name="name" label="User Name" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
<Form.Item name="age" label="User Age" rules={[{ required: true }]}> |
|||
<InputNumber /> |
|||
</Form.Item> |
|||
</Form> |
|||
</Modal> |
|||
); |
|||
}; |
|||
|
|||
const Demo = () => { |
|||
const [visible, setVisible] = React.useState(false); |
|||
|
|||
const showUserModal = () => { |
|||
setVisible(true); |
|||
}; |
|||
|
|||
const hideUserModal = () => { |
|||
setVisible(false); |
|||
}; |
|||
|
|||
const onFinish = values => { |
|||
console.log('Finish:', values); |
|||
}; |
|||
|
|||
return ( |
|||
<div> |
|||
<Form.Provider |
|||
onFormFinish={(name, { values, forms }) => { |
|||
if (name === 'userForm') { |
|||
const { basicForm } = forms; |
|||
const users = basicForm.getFieldValue('users') || []; |
|||
basicForm.setFieldsValue({ users: [...users, values] }); |
|||
setVisible(false); |
|||
} |
|||
}} |
|||
> |
|||
<Form {...layout} name="basicForm" onFinish={onFinish}> |
|||
<Form.Item name="group" label="Group Name" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
<Form.Item |
|||
label="User List" |
|||
shouldUpdate={(prevValues, curValues) => prevValues.users !== curValues.users} |
|||
> |
|||
{({ getFieldValue }) => { |
|||
const users = getFieldValue('users') || []; |
|||
return users.length ? ( |
|||
<ul> |
|||
{users.map((user, index) => ( |
|||
<li key={index} className="user"> |
|||
<Avatar icon="user" /> |
|||
{user.name} - {user.age} |
|||
</li> |
|||
))} |
|||
</ul> |
|||
) : ( |
|||
<Typography.Text className="ant-form-text" type="secondary"> |
|||
( <Icon type="smile" /> No user yet. ) |
|||
</Typography.Text> |
|||
); |
|||
}} |
|||
</Form.Item> |
|||
<Form.Item {...tailLayout}> |
|||
<Button htmlType="submit" type="primary"> |
|||
Submit |
|||
</Button> |
|||
<Button htmlType="button" style={{ marginLeft: 8 }} onClick={showUserModal}> |
|||
Add User |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
|
|||
<ModalForm visible={visible} onCancel={hideUserModal} /> |
|||
</Form.Provider> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<Demo />, mountNode); |
|||
``` |
|||
|
|||
```css |
|||
#components-form-demo-form-context .user { |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
#components-form-demo-form-context .user .ant-avatar { |
|||
margin-right: 8px; |
|||
} |
|||
``` |
@ -0,0 +1,119 @@ |
|||
--- |
|||
order: 14 |
|||
title: |
|||
zh-CN: 弹出层中的新建表单 |
|||
en-US: Form in Modal to Create |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
当用户访问一个展示了某个列表的页面,想新建一项但又不想跳转页面时,可以用 Modal 弹出一个表单,用户填写必要信息后创建新的项。 |
|||
|
|||
## en-US |
|||
|
|||
When user visit a page with a list of items, and want to create a new item. The page can popup a form in Modal, then let user fill in the form to create an item. |
|||
|
|||
```tsx |
|||
import { Button, Modal, Form, Input, Radio } from 'antd'; |
|||
|
|||
interface Values { |
|||
title: string; |
|||
description: string; |
|||
modifier: string; |
|||
} |
|||
|
|||
interface CollectionCreateFormProps { |
|||
visible: boolean; |
|||
onCreate: (values: Values) => void; |
|||
onCancel: () => void; |
|||
} |
|||
|
|||
const CollectionCreateForm: React.FC<CollectionCreateFormProps> = ({ |
|||
visible, |
|||
onCreate, |
|||
onCancel, |
|||
}) => { |
|||
const [form] = Form.useForm(); |
|||
return ( |
|||
<Modal |
|||
visible={visible} |
|||
title="Create a new collection" |
|||
okText="Create" |
|||
cancelText="Cancel" |
|||
onCancel={onCancel} |
|||
onOk={() => { |
|||
form |
|||
.validateFields() |
|||
.then(values => { |
|||
form.resetFields(); |
|||
onCreate(values); |
|||
}) |
|||
.catch(info => { |
|||
console.log('Validate Failed:', info); |
|||
}); |
|||
}} |
|||
> |
|||
<Form |
|||
form={form} |
|||
layout="vertical" |
|||
name="form_in_modal" |
|||
initialValues={{ modifier: 'public' }} |
|||
> |
|||
<Form.Item |
|||
name="title" |
|||
label="Title" |
|||
rules={[{ required: true, message: 'Please input the title of collection!' }]} |
|||
> |
|||
<Input /> |
|||
</Form.Item> |
|||
<Form.Item name="description" label="Description"> |
|||
<Input type="textarea" /> |
|||
</Form.Item> |
|||
<Form.Item name="modifier" className="collection-create-form_last-form-item"> |
|||
<Radio.Group> |
|||
<Radio value="public">Public</Radio> |
|||
<Radio value="private">Private</Radio> |
|||
</Radio.Group> |
|||
</Form.Item> |
|||
</Form> |
|||
</Modal> |
|||
); |
|||
}; |
|||
|
|||
const CollectionsPage = () => { |
|||
const [visible, setVisible] = React.useState(false); |
|||
|
|||
const onCreate = values => { |
|||
console.log('Received values of form: ', values); |
|||
setVisible(false); |
|||
}; |
|||
|
|||
return ( |
|||
<div> |
|||
<Button |
|||
type="primary" |
|||
onClick={() => { |
|||
setVisible(true); |
|||
}} |
|||
> |
|||
New Collection |
|||
</Button> |
|||
<CollectionCreateForm |
|||
visible={visible} |
|||
onCreate={onCreate} |
|||
onCancel={() => { |
|||
setVisible(false); |
|||
}} |
|||
/> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<CollectionsPage />, mountNode); |
|||
``` |
|||
|
|||
```css |
|||
.collection-create-form_last-form-item { |
|||
margin-bottom: 0; |
|||
} |
|||
``` |
@ -0,0 +1,82 @@ |
|||
--- |
|||
order: 7 |
|||
title: |
|||
zh-CN: 表单数据存储于上层组件 |
|||
en-US: Store Form Data into Upper Component |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
通过 `onFieldsChange` 和 `fields`,可以把表单的数据存储到上层组件或者 [Redux](https://github.com/reactjs/redux)、[dva](https://github.com/dvajs/dva) 中,更多可参考 [rc-field-form 示例](https://rc-field-form.react-component.now.sh/?selectedKind=rc-field-form&selectedStory=StateForm-redux&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybook%2Factions%2Factions-panel)。 |
|||
|
|||
**注意:** 将表单数据存储于外部容器[并非好的实践](https://github.com/reduxjs/redux/issues/1287#issuecomment-175351978),如无必要请避免使用。 |
|||
|
|||
## en-US |
|||
|
|||
We can store form data into upper component or [Redux](https://github.com/reactjs/redux) or [dva](https://github.com/dvajs/dva) by using `onFieldsChange` and `fields`, see more at this [rc-field-form demo](https://rc-field-form.react-component.now.sh/?selectedKind=rc-field-form&selectedStory=StateForm-redux&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybook%2Factions%2Factions-panel). |
|||
|
|||
**Note:** Save Form data globally [is not a good practice](https://github.com/reduxjs/redux/issues/1287#issuecomment-175351978). You should avoid this if not necessary. |
|||
|
|||
```tsx |
|||
import { Form, Input } from 'antd'; |
|||
|
|||
interface FieldData { |
|||
name: string[]; |
|||
value: any; |
|||
touched: boolean; |
|||
validating: boolean; |
|||
errors: string[]; |
|||
} |
|||
|
|||
interface CustomizedFormProps { |
|||
onChange: (fields: FieldData[]) => void; |
|||
fields: FieldData[]; |
|||
} |
|||
|
|||
const CustomizedForm: React.FC<CustomizedFormProps> = ({ onChange, fields }) => { |
|||
return ( |
|||
<Form |
|||
name="global_state" |
|||
layout="inline" |
|||
fields={fields} |
|||
onFieldsChange={(changedFields, allFields) => { |
|||
onChange(allFields); |
|||
}} |
|||
> |
|||
<Form.Item |
|||
name="username" |
|||
label="Username" |
|||
rules={[{ required: true, message: 'Username is required!' }]} |
|||
> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
const Demo = () => { |
|||
const [fields, setFields] = React.useState([{ name: ['username'], value: 'Ant Design' }]); |
|||
|
|||
return ( |
|||
<div> |
|||
<CustomizedForm |
|||
fields={fields} |
|||
onChange={newFields => { |
|||
setFields(newFields); |
|||
}} |
|||
/> |
|||
<pre className="language-bash">{JSON.stringify(fields, null, 2)}</pre> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<Demo />, mountNode); |
|||
``` |
|||
|
|||
<style> |
|||
#components-form-demo-global-state .language-bash { |
|||
max-width: 400px; |
|||
border-radius: 6px; |
|||
margin-top: 24px; |
|||
} |
|||
</style> |
@ -0,0 +1,65 @@ |
|||
--- |
|||
order: 3 |
|||
title: |
|||
zh-CN: 表单布局 |
|||
en-US: Form Layout |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
表单有三种布局。 |
|||
|
|||
## en-US |
|||
|
|||
There are three layout for form: `horizontal`, `vertical`, `inline`. |
|||
|
|||
```tsx |
|||
import { Form, Input, Button, Radio } from 'antd'; |
|||
|
|||
const FormLayoutDemo = () => { |
|||
const [formLayout, setFormLayout] = React.useState('horizontal'); |
|||
|
|||
const onFormLayoutChange = e => { |
|||
setFormLayout(e.target.value); |
|||
}; |
|||
|
|||
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}> |
|||
<Form.Item label="Form Layout" {...formItemLayout}> |
|||
<Radio.Group defaultValue="horizontal" onChange={onFormLayoutChange}> |
|||
<Radio.Button value="horizontal">Horizontal</Radio.Button> |
|||
<Radio.Button value="vertical">Vertical</Radio.Button> |
|||
<Radio.Button value="inline">Inline</Radio.Button> |
|||
</Radio.Group> |
|||
</Form.Item> |
|||
<Form.Item label="Field A" {...formItemLayout}> |
|||
<Input placeholder="input placeholder" /> |
|||
</Form.Item> |
|||
<Form.Item label="Field B" {...formItemLayout}> |
|||
<Input placeholder="input placeholder" /> |
|||
</Form.Item> |
|||
<Form.Item {...buttonItemLayout}> |
|||
<Button type="primary">Submit</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<FormLayoutDemo />, mountNode); |
|||
``` |
@ -0,0 +1,67 @@ |
|||
--- |
|||
order: 5 |
|||
title: |
|||
zh-CN: 嵌套结构与校验信息 |
|||
en-US: Nest |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
`name` 属性支持嵌套数据结构。通过 `validateMessages` 或 `message` 自定义校验信息模板,模板内容可参考[此处](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts)。 |
|||
|
|||
## en-US |
|||
|
|||
`name` prop support nest data structure. Customize validate message template with `validateMessages` or `message`. Ref [here](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) about message template. |
|||
|
|||
```tsx |
|||
import { Form, Input, InputNumber, Button } from 'antd'; |
|||
|
|||
const layout = { |
|||
labelCol: { span: 8 }, |
|||
wrapperCol: { span: 16 }, |
|||
}; |
|||
|
|||
const validateMessages = { |
|||
required: 'This field is required!', |
|||
types: { |
|||
email: 'Not a validate email!', |
|||
number: 'Not a validate number!', |
|||
}, |
|||
number: { |
|||
range: 'Must be between ${min} and ${max}', |
|||
}, |
|||
}; |
|||
|
|||
const Demo = () => { |
|||
const onFinish = values => { |
|||
console.log(values); |
|||
}; |
|||
|
|||
return ( |
|||
<Form {...layout} name="nest-messages" onFinish={onFinish} validateMessages={validateMessages}> |
|||
<Form.Item name={['user', 'name']} label="Name" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
<Form.Item name={['user', 'email']} label="Email" rules={[{ type: 'email' }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
<Form.Item name={['user', 'age']} label="Age" rules={[{ type: 'number', min: 0, max: 99 }]}> |
|||
<InputNumber /> |
|||
</Form.Item> |
|||
<Form.Item name={['user', 'website']} label="Website"> |
|||
<Input /> |
|||
</Form.Item> |
|||
<Form.Item name={['user', 'introduction']} label="Introduction"> |
|||
<Input.TextArea /> |
|||
</Form.Item> |
|||
<Form.Item wrapperCol={{ ...layout.wrapperCol, offset: 8 }}> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<Demo />, mountNode); |
|||
``` |
@ -0,0 +1,83 @@ |
|||
--- |
|||
order: 11 |
|||
title: |
|||
zh-CN: 登录框 |
|||
en-US: Login Form |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
普通的登录框,可以容纳更多的元素。 |
|||
|
|||
## en-US |
|||
|
|||
Normal login form which can contain more elements. |
|||
|
|||
```tsx |
|||
import { Form, Icon, Input, Button, Checkbox } from 'antd'; |
|||
|
|||
const NormalLoginForm = () => { |
|||
const onFinish = values => { |
|||
console.log('Received values of form: ', values); |
|||
}; |
|||
|
|||
return ( |
|||
<Form |
|||
name="normal_login" |
|||
className="login-form" |
|||
initialValues={{ remember: true }} |
|||
onFinish={onFinish} |
|||
> |
|||
<Form.Item |
|||
name="username" |
|||
rules={[{ required: true, message: 'Please input your Username!' }]} |
|||
> |
|||
<Input |
|||
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} |
|||
placeholder="Username" |
|||
/> |
|||
</Form.Item> |
|||
<Form.Item |
|||
name="password" |
|||
rules={[{ required: true, message: 'Please input your Password!' }]} |
|||
> |
|||
<Input |
|||
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} |
|||
type="password" |
|||
placeholder="Password" |
|||
/> |
|||
</Form.Item> |
|||
<Form.Item> |
|||
<Form.Item name="remember" valuePropName="checked" inline> |
|||
<Checkbox>Remember me</Checkbox> |
|||
</Form.Item> |
|||
|
|||
<a className="login-form-forgot" href=""> |
|||
Forgot password |
|||
</a> |
|||
</Form.Item> |
|||
|
|||
<Form.Item> |
|||
<Button type="primary" htmlType="submit" className="login-form-button"> |
|||
Log in |
|||
</Button> |
|||
Or <a href="">register now!</a> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<NormalLoginForm />, mountNode); |
|||
``` |
|||
|
|||
```css |
|||
#components-form-demo-normal-login .login-form { |
|||
max-width: 300px; |
|||
} |
|||
#components-form-demo-normal-login .login-form-forgot { |
|||
float: right; |
|||
} |
|||
#components-form-demo-normal-login .login-form-button { |
|||
width: 100%; |
|||
} |
|||
``` |
@ -0,0 +1,259 @@ |
|||
--- |
|||
order: 12 |
|||
title: |
|||
zh-CN: 注册新用户 |
|||
en-US: Registration |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
用户填写必须的信息以注册新用户。 |
|||
|
|||
## en-US |
|||
|
|||
Fill in this form to create a new account for you. |
|||
|
|||
```tsx |
|||
import { |
|||
Form, |
|||
Input, |
|||
Tooltip, |
|||
Icon, |
|||
Cascader, |
|||
Select, |
|||
Row, |
|||
Col, |
|||
Checkbox, |
|||
Button, |
|||
AutoComplete, |
|||
} from 'antd'; |
|||
|
|||
const { Option } = Select; |
|||
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', |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
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 RegistrationForm = () => { |
|||
const onFinish = values => { |
|||
console.log('Received values of form: ', values); |
|||
}; |
|||
|
|||
const prefixSelector = ( |
|||
<Form.Item name="prefix" inline> |
|||
<Select style={{ width: 70 }}> |
|||
<Option value="86">+86</Option> |
|||
<Option value="87">+87</Option> |
|||
</Select> |
|||
</Form.Item> |
|||
); |
|||
|
|||
const [autoCompleteResult, setAutoCompleteResult] = React.useState([]); |
|||
|
|||
const onWebsiteChange = value => { |
|||
if (!value) { |
|||
setAutoCompleteResult([]); |
|||
} else { |
|||
setAutoCompleteResult(['.com', '.org', '.net'].map(domain => `${value}${domain}`)); |
|||
} |
|||
}; |
|||
|
|||
const websiteOptions = autoCompleteResult.map(website => ( |
|||
<AutoCompleteOption key={website}>{website}</AutoCompleteOption> |
|||
)); |
|||
|
|||
return ( |
|||
<Form |
|||
{...formItemLayout} |
|||
name="register" |
|||
onFinish={onFinish} |
|||
initialValues={{ |
|||
residence: ['zhejiang', 'hangzhou', 'xihu'], |
|||
prefix: '86', |
|||
}} |
|||
> |
|||
<Form.Item |
|||
name="email" |
|||
label="E-mail" |
|||
rules={[ |
|||
{ |
|||
type: 'email', |
|||
message: 'The input is not valid E-mail!', |
|||
}, |
|||
{ |
|||
required: true, |
|||
message: 'Please input your E-mail!', |
|||
}, |
|||
]} |
|||
> |
|||
<Input /> |
|||
</Form.Item> |
|||
|
|||
<Form.Item |
|||
name="password" |
|||
label="Password" |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: 'Please input your password!', |
|||
}, |
|||
]} |
|||
hasFeedback |
|||
> |
|||
<Input.Password /> |
|||
</Form.Item> |
|||
|
|||
<Form.Item |
|||
name="confirm" |
|||
label="Confirm Password" |
|||
dependencies={['password']} |
|||
hasFeedback |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: 'Please confirm your password!', |
|||
}, |
|||
({ getFieldValue }) => ({ |
|||
validator(rule, value) { |
|||
if (!value || getFieldValue('password') === value) { |
|||
return Promise.resolve(); |
|||
} |
|||
return Promise.reject('Two passwords that you enter is inconsistent!'); |
|||
}, |
|||
}), |
|||
]} |
|||
> |
|||
<Input.Password /> |
|||
</Form.Item> |
|||
|
|||
<Form.Item |
|||
name="nickname" |
|||
label={ |
|||
<span> |
|||
Nickname |
|||
<Tooltip title="What do you want others to call you?"> |
|||
<Icon type="question-circle-o" /> |
|||
</Tooltip> |
|||
</span> |
|||
} |
|||
rules={[{ required: true, message: 'Please input your nickname!', whitespace: true }]} |
|||
> |
|||
<Input /> |
|||
</Form.Item> |
|||
|
|||
<Form.Item |
|||
name="residence" |
|||
label="Habitual Residence" |
|||
rules={[ |
|||
{ type: 'array', required: true, message: 'Please select your habitual residence!' }, |
|||
]} |
|||
> |
|||
<Cascader options={residences} /> |
|||
</Form.Item> |
|||
|
|||
<Form.Item |
|||
name="phone" |
|||
label="Phone Number" |
|||
rules={[{ required: true, message: 'Please input your phone number!' }]} |
|||
> |
|||
<Input addonBefore={prefixSelector} style={{ width: '100%' }} /> |
|||
</Form.Item> |
|||
|
|||
<Form.Item |
|||
name="website" |
|||
label="Website" |
|||
rules={[{ required: true, message: 'Please input website!' }]} |
|||
> |
|||
<AutoComplete dataSource={websiteOptions} onChange={onWebsiteChange} placeholder="website"> |
|||
<Input /> |
|||
</AutoComplete> |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="Captcha" extra="We must make sure that your are a human."> |
|||
<Row gutter={8}> |
|||
<Col span={12}> |
|||
<Form.Item |
|||
name="captcha" |
|||
inline |
|||
rules={[{ required: true, message: 'Please input the captcha you got!' }]} |
|||
> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Col> |
|||
<Col span={12}> |
|||
<Button>Get captcha</Button> |
|||
</Col> |
|||
</Row> |
|||
</Form.Item> |
|||
|
|||
<Form.Item name="agreement" valuePropName="checked" {...tailFormItemLayout}> |
|||
<Checkbox> |
|||
I have read the <a href="">agreement</a> |
|||
</Checkbox> |
|||
</Form.Item> |
|||
<Form.Item {...tailFormItemLayout}> |
|||
<Button type="primary" htmlType="submit"> |
|||
Register |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<RegistrationForm />, mountNode); |
|||
``` |
@ -0,0 +1,93 @@ |
|||
--- |
|||
order: 16 |
|||
title: |
|||
zh-CN: 时间类控件 |
|||
en-US: Time-related Controls |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
时间类组件的 `value` 类型为 `moment` 对象,所以在提交服务器前需要预处理。 |
|||
|
|||
## en-US |
|||
|
|||
The `value` of time-related components is a `moment` object, which we need to pre-process it before we submit to server. |
|||
|
|||
```tsx |
|||
import { Form, DatePicker, TimePicker, Button } from 'antd'; |
|||
|
|||
const { MonthPicker, RangePicker } = DatePicker; |
|||
|
|||
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!' }], |
|||
}; |
|||
|
|||
const TimeRelatedForm = () => { |
|||
const onFinish = fieldsValue => { |
|||
// 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); |
|||
}; |
|||
|
|||
return ( |
|||
<Form name="time_related_controls" {...formItemLayout} onFinish={onFinish}> |
|||
<Form.Item name="date-picker" label="DatePicker" {...config}> |
|||
<DatePicker /> |
|||
</Form.Item> |
|||
<Form.Item name="date-time-picker" label="DatePicker[showTime]" {...config}> |
|||
<DatePicker showTime format="YYYY-MM-DD HH:mm:ss" /> |
|||
</Form.Item> |
|||
<Form.Item name="month-picker" label="MonthPicker" {...config}> |
|||
<MonthPicker /> |
|||
</Form.Item> |
|||
<Form.Item name="range-picker" label="RangePicker" {...rangeConfig}> |
|||
<RangePicker /> |
|||
</Form.Item> |
|||
<Form.Item name="range-time-picker" label="RangePicker[showTime]" {...rangeConfig}> |
|||
<RangePicker showTime format="YYYY-MM-DD HH:mm:ss" /> |
|||
</Form.Item> |
|||
<Form.Item name="time-picker" label="TimePicker" {...config}> |
|||
<TimePicker /> |
|||
</Form.Item> |
|||
<Form.Item |
|||
wrapperCol={{ |
|||
xs: { span: 24, offset: 0 }, |
|||
sm: { span: 16, offset: 8 }, |
|||
}} |
|||
> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<TimeRelatedForm />, mountNode); |
|||
``` |
@ -0,0 +1,204 @@ |
|||
--- |
|||
order: 24 |
|||
title: |
|||
zh-CN: 校验其他组件 |
|||
en-US: Other Form Controls |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
以上演示没有出现的表单控件对应的校验演示。 |
|||
|
|||
## en-US |
|||
|
|||
Demonstration of validation configuration for form controls which are not shown in the demos above. |
|||
|
|||
```tsx |
|||
import { |
|||
Form, |
|||
Select, |
|||
InputNumber, |
|||
Switch, |
|||
Radio, |
|||
Slider, |
|||
Button, |
|||
Upload, |
|||
Icon, |
|||
Rate, |
|||
Checkbox, |
|||
Row, |
|||
Col, |
|||
} from 'antd'; |
|||
|
|||
const { Option } = Select; |
|||
|
|||
const formItemLayout = { |
|||
labelCol: { span: 6 }, |
|||
wrapperCol: { span: 14 }, |
|||
}; |
|||
|
|||
const normFile = e => { |
|||
console.log('Upload event:', e); |
|||
if (Array.isArray(e)) { |
|||
return e; |
|||
} |
|||
return e && e.fileList; |
|||
}; |
|||
|
|||
const Demo = () => { |
|||
const onFinish = values => { |
|||
console.log('Received values of form: ', values); |
|||
}; |
|||
|
|||
return ( |
|||
<Form |
|||
name="validate_other" |
|||
{...formItemLayout} |
|||
onFinish={onFinish} |
|||
initialValues={{ |
|||
['input-number']: 3, |
|||
['checkbox-group']: ['A', 'B'], |
|||
rate: 3.5, |
|||
}} |
|||
> |
|||
<Form.Item label="Plain Text"> |
|||
<span className="ant-form-text">China</span> |
|||
</Form.Item> |
|||
<Form.Item |
|||
name="select" |
|||
label="Select" |
|||
hasFeedback |
|||
rules={[{ required: true, message: 'Please select your country!' }]} |
|||
> |
|||
<Select placeholder="Please select a country"> |
|||
<Option value="china">China</Option> |
|||
<Option value="usa">U.S.A</Option> |
|||
</Select> |
|||
</Form.Item> |
|||
|
|||
<Form.Item |
|||
name="select-multiple" |
|||
label="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> |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="InputNumber"> |
|||
<Form.Item name="input-number" inline> |
|||
<InputNumber min={1} max={10} /> |
|||
</Form.Item> |
|||
<span className="ant-form-text"> machines</span> |
|||
</Form.Item> |
|||
|
|||
<Form.Item name="switch" label="Switch" valuePropName="checked"> |
|||
<Switch /> |
|||
</Form.Item> |
|||
|
|||
<Form.Item name="slider" label="Slider"> |
|||
<Slider |
|||
marks={{ |
|||
0: 'A', |
|||
20: 'B', |
|||
40: 'C', |
|||
60: 'D', |
|||
80: 'E', |
|||
100: 'F', |
|||
}} |
|||
/> |
|||
</Form.Item> |
|||
|
|||
<Form.Item name="radio-group" label="Radio.Group"> |
|||
<Radio.Group> |
|||
<Radio value="a">item 1</Radio> |
|||
<Radio value="b">item 2</Radio> |
|||
<Radio value="c">item 3</Radio> |
|||
</Radio.Group> |
|||
</Form.Item> |
|||
|
|||
<Form.Item name="radio-button" label="Radio.Button"> |
|||
<Radio.Group> |
|||
<Radio.Button value="a">item 1</Radio.Button> |
|||
<Radio.Button value="b">item 2</Radio.Button> |
|||
<Radio.Button value="c">item 3</Radio.Button> |
|||
</Radio.Group> |
|||
</Form.Item> |
|||
|
|||
<Form.Item name="checkbox-group" label="Checkbox.Group"> |
|||
<Checkbox.Group style={{ width: '100%' }}> |
|||
<Row> |
|||
<Col span={8}> |
|||
<Checkbox value="A">A</Checkbox> |
|||
</Col> |
|||
<Col span={8}> |
|||
<Checkbox disabled value="B"> |
|||
B |
|||
</Checkbox> |
|||
</Col> |
|||
<Col span={8}> |
|||
<Checkbox value="C">C</Checkbox> |
|||
</Col> |
|||
<Col span={8}> |
|||
<Checkbox value="D">D</Checkbox> |
|||
</Col> |
|||
<Col span={8}> |
|||
<Checkbox value="E">E</Checkbox> |
|||
</Col> |
|||
</Row> |
|||
</Checkbox.Group> |
|||
</Form.Item> |
|||
|
|||
<Form.Item name="rate" label="Rate"> |
|||
<Rate /> |
|||
</Form.Item> |
|||
|
|||
<Form.Item |
|||
name="upload" |
|||
label="Upload" |
|||
valuePropName="fileList" |
|||
getValueFromEvent={normFile} |
|||
extra="longgggggggggggggggggggggggggggggggggg" |
|||
> |
|||
<Upload name="logo" action="/upload.do" listType="picture"> |
|||
<Button> |
|||
<Icon type="upload" /> Click to upload |
|||
</Button> |
|||
</Upload> |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="Dragger"> |
|||
<div className="dropbox"> |
|||
<Form.Item name="dragger" valuePropName="fileList" getValueFromEvent={normFile} inline> |
|||
<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> |
|||
</Form.Item> |
|||
</div> |
|||
</Form.Item> |
|||
|
|||
<Form.Item wrapperCol={{ span: 12, offset: 6 }}> |
|||
<Button type="primary" htmlType="submit"> |
|||
Submit |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<Demo />, mountNode); |
|||
``` |
|||
|
|||
```css |
|||
#components-form-demo-validate-other .dropbox { |
|||
height: 180px; |
|||
line-height: 1.5; |
|||
} |
|||
``` |
@ -0,0 +1,67 @@ |
|||
--- |
|||
order: 19 |
|||
title: |
|||
zh-CN: 自行处理表单数据 |
|||
en-US: Handle Form Data Manually |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
`Form` 具有自动收集数据并校验的功能,但如果您不需要这个功能,或者默认的行为无法满足业务需求,可以选择自行处理数据。 |
|||
|
|||
## en-US |
|||
|
|||
`Form` will collect and validate form data automatically. But if you don't need this feature or the default behavior cannot satisfy your business, you can handle form data manually. |
|||
|
|||
```tsx |
|||
import { Form, InputNumber } from 'antd'; |
|||
|
|||
function validatePrimeNumber(number) { |
|||
if (number === 11) { |
|||
return { |
|||
validateStatus: 'success', |
|||
errorMsg: null, |
|||
}; |
|||
} |
|||
return { |
|||
validateStatus: 'error', |
|||
errorMsg: 'The prime between 8 and 12 is 11!', |
|||
}; |
|||
} |
|||
|
|||
const formItemLayout = { |
|||
labelCol: { span: 7 }, |
|||
wrapperCol: { span: 12 }, |
|||
}; |
|||
|
|||
const RawForm = () => { |
|||
const [number, setNumber] = React.useState({ |
|||
value: 11, |
|||
}); |
|||
|
|||
const tips = |
|||
'A prime is a natural number greater than 1 that has no positive divisors other than 1 and itself.'; |
|||
|
|||
const onNumberChange = value => { |
|||
setNumber({ |
|||
...validatePrimeNumber(value), |
|||
value, |
|||
}); |
|||
}; |
|||
|
|||
return ( |
|||
<Form> |
|||
<Form.Item |
|||
{...formItemLayout} |
|||
label="Prime between 8 & 12" |
|||
validateStatus={number.validateStatus} |
|||
help={number.errorMsg || tips} |
|||
> |
|||
<InputNumber min={8} max={12} value={number.value} onChange={onNumberChange} /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
ReactDOM.render(<RawForm />, mountNode); |
|||
``` |
@ -1,3 +1,23 @@ |
|||
import Form from './Form'; |
|||
import InternalForm, { useForm, FormInstance } from './Form'; |
|||
import Item from './FormItem'; |
|||
import List from './FormList'; |
|||
import { FormProvider } from './context'; |
|||
|
|||
type InternalForm = typeof InternalForm; |
|||
interface Form extends InternalForm { |
|||
useForm: typeof useForm; |
|||
Item: typeof Item; |
|||
List: typeof List; |
|||
Provider: typeof FormProvider; |
|||
} |
|||
|
|||
const Form: Form = InternalForm as Form; |
|||
|
|||
Form.Item = Item; |
|||
Form.List = List; |
|||
Form.useForm = useForm; |
|||
Form.Provider = FormProvider; |
|||
|
|||
export { FormInstance }; |
|||
|
|||
export default Form; |
|||
|
@ -0,0 +1 @@ |
|||
export type FormLabelAlign = 'left' | 'right'; |
@ -0,0 +1,76 @@ |
|||
@import './index'; |
|||
|
|||
// ================================================================ |
|||
// = Children Component = |
|||
// ================================================================ |
|||
.@{form-item-prefix-cls} { |
|||
.@{ant-prefix}-mentions, |
|||
textarea.@{ant-prefix}-input { |
|||
height: auto; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
// input[type=file] |
|||
.@{ant-prefix}-upload { |
|||
background: transparent; |
|||
} |
|||
|
|||
input[type='radio'], |
|||
input[type='checkbox'] { |
|||
width: 14px; |
|||
height: 14px; |
|||
} |
|||
|
|||
// Radios and checkboxes on same line |
|||
.@{ant-prefix}-radio-inline, |
|||
.@{ant-prefix}-checkbox-inline { |
|||
display: inline-block; |
|||
margin-left: 8px; |
|||
font-weight: normal; |
|||
vertical-align: middle; |
|||
cursor: pointer; |
|||
|
|||
&:first-child { |
|||
margin-left: 0; |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-checkbox-vertical, |
|||
.@{ant-prefix}-radio-vertical { |
|||
display: block; |
|||
} |
|||
|
|||
.@{ant-prefix}-checkbox-vertical + .@{ant-prefix}-checkbox-vertical, |
|||
.@{ant-prefix}-radio-vertical + .@{ant-prefix}-radio-vertical { |
|||
margin-left: 0; |
|||
} |
|||
|
|||
.@{ant-prefix}-input-number { |
|||
+ .@{form-prefix-cls}-text { |
|||
margin-left: 8px; |
|||
} |
|||
&-handler-wrap { |
|||
z-index: 2; // https://github.com/ant-design/ant-design/issues/6289 |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-select, |
|||
.@{ant-prefix}-cascader-picker { |
|||
width: 100%; |
|||
} |
|||
|
|||
// Don't impact select inside input group |
|||
.@{ant-prefix}-input-group .@{ant-prefix}-select, |
|||
.@{ant-prefix}-input-group .@{ant-prefix}-cascader-picker { |
|||
width: auto; |
|||
} |
|||
|
|||
// fix input with addon position. https://github.com/ant-design/ant-design/issues/8243 |
|||
:not(.@{ant-prefix}-input-group-wrapper) > .@{ant-prefix}-input-group, |
|||
.@{ant-prefix}-input-group-wrapper { |
|||
position: relative; |
|||
top: -1px; |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
} |
|||
} |
@ -0,0 +1 @@ |
|||
@import './index'; |
@ -0,0 +1,31 @@ |
|||
@import './index'; |
|||
|
|||
.@{form-prefix-cls}-inline { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
|
|||
.@{form-prefix-cls}-item { |
|||
margin-right: 16px; |
|||
margin-bottom: 0; |
|||
flex-wrap: nowrap; |
|||
flex: none; |
|||
|
|||
&-with-help { |
|||
margin-bottom: @form-item-margin-bottom; |
|||
} |
|||
|
|||
> .@{form-item-prefix-cls}-label, |
|||
> .@{form-item-prefix-cls}-control { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
} |
|||
|
|||
.@{form-prefix-cls}-text { |
|||
display: inline-block; |
|||
} |
|||
|
|||
.@{form-item-prefix-cls}-has-feedback { |
|||
display: inline-block; |
|||
} |
|||
} |
|||
} |
@ -1 +1,125 @@ |
|||
@import '../../input/style/mixin'; |
|||
|
|||
.form-control-validation(@text-color: @input-color; @border-color: @input-border-color; @background-color: @input-bg) { |
|||
.@{ant-prefix}-form-item-explain, |
|||
.@{ant-prefix}-form-item-split { |
|||
color: @text-color; |
|||
} |
|||
// 输入框的不同校验状态 |
|||
.@{ant-prefix}-input { |
|||
&, |
|||
&:hover { |
|||
border-color: @border-color; |
|||
} |
|||
|
|||
&:focus { |
|||
.active(@border-color); |
|||
} |
|||
|
|||
&:not([disabled]):hover { |
|||
border-color: @border-color; |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-calendar-picker-open .@{ant-prefix}-calendar-picker-input { |
|||
.active(@border-color); |
|||
} |
|||
|
|||
// Input prefix |
|||
.@{ant-prefix}-input-affix-wrapper { |
|||
.@{ant-prefix}-input { |
|||
&, |
|||
&:hover { |
|||
background-color: @background-color; |
|||
border-color: @border-color; |
|||
} |
|||
|
|||
&:focus { |
|||
.active(@border-color); |
|||
} |
|||
} |
|||
|
|||
&:hover .@{ant-prefix}-input:not(.@{ant-prefix}-input-disabled) { |
|||
border-color: @border-color; |
|||
} |
|||
} |
|||
|
|||
.@{ant-prefix}-input-prefix { |
|||
color: @text-color; |
|||
} |
|||
|
|||
.@{ant-prefix}-input-group-addon { |
|||
color: @text-color; |
|||
background-color: @background-color; |
|||
border-color: @border-color; |
|||
} |
|||
|
|||
.has-feedback { |
|||
color: @text-color; |
|||
} |
|||
} |
|||
|
|||
// Reset form styles |
|||
// ----------------------------- |
|||
// Based on Bootstrap framework |
|||
.reset-form() { |
|||
legend { |
|||
display: block; |
|||
width: 100%; |
|||
margin-bottom: 20px; |
|||
padding: 0; |
|||
color: @text-color-secondary; |
|||
font-size: @font-size-lg; |
|||
line-height: inherit; |
|||
border: 0; |
|||
border-bottom: @border-width-base @border-style-base @border-color-base; |
|||
} |
|||
|
|||
label { |
|||
font-size: @font-size-base; |
|||
} |
|||
|
|||
input[type='search'] { |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
// Position radios and checkboxes better |
|||
input[type='radio'], |
|||
input[type='checkbox'] { |
|||
line-height: normal; |
|||
} |
|||
|
|||
input[type='file'] { |
|||
display: block; |
|||
} |
|||
|
|||
// Make range inputs behave like textual form controls |
|||
input[type='range'] { |
|||
display: block; |
|||
width: 100%; |
|||
} |
|||
|
|||
// Make multiple select elements height not fixed |
|||
select[multiple], |
|||
select[size] { |
|||
height: auto; |
|||
} |
|||
|
|||
// Focus for file, radio, and checkbox |
|||
input[type='file']:focus, |
|||
input[type='radio']:focus, |
|||
input[type='checkbox']:focus { |
|||
outline: thin dotted; |
|||
outline: 5px auto -webkit-focus-ring-color; |
|||
outline-offset: -2px; |
|||
} |
|||
|
|||
// Adjust output element |
|||
output { |
|||
display: block; |
|||
padding-top: 15px; |
|||
color: @input-color; |
|||
font-size: @font-size-base; |
|||
line-height: @line-height-base; |
|||
} |
|||
} |
|||
|
@ -0,0 +1,61 @@ |
|||
@import './index'; |
|||
|
|||
// ================== Label ================== |
|||
.make-vertical-layout-label() { |
|||
margin: @form-vertical-label-margin; |
|||
padding: @form-vertical-label-padding; |
|||
line-height: @line-height-base; |
|||
white-space: initial; |
|||
text-align: left; |
|||
|
|||
> label { |
|||
margin: 0; |
|||
|
|||
&::after { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.@{form-prefix-cls}-vertical { |
|||
.@{form-item-prefix-cls} { |
|||
flex-direction: column; |
|||
} |
|||
} |
|||
|
|||
.@{form-prefix-cls}-vertical .@{form-item-prefix-cls}-label, |
|||
// when labelCol is 24, it is a vertical form |
|||
.@{ant-prefix}-col-24.@{form-item-prefix-cls}-label, |
|||
.@{ant-prefix}-col-xl-24.@{form-item-prefix-cls}-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
|
|||
@media (max-width: @screen-xs-max) { |
|||
.@{ant-prefix}-col-xs-24.@{form-item-prefix-cls}-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
} |
|||
|
|||
@media (max-width: @screen-sm-max) { |
|||
.@{ant-prefix}-col-sm-24.@{form-item-prefix-cls}-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
} |
|||
|
|||
@media (max-width: @screen-md-max) { |
|||
.@{ant-prefix}-col-md-24.@{form-item-prefix-cls}-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
} |
|||
|
|||
@media (max-width: @screen-lg-max) { |
|||
.@{ant-prefix}-col-lg-24.@{form-item-prefix-cls}-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
} |
|||
|
|||
@media (max-width: @screen-xl-max) { |
|||
.@{ant-prefix}-col-xl-24.@{form-item-prefix-cls}-label { |
|||
.make-vertical-layout-label(); |
|||
} |
|||
} |
@ -0,0 +1,60 @@ |
|||
import * as React from 'react'; |
|||
|
|||
/** |
|||
* We will remove light way shake like: |
|||
* errors -> none -> errors (in 100 ms) |
|||
* to |
|||
* errors (in 100 ms) |
|||
*/ |
|||
function useDebounce<T>(content: T[], delay: number = 10): T[] { |
|||
const [cache, setCache] = React.useState(content); |
|||
|
|||
// React hooks still have bug with unmount setState
|
|||
// https://github.com/facebook/react/issues/15057
|
|||
React.useEffect(() => { |
|||
if (content.length) { |
|||
setCache(content); |
|||
} else { |
|||
const timeout = setTimeout(() => { |
|||
setCache(content); |
|||
}, delay); |
|||
|
|||
return () => { |
|||
clearTimeout(timeout); |
|||
}; |
|||
} |
|||
}, [content]); |
|||
|
|||
return cache; |
|||
} |
|||
|
|||
/** |
|||
* Cache latest errors and trigger change event if visible change |
|||
*/ |
|||
export function useCacheErrors( |
|||
errors: React.ReactNode[], |
|||
changeTrigger: (visible: boolean) => void, |
|||
) { |
|||
const debounceErrors = useDebounce(errors); |
|||
const [cacheErrors, setCacheErrors] = React.useState(debounceErrors); |
|||
const [visible, setVisible] = React.useState(false); |
|||
|
|||
React.useEffect(() => { |
|||
const newVisible = !!debounceErrors.length; |
|||
if (debounceErrors.length) { |
|||
setCacheErrors(debounceErrors); |
|||
} |
|||
if (newVisible !== visible) { |
|||
changeTrigger(newVisible); |
|||
} |
|||
setVisible(newVisible); |
|||
}, [debounceErrors]); |
|||
|
|||
return [visible, cacheErrors]; |
|||
} |
|||
|
|||
export function toArray<T>(candidate?: T | T[] | false): T[] { |
|||
if (candidate === undefined || candidate === false) return []; |
|||
|
|||
return Array.isArray(candidate) ? candidate : [candidate]; |
|||
} |
@ -0,0 +1,270 @@ |
|||
--- |
|||
title: Migrate to v4 |
|||
skip: true |
|||
--- |
|||
|
|||
### Remove Form.create |
|||
|
|||
Form of v4 is not need create context by `Form.create()`. Form is now have own data scope and you don't need `getFieldDecorator` anymore. Just use Form.Item directly: |
|||
|
|||
```jsx |
|||
// antd v3 |
|||
const Demo = ({ form: { getFieldDecorator } }) => ( |
|||
<Form> |
|||
<Form.Item> |
|||
{getFieldDecorator('username', { |
|||
rules: [{ required: true }], |
|||
})(<Input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
|
|||
const WrappedDemo = Form.create()(Demo); |
|||
``` |
|||
|
|||
To: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
const Demo = () => ( |
|||
<Form> |
|||
<Form.Item name="username" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
``` |
|||
|
|||
Since `Form.create()` is removed, origin `onFieldsChange` or other methods moved to Form, and control by `fields`. ref [example](/components/form/#components-form-demo-global-state)。 |
|||
|
|||
### Form control |
|||
|
|||
If you want to control form, you can use `Form.useForm()` to create Form instance for operation: |
|||
|
|||
```jsx |
|||
// antd v3 |
|||
const Demo = ({ form: { setFieldsValue } }) => { |
|||
React.useEffect(() => { |
|||
setFieldsValue({ |
|||
username: 'Bamboo', |
|||
}); |
|||
}, []); |
|||
|
|||
return ( |
|||
<Form> |
|||
<Form.Item> |
|||
{getFieldDecorator('username', { |
|||
rules: [{ required: true }], |
|||
})(<Input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
const WrappedDemo = Form.create()(Demo); |
|||
``` |
|||
|
|||
To: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
const Demo = () => { |
|||
const [form] = Form.useForm(); |
|||
|
|||
React.useEffect(() => { |
|||
form.setFieldsValue({ |
|||
username: 'Bamboo', |
|||
}); |
|||
}, []); |
|||
|
|||
return ( |
|||
<Form form={form}> |
|||
<Form.Item name="username" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
For class component, you can use `ref` to access instance: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
class Demo extends React.Component { |
|||
formRef = React.createRef(); |
|||
|
|||
componentDidMount() { |
|||
this.formRef.setFieldsValue({ |
|||
username: 'Bamboo', |
|||
}); |
|||
} |
|||
|
|||
render() { |
|||
return ( |
|||
<Form ref={formRef}> |
|||
<Form.Item name="username" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
If you don't want to use the Item style, you can use `inline` prop to remove it: |
|||
|
|||
```jsx |
|||
// antd v3 |
|||
const Demo = ({ form: { setFieldsValue } }) => { |
|||
return <Form>{getFieldDecorator('username')(<Input />)}</Form>; |
|||
}; |
|||
|
|||
const WrappedDemo = Form.create()(Demo); |
|||
``` |
|||
|
|||
To: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
const Demo = () => { |
|||
return ( |
|||
<Form> |
|||
<Form.Item name="username" inline> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
### Linkage with field |
|||
|
|||
New Form use incremental update which only update related field. So if there are some linkage between fields or update with whole form. You can use [`dependencies`](/components/form/#dependencies) or [`shouldUpdate`](/components/form/#shouldUpdate) to handle that. |
|||
|
|||
### replace onSubmit with onFinish |
|||
|
|||
You need to listen `onSubmit` and call `validateFields` to handle validation in old Form. New Form provides `onFinish` which only trigger when validation is passed: |
|||
|
|||
```jsx |
|||
// antd v3 |
|||
const Demo = ({ form: { getFieldDecorator, validateFields } }) => { |
|||
const onSubmit = e => { |
|||
e.preventDefault(); |
|||
validateFields((err, values) => { |
|||
if (!err) { |
|||
console.log('Received values of form: ', values); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
return ( |
|||
<Form onSubmit={onSubmit}> |
|||
<Form.Item> |
|||
{getFieldDecorator('username', { |
|||
rules: [{ required: true }], |
|||
})(<Input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
const WrappedDemo = Form.create()(Demo); |
|||
``` |
|||
|
|||
To: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
const Demo = () => { |
|||
const onFinish = values => { |
|||
console.log('Received values of form: ', values); |
|||
}; |
|||
|
|||
return ( |
|||
<Form onFinish={onFinish}> |
|||
<Form.Item name="username" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
### Initialization |
|||
|
|||
Besides, we move `initialValue` into Form to avoid field with same name both using `initialValue` to cause conflict: |
|||
|
|||
```jsx |
|||
// antd v3 |
|||
const Demo = ({ form: { getFieldDecorator } }) => ( |
|||
<Form> |
|||
<Form.Item> |
|||
{getFieldDecorator('username', { |
|||
rules: [{ required: true }], |
|||
initialValue: 'Bamboo', |
|||
})(<Input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
|
|||
const WrappedDemo = Form.create()(Demo); |
|||
``` |
|||
|
|||
To: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
const Demo = () => ( |
|||
<Form initialValues={{ username: 'Bamboo' }}> |
|||
<Form.Item name="username" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
``` |
|||
|
|||
In v3, modifying the `initialValue` of un-operated field will update the field value synchronously, which is a bug. However, since it has been used as a feature for a long time, we have not fixed it. In v4, the bug has been fixed. `initialValues` only takes effect when initializing and resetting the form. |
|||
|
|||
### Remove field no longer removes field value |
|||
|
|||
In v3, we will clean up the values when the fields removed. However, it has been found that clearing fields can cause greater inconvenience than retention in practice. For example like some switching operations, the user has to retain the field value in a hidden way. Therefore, the new version of Form will always keep the fields value, and the validation rules for removing field will be ignored. So `preserve` is no longer needed. |
|||
|
|||
### Nested field paths using arrays |
|||
|
|||
In the past versions we used `.` to represent nested paths (such as `user.name` to represent `{ user: { name: '' } }`). However, in some backend systems, `.` is also included in the variable name. This causes the user to need additional code to convert, so in the new version, nested paths are represented by arrays to avoid unexpected behavior (eg `['user', 'name']`). |
|||
|
|||
Therefore, paths returned by methods such as `getFieldsError` are always in an array form for the user to handle: |
|||
|
|||
```jsx |
|||
form.getFieldsError(); |
|||
|
|||
/* |
|||
[ |
|||
{ name: ['user', 'name'], errors: [] }, |
|||
{ name: ['user', 'age'], errors: ['Some error message'] }, |
|||
] |
|||
*/ |
|||
``` |
|||
|
|||
### Remove callback in validateFields |
|||
|
|||
`validateFields` will return a Promise, so you can handle the error with `async/await` or `then/catch`. It is no longer necessary to determine if `errors` is empty: |
|||
|
|||
```jsx |
|||
// antd v3 |
|||
validateFields((err, value) => { |
|||
if (!err) { |
|||
// Do something with value |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
To |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
validateFields().then(values => { |
|||
// Do something with value |
|||
}); |
|||
``` |
@ -0,0 +1,272 @@ |
|||
--- |
|||
title: 从 v3 到 v4 |
|||
skip: true |
|||
--- |
|||
|
|||
新版本 Form 对使用方式进行了简化,因而如果你是从 v3 迁移上来。你可以参考本文做迁移工作。 |
|||
|
|||
## 去除 Form.create |
|||
|
|||
v4 的 Form 不再需要通过 `Form.create()` 创建上下文。Form 组件现在自带数据域,因而 `getFieldDecorator` 也不再需要,直接写入 Form.Item 即可: |
|||
|
|||
```jsx |
|||
// antd v3 |
|||
const Demo = ({ form: { getFieldDecorator } }) => ( |
|||
<Form> |
|||
<Form.Item> |
|||
{getFieldDecorator('username', { |
|||
rules: [{ required: true }], |
|||
})(<Input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
|
|||
const WrappedDemo = Form.create()(Demo); |
|||
``` |
|||
|
|||
改成: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
const Demo = () => ( |
|||
<Form> |
|||
<Form.Item name="username" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
``` |
|||
|
|||
由于移除了 `Form.create()`,原本的 `onFieldsChange` 等方法移入 Form 中,通过 `fields` 对 Form 进行控制。参考[示例](/components/form/#components-form-demo-global-state)。 |
|||
|
|||
## 表单控制调整 |
|||
|
|||
Form 自带表单控制实体,如需要调用 form 方法,可以通过 `Form.useForm()` 创建 Form 实体进行操作: |
|||
|
|||
```jsx |
|||
// antd v3 |
|||
const Demo = ({ form: { setFieldsValue } }) => { |
|||
React.useEffect(() => { |
|||
setFieldsValue({ |
|||
username: 'Bamboo', |
|||
}); |
|||
}, []); |
|||
|
|||
return ( |
|||
<Form> |
|||
<Form.Item> |
|||
{getFieldDecorator('username', { |
|||
rules: [{ required: true }], |
|||
})(<Input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
const WrappedDemo = Form.create()(Demo); |
|||
``` |
|||
|
|||
改成: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
const Demo = () => { |
|||
const [form] = Form.useForm(); |
|||
|
|||
React.useEffect(() => { |
|||
form.setFieldsValue({ |
|||
username: 'Bamboo', |
|||
}); |
|||
}, []); |
|||
|
|||
return ( |
|||
<Form form={form}> |
|||
<Form.Item name="username" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
对于 class component,也可以通过 `ref` 获得实体: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
class Demo extends React.Component { |
|||
formRef = React.createRef(); |
|||
|
|||
componentDidMount() { |
|||
this.formRef.setFieldsValue({ |
|||
username: 'Bamboo', |
|||
}); |
|||
} |
|||
|
|||
render() { |
|||
return ( |
|||
<Form ref={formRef}> |
|||
<Form.Item name="username" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
由于 Form.Item 内置字段绑定,如果需要不带样式的表单绑定,可以使用 `inline` 属性移除额外样式: |
|||
|
|||
```jsx |
|||
// antd v3 |
|||
const Demo = ({ form: { setFieldsValue } }) => { |
|||
return <Form>{getFieldDecorator('username')(<Input />)}</Form>; |
|||
}; |
|||
|
|||
const WrappedDemo = Form.create()(Demo); |
|||
``` |
|||
|
|||
改成: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
const Demo = () => { |
|||
return ( |
|||
<Form> |
|||
<Form.Item name="username" inline> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
## 字段联动调整 |
|||
|
|||
新版 Form 采用增量更新方式,仅会更新需要更新的字段。因而如果有字段关联更新,或者跟随整个表单更新而更新。可以使用 [`dependencies`](/components/form/#dependencies) 或 [`shouldUpdate`](/components/form/#shouldUpdate)。 |
|||
|
|||
## onFinish 替代 onSubmit |
|||
|
|||
对于表单校验,过去版本需要通过监听 `onSubmit` 事件手工触发 `validateFields`。新版直接使用 `onFinish` 事件,该事件仅当校验通过后才会执行: |
|||
|
|||
```jsx |
|||
// antd v3 |
|||
const Demo = ({ form: { getFieldDecorator, validateFields } }) => { |
|||
const onSubmit = e => { |
|||
e.preventDefault(); |
|||
validateFields((err, values) => { |
|||
if (!err) { |
|||
console.log('Received values of form: ', values); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
return ( |
|||
<Form onSubmit={onSubmit}> |
|||
<Form.Item> |
|||
{getFieldDecorator('username', { |
|||
rules: [{ required: true }], |
|||
})(<Input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
const WrappedDemo = Form.create()(Demo); |
|||
``` |
|||
|
|||
改成: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
const Demo = () => { |
|||
const onFinish = values => { |
|||
console.log('Received values of form: ', values); |
|||
}; |
|||
|
|||
return ( |
|||
<Form onFinish={onFinish}> |
|||
<Form.Item name="username" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
## 初始化调整 |
|||
|
|||
此外,我们将 `initialValue` 从字段中移到 Form 中。以避免同名字段设置 `initialValue` 的冲突问题: |
|||
|
|||
```jsx |
|||
// antd v3 |
|||
const Demo = ({ form: { getFieldDecorator } }) => ( |
|||
<Form> |
|||
<Form.Item> |
|||
{getFieldDecorator('username', { |
|||
rules: [{ required: true }], |
|||
initialValue: 'Bamboo', |
|||
})(<Input />)} |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
|
|||
const WrappedDemo = Form.create()(Demo); |
|||
``` |
|||
|
|||
改成: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
const Demo = () => ( |
|||
<Form initialValues={{ username: 'Bamboo' }}> |
|||
<Form.Item name="username" rules={[{ required: true }]}> |
|||
<Input /> |
|||
</Form.Item> |
|||
</Form> |
|||
); |
|||
``` |
|||
|
|||
在 v3 版本中,修改未操作的字段 `initialValue` 会同步更新字段值,这是一个 BUG。但是由于被长期作为一个 feature 使用,因而我们一直没有修复。在 v4 中,该 BUG 已被修复。`initialValue` 只有在初始化以及重置表单时生效。 |
|||
|
|||
## 移除字段不再移除字段值 |
|||
|
|||
v3 版本中,我们会将移除的字段对应的值同步清理。但是在实践中,发现清除字段相比保留会造成更大的不便。例如一些切换操作,用户不得不通过隐藏的方式保留字段值。因而新版本的 Form 总是会保留字段值,而移除字段的校验规则会被忽略。也因此 `preserve` 不再需要。 |
|||
|
|||
## 嵌套字段路径使用数组 |
|||
|
|||
过去版本我们通过 `.` 代表嵌套路径(诸如 `user.name` 来代表 `{ user: { name: '' } }`)。然而在一些后台系统中,变量名中也会带上 `.`。这造成用户需要额外的代码进行转化,因而新版中,嵌套路径通过数组来表示以避免错误的处理行为(如 `['user', 'name']`)。 |
|||
|
|||
也因此,诸如 `getFieldsError` 等方法返回的路径总是数组形式以便于用户处理: |
|||
|
|||
```jsx |
|||
form.getFieldsError(); |
|||
|
|||
/* |
|||
[ |
|||
{ name: ['user', 'name'], errors: [] }, |
|||
{ name: ['user', 'age'], errors: ['Some error message'] }, |
|||
] |
|||
*/ |
|||
``` |
|||
|
|||
## validateFields 不再支持 callback |
|||
|
|||
`validateFields` 会返回 Promise 对象,因而你可以通过 `async/await` 或者 `then/catch` 来执行对应的错误处理。不再需要判断 `errors` 是否为空: |
|||
|
|||
```jsx |
|||
// antd v3 |
|||
validateFields((err, value) => { |
|||
if (!err) { |
|||
// Do something with value |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
改成: |
|||
|
|||
```jsx |
|||
// antd v4 |
|||
validateFields().then(values => { |
|||
// Do something with value |
|||
}); |
|||
``` |
@ -1,5 +1,3 @@ |
|||
import demoTest from '../../../tests/shared/demoTest'; |
|||
|
|||
demoTest('table', { |
|||
skip: process.env.REACT === '15' ? ['edit-row', 'drag-sorting'] : [], |
|||
}); |
|||
demoTest('table'); |
|||
|
@ -1,3 +1,3 @@ |
|||
import demoTest from '../../../tests/shared/demoTest'; |
|||
|
|||
demoTest('tabs', { skip: process.env.REACT === '15' ? ['custom-tab-bar-node.md'] : [] }); |
|||
demoTest('tabs'); |
|||
|
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue