未验证 提交 4cdf37be 编写于 作者: Z zombieJ 提交者: GitHub

feat: New Form (#17327)

* 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 skip
上级 f5895777
......@@ -10,13 +10,6 @@ references:
attach_workspace:
at: ~/ant-design
install_react: &install_react
run: REACT=15 ./scripts/install-react.sh
react_15: &react_15
environment:
REACT: 15
react_16: &react_16
environment:
REACT: 16
......@@ -51,21 +44,6 @@ references:
- test_node:
requires:
- setup
- test_dist_15:
requires:
- dist
- test_lib_15:
requires:
- compile
- test_es_15:
requires:
- compile
- test_dom_15:
requires:
- setup
- test_node_15:
requires:
- setup
jobs:
setup:
......@@ -169,60 +147,6 @@ jobs:
- *attach_workspace
- run: npm run test-node -- -w 1
test_dist_15:
<<: *container_config
<<: *react_15
steps:
- checkout
- *attach_workspace
- *install_react
- run:
command: npm test -- -w 1 -u
environment:
LIB_DIR: dist
test_lib_15:
<<: *container_config
<<: *react_15
steps:
- checkout
- *attach_workspace
- *install_react
- run:
command: npm test -- -w 1 -u
environment:
LIB_DIR: lib
test_es_15:
<<: *container_config
<<: *react_15
steps:
- checkout
- *attach_workspace
- *install_react
- run:
command: npm test -- -w 1 -u
environment:
LIB_DIR: es
test_dom_15:
<<: *container_config
<<: *react_15
steps:
- checkout
- *attach_workspace
- *install_react
- run: npm test -- -w 1 -u
test_node_15:
<<: *container_config
<<: *react_15
steps:
- checkout
- *attach_workspace
- *install_react
- run: npm run test-node -- -w 1 -u
workflows:
version: 2
build_test:
......
......@@ -13,19 +13,11 @@ matrix:
fast_finish: true
include:
- env: TEST_TYPE=lint
- env: REACT=16 TEST_TYPE=test:dist
- env: REACT=16 TEST_TYPE=test:lib
- env: REACT=16 TEST_TYPE=test:es
- env: REACT=16 TEST_TYPE=test:dom
- env: REACT=16 TEST_TYPE=test:node
- env: REACT=15 TEST_TYPE=test:dist
- env: REACT=15 TEST_TYPE=test:lib
- env: REACT=15 TEST_TYPE=test:es
- env: REACT=15 TEST_TYPE=test:dom
- env: REACT=15 TEST_TYPE=test:node
before_script:
- scripts/install-react.sh
- env: TEST_TYPE=test:dist
- env: TEST_TYPE=test:lib
- env: TEST_TYPE=test:es
- env: TEST_TYPE=test:dom
- env: TEST_TYPE=test:node
script:
- scripts/travis-script.sh
......@@ -33,21 +33,6 @@ jobs:
node-react@16:
REACT: 16
TEST_TYPE: test:node
dist-react@15:
REACT: 15
TEST_TYPE: test:dist
lib-react@15:
REACT: 15
TEST_TYPE: test:lib
es-react@15:
REACT: 15
TEST_TYPE: test:es
dom-react@15:
REACT: 15
TEST_TYPE: test:dom
node-react@15:
REACT: 15
TEST_TYPE: test:node
steps:
- checkout: self
fetchDepth: 1
......@@ -57,8 +42,6 @@ jobs:
versionSpec: '10.x'
- script: npm install
displayName: install
- script: scripts/install-react.sh
displayName: install-react
- script: scripts/travis-script.sh
displayName: test
- task: PublishBuildArtifacts@1
......
......@@ -179,9 +179,6 @@ describe('Test utils function', () => {
});
it('should not throw when no children', () => {
if (process.env.REACT === '15') {
return;
}
expect(() => mount(<Wave />)).not.toThrow();
});
});
......
......@@ -11,9 +11,6 @@ describe('Button', () => {
});
it('mount correctly', () => {
if (process.env.REACT === '15') {
return;
}
expect(() => renderer.create(<Button>Follow</Button>)).not.toThrow();
});
......
......@@ -150,46 +150,38 @@ exports[`renders ./components/comment/demo/editor.md correctly 1`] = `
>
<div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-control-wrapper"
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control"
class="ant-form-item-control-input"
>
<span
class="ant-form-item-children"
>
<textarea
class="ant-input"
rows="4"
/>
</span>
<textarea
class="ant-input"
rows="4"
/>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-control-wrapper"
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control"
class="ant-form-item-control-input"
>
<span
class="ant-form-item-children"
<button
class="ant-btn ant-btn-primary"
type="submit"
>
<button
class="ant-btn ant-btn-primary"
type="submit"
>
<span>
Add Comment
</span>
</button>
</span>
<span>
Add Comment
</span>
</button>
</div>
</div>
</div>
......
......@@ -7067,28 +7067,19 @@ exports[`ConfigProvider components Form configProvider 1`] = `
class="config-form config-form-horizontal"
>
<div
class="config-row config-form-item config-form-item-with-help"
class="config-row-flex config-form-item config-form-item-has-error"
>
<div
class="config-col config-form-item-control-wrapper"
class="config-col config-form-item-control"
>
<div
class="config-form-item-control has-error"
class="config-form-item-control-input"
>
<span
class="config-form-item-children"
>
<input
class="config-input"
type="text"
value=""
/>
</span>
<div
class="config-form-explain"
>
Bamboo is Light
</div>
<input
class="config-input"
type="text"
value=""
/>
</div>
</div>
</div>
......@@ -7100,28 +7091,19 @@ exports[`ConfigProvider components Form normal 1`] = `
class="ant-form ant-form-horizontal"
>
<div
class="ant-row ant-form-item ant-form-item-with-help"
class="ant-row-flex ant-form-item ant-form-item-has-error"
>
<div
class="ant-col ant-form-item-control-wrapper"
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control has-error"
class="ant-form-item-control-input"
>
<span
class="ant-form-item-children"
>
<input
class="ant-input"
type="text"
value=""
/>
</span>
<div
class="ant-form-explain"
>
Bamboo is Light
</div>
<input
class="ant-input"
type="text"
value=""
/>
</div>
</div>
</div>
......@@ -7133,28 +7115,19 @@ exports[`ConfigProvider components Form prefixCls 1`] = `
class="prefix-Form prefix-Form-horizontal"
>
<div
class="ant-row prefix-Form-item prefix-Form-item-with-help"
class="ant-row-flex prefix-Form-item prefix-Form-item-has-error"
>
<div
class="ant-col prefix-Form-item-control-wrapper"
class="ant-col prefix-Form-item-control"
>
<div
class="prefix-Form-item-control has-error"
class="prefix-Form-item-control-input"
>
<span
class="prefix-Form-item-children"
>
<input
class="prefix-Form"
type="text"
value=""
/>
</span>
<div
class="prefix-Form-explain"
>
Bamboo is Light
</div>
<input
class="prefix-Form"
type="text"
value=""
/>
</div>
</div>
</div>
......
......@@ -39,6 +39,7 @@ Some component use dynamic style to support wave effect. You can config `csp` pr
| --- | --- | --- | --- |
| autoInsertSpaceInButton | Set `false` to remove space between 2 chinese characters on Button | boolean | true |
| csp | Set [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) config | { nonce: string } | - |
| form | Set Form common props | { validateMessages?: [ValidateMessages](/components/form/#validateMessages) } | - |
| renderEmpty | set empty content of components. Ref [Empty](/components/empty/) | Function(componentName: string): ReactNode | - |
| getPopupContainer | to set the container of the popup element. The default is to create a `div` element in `body`. | Function(triggerNode) | `() => document.body` |
| prefixCls | set prefix class | string | ant |
import * as React from 'react';
import createReactContext from '@ant-design/create-react-context';
import { FormProvider as RcFormProvider } from 'rc-field-form';
import { ValidateMessages } from 'rc-field-form/lib/interface';
import defaultRenderEmpty, { RenderEmptyHandler } from './renderEmpty';
export { RenderEmptyHandler };
......@@ -34,9 +34,12 @@ export interface ConfigProviderProps {
renderEmpty?: RenderEmptyHandler;
csp?: CSPConfig;
autoInsertSpaceInButton?: boolean;
form?: {
validateMessages?: ValidateMessages;
};
}
const ConfigContext = createReactContext<ConfigConsumerProps>({
export const ConfigContext = React.createContext<ConfigConsumerProps>({
// We provide a default function for Context without provider
getPrefixCls: (suffixCls: string, customizePrefixCls?: string) => {
if (customizePrefixCls) return customizePrefixCls;
......@@ -59,7 +62,14 @@ class ConfigProvider extends React.Component<ConfigProviderProps> {
};
renderProvider = (context: ConfigConsumerProps) => {
const { children, getPopupContainer, renderEmpty, csp, autoInsertSpaceInButton } = this.props;
const {
children,
getPopupContainer,
renderEmpty,
csp,
autoInsertSpaceInButton,
form,
} = this.props;
const config: ConfigConsumerProps = {
...context,
......@@ -75,7 +85,16 @@ class ConfigProvider extends React.Component<ConfigProviderProps> {
config.renderEmpty = renderEmpty;
}
return <ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>;
let childNode = children;
// Additional Form provider
if (form && form.validateMessages) {
childNode = (
<RcFormProvider validateMessages={form.validateMessages}>{children}</RcFormProvider>
);
}
return <ConfigContext.Provider value={config}>{childNode}</ConfigContext.Provider>;
};
render() {
......
......@@ -40,6 +40,7 @@ return (
| --- | --- | --- | --- |
| autoInsertSpaceInButton | 设置为 `false` 时,移除按钮中 2 个汉字之间的空格 | boolean | true |
| csp | 设置 [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) 配置 | { nonce: string } | - |
| form | 设置 Form 组件的通用属性 | { validateMessages?: [ValidateMessages](/components/form/#validateMessages) } | - |
| renderEmpty | 自定义组件空状态。参考 [空状态](/components/empty/) | Function(componentName: string): ReactNode | - |
| getPopupContainer | 弹出框(Select, Tooltip, Menu 等等)渲染父节点,默认渲染到 body 上。 | Function(triggerNode) | () => document.body |
| prefixCls | 设置统一样式前缀 | string | ant |
......@@ -34,7 +34,6 @@ class DrawerForm extends React.Component {
};
render() {
const { getFieldDecorator } = this.props.form;
return (
<div>
<Button type="primary" onClick={this.showDrawer}>
......@@ -49,90 +48,94 @@ class DrawerForm extends React.Component {
<Form layout="vertical" hideRequiredMark>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="Name">
{getFieldDecorator('name', {
rules: [{ required: true, message: 'Please enter user name' }],
})(<Input placeholder="Please enter user name" />)}
<Form.Item
name="name"
label="Name"
rules={[{ required: true, message: 'Please enter user name' }]}
>
<Input placeholder="Please enter user name" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="Url">
{getFieldDecorator('url', {
rules: [{ required: true, message: 'Please enter url' }],
})(
<Input
style={{ width: '100%' }}
addonBefore="http://"
addonAfter=".com"
placeholder="Please enter url"
/>,
)}
<Form.Item
name="url"
label="Url"
rules={[{ required: true, message: 'Please enter url' }]}
>
<Input
style={{ width: '100%' }}
addonBefore="http://"
addonAfter=".com"
placeholder="Please enter url"
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="Owner">
{getFieldDecorator('owner', {
rules: [{ required: true, message: 'Please select an owner' }],
})(
<Select placeholder="Please select an owner">
<Option value="xiao">Xiaoxiao Fu</Option>
<Option value="mao">Maomao Zhou</Option>
</Select>,
)}
<Form.Item
name="owner"
label="Owner"
rules={[{ required: true, message: 'Please select an owner' }]}
>
<Select placeholder="Please select an owner">
<Option value="xiao">Xiaoxiao Fu</Option>
<Option value="mao">Maomao Zhou</Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="Type">
{getFieldDecorator('type', {
rules: [{ required: true, message: 'Please choose the type' }],
})(
<Select placeholder="Please choose the type">
<Option value="private">Private</Option>
<Option value="public">Public</Option>
</Select>,
)}
<Form.Item
name="type"
label="Type"
rules={[{ required: true, message: 'Please choose the type' }]}
>
<Select placeholder="Please choose the type">
<Option value="private">Private</Option>
<Option value="public">Public</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="Approver">
{getFieldDecorator('approver', {
rules: [{ required: true, message: 'Please choose the approver' }],
})(
<Select placeholder="Please choose the approver">
<Option value="jack">Jack Ma</Option>
<Option value="tom">Tom Liu</Option>
</Select>,
)}
<Form.Item
name="approver"
label="Approver"
rules={[{ required: true, message: 'Please choose the approver' }]}
>
<Select placeholder="Please choose the approver">
<Option value="jack">Jack Ma</Option>
<Option value="tom">Tom Liu</Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="DateTime">
{getFieldDecorator('dateTime', {
rules: [{ required: true, message: 'Please choose the dateTime' }],
})(
<DatePicker.RangePicker
style={{ width: '100%' }}
getPopupContainer={trigger => trigger.parentNode}
/>,
)}
<Form.Item
name="dateTime"
label="DateTime"
rules={[{ required: true, message: 'Please choose the dateTime' }]}
>
<DatePicker.RangePicker
style={{ width: '100%' }}
getPopupContainer={trigger => trigger.parentNode}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<Form.Item label="Description">
{getFieldDecorator('description', {
rules: [
{
required: true,
message: 'please enter url description',
},
],
})(<Input.TextArea rows={4} placeholder="please enter url description" />)}
<Form.Item
name="description"
label="Description"
rules={[
{
required: true,
message: 'please enter url description',
},
]}
>
<Input.TextArea rows={4} placeholder="please enter url description" />
</Form.Item>
</Col>
</Row>
......@@ -162,7 +165,5 @@ class DrawerForm extends React.Component {
}
}
const App = Form.create()(DrawerForm);
ReactDOM.render(<App />, mountNode);
ReactDOM.render(<DrawerForm />, mountNode);
```
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>
);
}
}
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>;
}
}
// 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>
`;
// 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>
`;
import demoTest from '../../../tests/shared/demoTest';
demoTest('form');
/* 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);
});
});
});
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');
});
});
});
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();
});
});
/* 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} />;
export const FIELD_META_PROP = 'data-__meta';
export const FIELD_DATA_PROP = 'data-__field';
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,
});
---
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);
```
---
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);
```
---
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;
}
```
---
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);
```
---
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;
}
```
---
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>
---
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);
```
---
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);
```
---
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%;
}
```
---
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&nbsp;
<Tooltip title="What do you want others to call you?">
<Icon type="question-circle-o" />
</Tooltip>
</span>
}
>
{getFieldDecorator('nickname', {
rules: [{ required: true, message: 'Please input your nickname!', whitespace: true }],
})(<Input />)}
</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);
```
---
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);
```
---
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);
```
---
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;
}
```
---
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);
```
# 表单实现原理
---
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 组件设定表单的布局、校验信息的绘制,参考案例 —— 表单布局、自行处理表单数据、自定义校验。
---
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 />&nbsp;&nbsp;\[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\]&#x3A; 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 />&nbsp;&nbsp;\[fieldNames: string\[]],<br />&nbsp;&nbsp;\[options: object\],<br />&nbsp;&nbsp;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>
import Form from './Form';
export {
FormProps,
FormComponentProps,
FormCreateOption,
ValidateCallback,
ValidationRule,
} from './Form';
export { FormItemProps } from './FormItem';
export default Form;
---
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 />&nbsp;&nbsp;\[fieldName\]: {value: any, errors: \[Error\] }<br />}) => void |
| setFieldsValue | 设置一组输入控件的值(注意:不要在 `componentWillReceiveProps` 内使用,否则会导致死循环,[原因](https://github.com/ant-design/ant-design/issues/2985)) | ({ \[fieldName\]&#x3A; value }) => void |
| validateFields | 校验并获取一组输入域的值与 Error,若 fieldNames 参数为空,则校验全部组件 | (<br />&nbsp;&nbsp;\[fieldNames: string\[]],<br />&nbsp;&nbsp;\[options: object\],<br />&nbsp;&nbsp;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>
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>>;
此差异已折叠。
import '../../style/index.less';
import './index.less';
// style dependencies
import '../../grid/style';
@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;
}
}
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;
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;
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
# Form Dom 变化
- 状态 className 现在移动到顶层,不再是 input only
- 去除 `ant-form-item-control-wrapper` 一层 div
- `.has-success` 等状态样式添加 `ant-form-item` 前缀
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
export type FormLabelAlign = 'left' | 'right';
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
import demoTest from '../../../tests/shared/demoTest';
demoTest('table', {
skip: process.env.REACT === '15' ? ['edit-row', 'drag-sorting'] : [],
});
demoTest('table');
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册