Browse Source

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
pull/17536/head
zombieJ 5 years ago
committed by GitHub
parent
commit
4cdf37bedb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 76
      .circleci/config.yml
  2. 18
      .travis.yml
  3. 17
      azure-pipelines.yml
  4. 3
      components/_util/__tests__/util.test.js
  5. 3
      components/button/__tests__/index.test.js
  6. 42
      components/comment/__tests__/__snapshots__/demo.test.js.snap
  7. 75
      components/config-provider/__tests__/__snapshots__/components.test.js.snap
  8. 1
      components/config-provider/index.en-US.md
  9. 29
      components/config-provider/index.tsx
  10. 1
      components/config-provider/index.zh-CN.md
  11. 129
      components/drawer/demo/form-in-drawer.md
  12. 280
      components/form-legacy/Form.tsx
  13. 421
      components/form-legacy/FormItem.tsx
  14. 5260
      components/form-legacy/__tests__/__snapshots__/demo.test.js.snap
  15. 149
      components/form-legacy/__tests__/__snapshots__/label.test.js.snap
  16. 151
      components/form-legacy/__tests__/__snapshots__/message.test.js.snap
  17. 3
      components/form-legacy/__tests__/demo.test.js
  18. 45
      components/form-legacy/__tests__/index.test.js
  19. 265
      components/form-legacy/__tests__/label.test.js
  20. 129
      components/form-legacy/__tests__/message.test.js
  21. 91
      components/form-legacy/__tests__/type.tsx
  22. 2
      components/form-legacy/constants.tsx
  23. 16
      components/form-legacy/context.ts
  24. 130
      components/form-legacy/demo/advanced-search.md
  25. 73
      components/form-legacy/demo/coordinated.md
  26. 147
      components/form-legacy/demo/customized-form-controls.md
  27. 144
      components/form-legacy/demo/dynamic-form-item.md
  28. 92
      components/form-legacy/demo/dynamic-rule.md
  29. 113
      components/form-legacy/demo/form-in-modal.md
  30. 89
      components/form-legacy/demo/global-state.md
  31. 80
      components/form-legacy/demo/horizontal-login.md
  32. 72
      components/form-legacy/demo/layout.md
  33. 87
      components/form-legacy/demo/normal-login.md
  34. 271
      components/form-legacy/demo/register.md
  35. 158
      components/form-legacy/demo/style-check-debug.md
  36. 109
      components/form-legacy/demo/time-related-controls.md
  37. 217
      components/form-legacy/demo/validate-other.md
  38. 72
      components/form-legacy/demo/without-form-create.md
  39. 98
      components/form-legacy/docs/tutorial.md
  40. 254
      components/form-legacy/index.en-US.md
  41. 12
      components/form-legacy/index.tsx
  42. 256
      components/form-legacy/index.zh-CN.md
  43. 89
      components/form-legacy/interface.ts
  44. 681
      components/form-legacy/style/index.less
  45. 5
      components/form-legacy/style/index.tsx
  46. 125
      components/form-legacy/style/mixin.less
  47. 79
      components/form/Form.tsx
  48. 219
      components/form/FormItem.tsx
  49. 110
      components/form/FormItemInput.tsx
  50. 75
      components/form/FormItemLabel.tsx
  51. 33
      components/form/FormList.tsx
  52. 4992
      components/form/__tests__/__snapshots__/demo.test.js.snap
  53. 126
      components/form/__tests__/index.test.js
  54. 91
      components/form/__tests__/type.tsx
  55. 5
      components/form/changelog.md
  56. 47
      components/form/context.tsx
  57. 129
      components/form/demo/advanced-search.md
  58. 66
      components/form/demo/basic.md
  59. 103
      components/form/demo/control-hooks.md
  60. 109
      components/form/demo/control-ref.md
  61. 118
      components/form/demo/customized-form-controls.md
  62. 123
      components/form/demo/dynamic-form-item.md
  63. 92
      components/form/demo/dynamic-rule.md
  64. 137
      components/form/demo/form-context.md
  65. 119
      components/form/demo/form-in-modal.md
  66. 82
      components/form/demo/global-state.md
  67. 50
      components/form/demo/horizontal-login.md
  68. 65
      components/form/demo/layout.md
  69. 67
      components/form/demo/nest-messages.md
  70. 83
      components/form/demo/normal-login.md
  71. 259
      components/form/demo/register.md
  72. 93
      components/form/demo/time-related-controls.md
  73. 204
      components/form/demo/validate-other.md
  74. 8
      components/form/demo/validate-static.md
  75. 67
      components/form/demo/without-form-create.md
  76. 394
      components/form/index.en-US.md
  77. 22
      components/form/index.tsx
  78. 387
      components/form/index.zh-CN.md
  79. 1
      components/form/interface.ts
  80. 76
      components/form/style/components.less
  81. 1
      components/form/style/horizontal.less
  82. 446
      components/form/style/index.less
  83. 31
      components/form/style/inline.less
  84. 124
      components/form/style/mixin.less
  85. 61
      components/form/style/vertical.less
  86. 60
      components/form/util.ts
  87. 270
      components/form/v3.en-US.md
  88. 272
      components/form/v3.zh-CN.md
  89. 39
      components/input/__tests__/index.test.js
  90. 111
      components/mention/__tests__/__snapshots__/demo.test.js.snap
  91. 12
      components/mention/__tests__/index.test.js
  92. 102
      components/mention/demo/controlled.md
  93. 69
      components/mentions/__tests__/__snapshots__/demo.test.js.snap
  94. 95
      components/mentions/demo/form.md
  95. 20
      components/modal/__tests__/confirm.test.js
  96. 532
      components/table/__tests__/__snapshots__/demo.test.js.snap
  97. 4
      components/table/__tests__/demo.test.js
  98. 151
      components/table/demo/edit-cell.md
  99. 314
      components/table/demo/edit-row.md
  100. 2
      components/tabs/__tests__/demo.test.js

76
.circleci/config.yml

@ -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:

18
.travis.yml

@ -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

17
azure-pipelines.yml

@ -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

3
components/_util/__tests__/util.test.js

@ -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();
});
});

3
components/button/__tests__/index.test.js

@ -11,9 +11,6 @@ describe('Button', () => {
});
it('mount correctly', () => {
if (process.env.REACT === '15') {
return;
}
expect(() => renderer.create(<Button>Follow</Button>)).not.toThrow();
});

42
components/comment/__tests__/__snapshots__/demo.test.js.snap

@ -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>

75
components/config-provider/__tests__/__snapshots__/components.test.js.snap

@ -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>

1
components/config-provider/index.en-US.md

@ -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 |

29
components/config-provider/index.tsx

@ -1,6 +1,6 @@
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() {

1
components/config-provider/index.zh-CN.md

@ -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 |

129
components/drawer/demo/form-in-drawer.md

@ -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);
```

280
components/form-legacy/Form.tsx

@ -1,280 +0,0 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import classNames from 'classnames';
import createDOMForm from 'rc-form/lib/createDOMForm';
import createFormField from 'rc-form/lib/createFormField';
import omit from 'omit.js';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ColProps } from '../grid/col';
import { tuple } from '../_util/type';
import warning from '../_util/warning';
import FormItem, { FormLabelAlign } from './FormItem';
import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants';
import { FormContext } from './context';
import { FormWrappedProps } from './interface';
type FormCreateOptionMessagesCallback = (...args: any[]) => string;
interface FormCreateOptionMessages {
[messageId: string]: string | FormCreateOptionMessagesCallback | FormCreateOptionMessages;
}
export interface FormCreateOption<T> {
onFieldsChange?: (props: T, fields: any, allFields: any) => void;
onValuesChange?: (props: T, changedValues: any, allValues: any) => void;
mapPropsToFields?: (props: T) => void;
validateMessages?: FormCreateOptionMessages;
withRef?: boolean;
name?: string;
}
const FormLayouts = tuple('horizontal', 'inline', 'vertical');
export type FormLayout = (typeof FormLayouts)[number];
export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {
layout?: FormLayout;
form?: WrappedFormUtils;
onSubmit?: React.FormEventHandler<any>;
style?: React.CSSProperties;
className?: string;
prefixCls?: string;
hideRequiredMark?: boolean;
/**
* @since 3.14.0
*/
wrapperCol?: ColProps;
labelCol?: ColProps;
/**
* @since 3.15.0
*/
colon?: boolean;
labelAlign?: FormLabelAlign;
}
export type ValidationRule = {
/** validation error message */
message?: React.ReactNode;
/** built-in validation type, available options: https://github.com/yiminghe/async-validator#type */
type?: string;
/** indicates whether field is required */
required?: boolean;
/** treat required fields that only contain whitespace as errors */
whitespace?: boolean;
/** validate the exact length of a field */
len?: number;
/** validate the min length of a field */
min?: number;
/** validate the max length of a field */
max?: number;
/** validate the value from a list of possible values */
enum?: string | string[];
/** validate from a regular expression */
pattern?: RegExp;
/** transform a value before validation */
transform?: (value: any) => any;
/** custom validate function (Note: callback must be called) */
validator?: (rule: any, value: any, callback: any, source?: any, options?: any) => any;
};
export type ValidateCallback<V> = (errors: any, values: V) => void;
export type GetFieldDecoratorOptions = {
/** 子节点的值的属性,如 Checkbox 的是 'checked' */
valuePropName?: string;
/** 子节点的初始值,类型、可选值均由子节点决定 */
initialValue?: any;
/** 收集子节点的值的时机 */
trigger?: string;
/** 可以把 onChange 的参数转化为控件的值,例如 DatePicker 可设为:(date, dateString) => dateString */
getValueFromEvent?: (...args: any[]) => any;
/** Get the component props according to field value. */
getValueProps?: (value: any) => any;
/** 校验子节点值的时机 */
validateTrigger?: string | string[];
/** 校验规则,参见 [async-validator](https://github.com/yiminghe/async-validator) */
rules?: ValidationRule[];
/** 是否和其他控件互斥,特别用于 Radio 单选控件 */
exclusive?: boolean;
/** Normalize value to form component */
normalize?: (value: any, prevValue: any, allValues: any) => any;
/** Whether stop validate on first rule of error for this field. */
validateFirst?: boolean;
/** 是否一直保留子节点的信息 */
preserve?: boolean;
};
/** dom-scroll-into-view 组件配置参数 */
export type DomScrollIntoViewConfig = {
/** 是否和左边界对齐 */
alignWithLeft?: boolean;
/** 是否和上边界对齐 */
alignWithTop?: boolean;
/** 顶部偏移量 */
offsetTop?: number;
/** 左侧偏移量 */
offsetLeft?: number;
/** 底部偏移量 */
offsetBottom?: number;
/** 右侧偏移量 */
offsetRight?: number;
/** 是否允许容器水平滚动 */
allowHorizontalScroll?: boolean;
/** 当内容可见时是否允许滚动容器 */
onlyScrollIfNeeded?: boolean;
};
export type ValidateFieldsOptions = {
/** 所有表单域是否在第一个校验规则失败后停止继续校验 */
first?: boolean;
/** 指定哪些表单域在第一个校验规则失败后停止继续校验 */
firstFields?: string[];
/** 已经校验过的表单域,在 validateTrigger 再次被触发时是否再次校验 */
force?: boolean;
/** 定义 validateFieldsAndScroll 的滚动行为 */
scroll?: DomScrollIntoViewConfig;
};
// function create
export type WrappedFormUtils<V = any> = {
/** 获取一组输入控件的值,如不传入参数,则获取全部组件的值 */
getFieldsValue(fieldNames?: Array<string>): { [field: string]: any };
/** 获取一个输入控件的值 */
getFieldValue(fieldName: string): any;
/** 设置一组输入控件的值 */
setFieldsValue(obj: Object): void;
/** 设置一组输入控件的值 */
setFields(obj: Object): void;
/** 校验并获取一组输入域的值与 Error */
validateFields(
fieldNames: Array<string>,
options: ValidateFieldsOptions,
callback: ValidateCallback<V>,
): void;
validateFields(options: ValidateFieldsOptions, callback: ValidateCallback<V>): void;
validateFields(fieldNames: Array<string>, callback: ValidateCallback<V>): void;
validateFields(fieldNames: Array<string>, options: ValidateFieldsOptions): void;
validateFields(fieldNames: Array<string>): void;
validateFields(callback: ValidateCallback<V>): void;
validateFields(options: ValidateFieldsOptions): void;
validateFields(): void;
/** 与 `validateFields` 相似,但校验完后,如果校验不通过的菜单域不在可见范围内,则自动滚动进可见范围 */
validateFieldsAndScroll(
fieldNames: Array<string>,
options: ValidateFieldsOptions,
callback: ValidateCallback<V>,
): void;
validateFieldsAndScroll(options: ValidateFieldsOptions, callback: ValidateCallback<V>): void;
validateFieldsAndScroll(fieldNames: Array<string>, callback: ValidateCallback<V>): void;
validateFieldsAndScroll(fieldNames: Array<string>, options: ValidateFieldsOptions): void;
validateFieldsAndScroll(fieldNames: Array<string>): void;
validateFieldsAndScroll(callback: ValidateCallback<V>): void;
validateFieldsAndScroll(options: ValidateFieldsOptions): void;
validateFieldsAndScroll(): void;
/** 获取某个输入控件的 Error */
getFieldError(name: string): string[] | undefined;
getFieldsError(names?: Array<string>): Record<string, string[] | undefined>;
/** 判断一个输入控件是否在校验状态 */
isFieldValidating(name: string): boolean;
isFieldTouched(name: string): boolean;
isFieldsTouched(names?: Array<string>): boolean;
/** 重置一组输入控件的值与状态,如不传入参数,则重置所有组件 */
resetFields(names?: Array<string>): void;
// tslint:disable-next-line:max-line-length
getFieldDecorator<T extends Object = {}>(
id: keyof T,
options?: GetFieldDecoratorOptions,
): (node: React.ReactNode) => React.ReactNode;
};
export interface WrappedFormInternalProps<V = any> {
form: WrappedFormUtils<V>;
}
export interface RcBaseFormProps {
wrappedComponentRef?: any;
}
export interface FormComponentProps<V = any> extends WrappedFormInternalProps<V>, RcBaseFormProps {
form: WrappedFormUtils<V>;
}
export default class Form extends React.Component<FormProps, any> {
static defaultProps = {
colon: true,
layout: 'horizontal' as FormLayout,
hideRequiredMark: false,
onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
},
};
static propTypes = {
prefixCls: PropTypes.string,
layout: PropTypes.oneOf(FormLayouts),
children: PropTypes.any,
onSubmit: PropTypes.func,
hideRequiredMark: PropTypes.bool,
colon: PropTypes.bool,
};
static Item = FormItem;
static createFormField = createFormField;
static create = function<TOwnProps extends FormComponentProps>(
options: FormCreateOption<TOwnProps> = {},
): FormWrappedProps<TOwnProps> {
return createDOMForm({
fieldNameProp: 'id',
...options,
fieldMetaProp: FIELD_META_PROP,
fieldDataProp: FIELD_DATA_PROP,
});
};
constructor(props: FormProps) {
super(props);
warning(!props.form, 'Form', 'It is unnecessary to pass `form` to `Form` after antd@1.7.0.');
}
renderForm = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, hideRequiredMark, className = '', layout } = this.props;
const prefixCls = getPrefixCls('form', customizePrefixCls);
const formClassName = classNames(
prefixCls,
{
[`${prefixCls}-horizontal`]: layout === 'horizontal',
[`${prefixCls}-vertical`]: layout === 'vertical',
[`${prefixCls}-inline`]: layout === 'inline',
[`${prefixCls}-hide-required-mark`]: hideRequiredMark,
},
className,
);
const formProps = omit(this.props, [
'prefixCls',
'className',
'layout',
'form',
'hideRequiredMark',
'wrapperCol',
'labelAlign',
'labelCol',
'colon',
]);
return <form {...formProps} className={formClassName} />;
};
render() {
const { wrapperCol, labelAlign, labelCol, layout, colon } = this.props;
return (
<FormContext.Provider
value={{ wrapperCol, labelAlign, labelCol, vertical: layout === 'vertical', colon }}
>
<ConfigConsumer>{this.renderForm}</ConfigConsumer>
</FormContext.Provider>
);
}
}

421
components/form-legacy/FormItem.tsx

@ -1,421 +0,0 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as PropTypes from 'prop-types';
import classNames from 'classnames';
import Animate from 'rc-animate';
import Row from '../grid/row';
import Col, { ColProps } from '../grid/col';
import Icon from '../icon';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import warning from '../_util/warning';
import { tuple } from '../_util/type';
import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants';
import { FormContext, FormContextProps } from './context';
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
export type FormLabelAlign = 'left' | 'right';
export interface FormItemProps {
prefixCls?: string;
className?: string;
id?: string;
htmlFor?: string;
label?: React.ReactNode;
labelAlign?: FormLabelAlign;
labelCol?: ColProps;
wrapperCol?: ColProps;
help?: React.ReactNode;
extra?: React.ReactNode;
validateStatus?: (typeof ValidateStatuses)[number];
hasFeedback?: boolean;
required?: boolean;
style?: React.CSSProperties;
colon?: boolean;
}
function intersperseSpace<T>(list: Array<T>): Array<T | string> {
return list.reduce((current, item) => [...current, ' ', item], []).slice(1);
}
export default class FormItem extends React.Component<FormItemProps, any> {
static defaultProps = {
hasFeedback: false,
};
static propTypes = {
prefixCls: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
labelAlign: PropTypes.string,
labelCol: PropTypes.object,
help: PropTypes.oneOfType([PropTypes.node, PropTypes.bool]),
validateStatus: PropTypes.oneOf(ValidateStatuses),
hasFeedback: PropTypes.bool,
wrapperCol: PropTypes.object,
className: PropTypes.string,
id: PropTypes.string,
children: PropTypes.node,
colon: PropTypes.bool,
};
helpShow = false;
componentDidMount() {
const { children, help, validateStatus, id } = this.props;
warning(
this.getControls(children, true).length <= 1 ||
(help !== undefined || validateStatus !== undefined),
'Form.Item',
'Cannot generate `validateStatus` and `help` automatically, ' +
'while there are more than one `getFieldDecorator` in it.',
);
warning(
!id,
'Form.Item',
'`id` is deprecated for its label `htmlFor`. Please use `htmlFor` directly.',
);
}
getHelpMessage() {
const { help } = this.props;
if (help === undefined && this.getOnlyControl()) {
const { errors } = this.getField();
if (errors) {
return intersperseSpace(
errors.map((e: any, index: number) => {
let node: React.ReactElement<any> | null = null;
if (React.isValidElement(e)) {
node = e;
} else if (React.isValidElement(e.message)) {
node = e.message;
}
return node ? React.cloneElement(node, { key: index }) : e.message;
}),
);
}
return '';
}
return help;
}
getControls(children: React.ReactNode, recursively: boolean) {
let controls: React.ReactElement<any>[] = [];
const childrenArray = React.Children.toArray(children);
for (let i = 0; i < childrenArray.length; i++) {
if (!recursively && controls.length > 0) {
break;
}
const child = childrenArray[i] as React.ReactElement<any>;
if (
child.type &&
((child.type as any) === FormItem || (child.type as any).displayName === 'FormItem')
) {
continue;
}
if (!child.props) {
continue;
}
if (FIELD_META_PROP in child.props) {
// And means FIELD_DATA_PROP in child.props, too.
controls.push(child);
} else if (child.props.children) {
controls = controls.concat(this.getControls(child.props.children, recursively));
}
}
return controls;
}
getOnlyControl() {
const child = this.getControls(this.props.children, false)[0];
return child !== undefined ? child : null;
}
getChildProp(prop: string) {
const child = this.getOnlyControl() as React.ReactElement<any>;
return child && child.props && child.props[prop];
}
getId() {
return this.getChildProp('id');
}
getMeta() {
return this.getChildProp(FIELD_META_PROP);
}
getField() {
return this.getChildProp(FIELD_DATA_PROP);
}
onHelpAnimEnd = (_key: string, helpShow: boolean) => {
this.helpShow = helpShow;
if (!helpShow) {
this.setState({});
}
};
renderHelp(prefixCls: string) {
const help = this.getHelpMessage();
const children = help ? (
<div className={`${prefixCls}-explain`} key="help">
{help}
</div>
) : null;
if (children) {
this.helpShow = !!children;
}
return (
<Animate
transitionName="show-help"
component=""
transitionAppear
key="help"
onEnd={this.onHelpAnimEnd}
>
{children}
</Animate>
);
}
renderExtra(prefixCls: string) {
const { extra } = this.props;
return extra ? <div className={`${prefixCls}-extra`}>{extra}</div> : null;
}
getValidateStatus() {
const onlyControl = this.getOnlyControl();
if (!onlyControl) {
return '';
}
const field = this.getField();
if (field.validating) {
return 'validating';
}
if (field.errors) {
return 'error';
}
const fieldValue = 'value' in field ? field.value : this.getMeta().initialValue;
if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
return 'success';
}
return '';
}
renderValidateWrapper(
prefixCls: string,
c1: React.ReactNode,
c2: React.ReactNode,
c3: React.ReactNode,
) {
const { props } = this;
const onlyControl = this.getOnlyControl;
const validateStatus =
props.validateStatus === undefined && onlyControl
? this.getValidateStatus()
: props.validateStatus;
let classes = `${prefixCls}-item-control`;
if (validateStatus) {
classes = classNames(`${prefixCls}-item-control`, {
'has-feedback': props.hasFeedback || validateStatus === 'validating',
'has-success': validateStatus === 'success',
'has-warning': validateStatus === 'warning',
'has-error': validateStatus === 'error',
'is-validating': validateStatus === 'validating',
});
}
let iconType = '';
switch (validateStatus) {
case 'success':
iconType = 'check-circle';
break;
case 'warning':
iconType = 'exclamation-circle';
break;
case 'error':
iconType = 'close-circle';
break;
case 'validating':
iconType = 'loading';
break;
default:
iconType = '';
break;
}
const icon =
props.hasFeedback && iconType ? (
<span className={`${prefixCls}-item-children-icon`}>
<Icon type={iconType} theme={iconType === 'loading' ? 'outlined' : 'filled'} />
</span>
) : null;
return (
<div className={classes}>
<span className={`${prefixCls}-item-children`}>
{c1}
{icon}
</span>
{c2}
{c3}
</div>
);
}
renderWrapper(prefixCls: string, children: React.ReactNode) {
return (
<FormContext.Consumer key="wrapper">
{({ wrapperCol: contextWrapperCol, vertical }: FormContextProps) => {
const { wrapperCol } = this.props;
const mergedWrapperCol: ColProps =
('wrapperCol' in this.props ? wrapperCol : contextWrapperCol) || {};
const className = classNames(
`${prefixCls}-item-control-wrapper`,
mergedWrapperCol.className,
);
// No pass FormContext since it's useless
return (
<FormContext.Provider value={{ vertical }}>
<Col {...mergedWrapperCol} className={className}>
{children}
</Col>
</FormContext.Provider>
);
}}
</FormContext.Consumer>
);
}
isRequired() {
const { required } = this.props;
if (required !== undefined) {
return required;
}
if (this.getOnlyControl()) {
const meta = this.getMeta() || {};
const validate = meta.validate || [];
return validate
.filter((item: any) => !!item.rules)
.some((item: any) => {
return item.rules.some((rule: any) => rule.required);
});
}
return false;
}
// Resolve duplicated ids bug between different forms
// https://github.com/ant-design/ant-design/issues/7351
onLabelClick = () => {
const id = this.props.id || this.getId();
if (!id) {
return;
}
const formItemNode = ReactDOM.findDOMNode(this) as Element;
const control = formItemNode.querySelector(`[id="${id}"]`) as HTMLElement;
if (control && control.focus) {
control.focus();
}
};
renderLabel(prefixCls: string) {
return (
<FormContext.Consumer key="label">
{({
vertical,
labelAlign: contextLabelAlign,
labelCol: contextLabelCol,
colon: contextColon,
}: FormContextProps) => {
const { label, labelCol, labelAlign, colon, id, htmlFor } = this.props;
const required = this.isRequired();
const mergedLabelCol: ColProps =
('labelCol' in this.props ? labelCol : contextLabelCol) || {};
const mergedLabelAlign: FormLabelAlign | undefined =
'labelAlign' in this.props ? labelAlign : contextLabelAlign;
const labelClsBasic = `${prefixCls}-item-label`;
const labelColClassName = classNames(
labelClsBasic,
mergedLabelAlign === 'left' && `${labelClsBasic}-left`,
mergedLabelCol.className,
);
let labelChildren = label;
// Keep label is original where there should have no colon
const computedColon = colon === true || (contextColon !== false && colon !== false);
const haveColon = computedColon && !vertical;
// Remove duplicated user input colon
if (haveColon && typeof label === 'string' && (label as string).trim() !== '') {
labelChildren = (label as string).replace(/[:|:]\s*$/, '');
}
const labelClassName = classNames({
[`${prefixCls}-item-required`]: required,
[`${prefixCls}-item-no-colon`]: !computedColon,
});
return label ? (
<Col {...mergedLabelCol} className={labelColClassName}>
<label
htmlFor={htmlFor || id || this.getId()}
className={labelClassName}
title={typeof label === 'string' ? label : ''}
onClick={this.onLabelClick}
>
{labelChildren}
</label>
</Col>
) : null;
}}
</FormContext.Consumer>
);
}
renderChildren(prefixCls: string) {
const { children } = this.props;
return [
this.renderLabel(prefixCls),
this.renderWrapper(
prefixCls,
this.renderValidateWrapper(
prefixCls,
children,
this.renderHelp(prefixCls),
this.renderExtra(prefixCls),
),
),
];
}
renderFormItem = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, style, className } = this.props;
const prefixCls = getPrefixCls('form', customizePrefixCls);
const children = this.renderChildren(prefixCls);
const itemClassName = {
[`${prefixCls}-item`]: true,
[`${prefixCls}-item-with-help`]: this.helpShow,
[`${className}`]: !!className,
};
return (
<Row className={classNames(itemClassName)} style={style} key="row">
{children}
</Row>
);
};
render() {
return <ConfigConsumer>{this.renderFormItem}</ConfigConsumer>;
}
}

5260
components/form-legacy/__tests__/__snapshots__/demo.test.js.snap

File diff suppressed because it is too large

149
components/form-legacy/__tests__/__snapshots__/label.test.js.snap

@ -1,149 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Form should \`labelAlign\` work in FormItem 1`] = `
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-label ant-form-item-label-left"
>
<label
class=""
title="test"
>
test
</label>
</div>
<div
class="ant-col ant-form-item-control-wrapper"
>
<div
class="ant-form-item-control"
>
<span
class="ant-form-item-children"
/>
</div>
</div>
</div>
`;
exports[`Form should \`labelAlign\` work in FormItem 2`] = `
<form
class="ant-form ant-form-horizontal"
>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-label ant-form-item-label-left"
>
<label
class=""
title="test"
>
test
</label>
</div>
<div
class="ant-col ant-form-item-control-wrapper"
>
<div
class="ant-form-item-control"
>
<span
class="ant-form-item-children"
/>
</div>
</div>
</div>
</form>
`;
exports[`Form should props colon of Form.Item override the props colon of Form. 1`] = `
<form
class="ant-form ant-form-horizontal"
>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-label"
>
<label
class="ant-form-item-no-colon"
title="label"
>
label
</label>
</div>
<div
class="ant-col ant-form-item-control-wrapper"
>
<div
class="ant-form-item-control"
>
<span
class="ant-form-item-children"
>
input
</span>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-label"
>
<label
class=""
title="label"
>
label
</label>
</div>
<div
class="ant-col ant-form-item-control-wrapper"
>
<div
class="ant-form-item-control"
>
<span
class="ant-form-item-children"
>
input
</span>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-label"
>
<label
class="ant-form-item-no-colon"
title="label"
>
label
</label>
</div>
<div
class="ant-col ant-form-item-control-wrapper"
>
<div
class="ant-form-item-control"
>
<span
class="ant-form-item-children"
>
input
</span>
</div>
</div>
</div>
</form>
`;

151
components/form-legacy/__tests__/__snapshots__/message.test.js.snap

@ -1,151 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Form should display custom message 1`] = `
<form
class="ant-form ant-form-horizontal"
>
<div
class="ant-row ant-form-item ant-form-item-with-help"
>
<div
class="ant-col ant-form-item-label"
>
<label
class=""
for="account"
title="Account"
>
Account
</label>
</div>
<div
class="ant-col ant-form-item-control-wrapper"
>
<div
class="ant-form-item-control has-error"
>
<span
class="ant-form-item-children"
>
<input
data-__field="[object Object]"
data-__meta="[object Object]"
id="account"
value="antd"
/>
</span>
<div
class="ant-form-explain show-help-enter"
>
<span>
Account does not exist,
<a
href="https://www.alipay.com/"
rel="noopener noreferrer"
target="_blank"
>
Forgot account?
</a>
</span>
</div>
</div>
</div>
</div>
</form>
`;
exports[`Form should display two message 1`] = `
<form
class="ant-form ant-form-horizontal"
>
<div
class="ant-row ant-form-item ant-form-item-with-help"
>
<div
class="ant-col ant-form-item-label"
>
<label
class=""
for="account"
title="Account"
>
Account
</label>
</div>
<div
class="ant-col ant-form-item-control-wrapper"
>
<div
class="ant-form-item-control has-error"
>
<span
class="ant-form-item-children"
>
<input
data-__field="[object Object]"
data-__meta="[object Object]"
id="account"
value="+=-/"
/>
</span>
<div
class="ant-form-explain show-help-enter"
>
Error message 1 Error message 2
</div>
</div>
</div>
</div>
</form>
`;
exports[`Form support error message with reactNode 1`] = `
<form
class="ant-form ant-form-horizontal"
>
<div
class="ant-row ant-form-item ant-form-item-with-help"
>
<div
class="ant-col ant-form-item-label"
>
<label
class=""
for="account"
title="Account"
>
Account
</label>
</div>
<div
class="ant-col ant-form-item-control-wrapper"
>
<div
class="ant-form-item-control has-error"
>
<span
class="ant-form-item-children"
>
<input
data-__field="[object Object]"
data-__meta="[object Object]"
id="account"
value=""
/>
</span>
<div
class="ant-form-explain show-help-enter"
>
<div>
Error 1
</div>
<div>
Error 2
</div>
</div>
</div>
</div>
</div>
</form>
`;

3
components/form-legacy/__tests__/demo.test.js

@ -1,3 +0,0 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('form');

45
components/form-legacy/__tests__/index.test.js

@ -1,45 +0,0 @@
/* eslint-disable react/prefer-stateless-function */
import React from 'react';
import { mount } from 'enzyme';
import Form from '..';
describe('Form', () => {
it('hideRequiredMark', () => {
const wrapper = mount(<Form hideRequiredMark />);
expect(wrapper.find('form').hasClass('ant-form-hide-required-mark')).toBe(true);
});
describe('wrappedComponentRef', () => {
it('warns on functional component', () => {
if (process.env.REACT === '15') {
return;
}
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
const TestForm = () => <Form />;
const Wrapped = Form.create()(TestForm);
mount(<Wrapped wrappedComponentRef={() => {}} />);
expect(spy).toHaveBeenCalled();
spy.mockReset();
spy.mockRestore();
});
it('get component ref', () => {
class TestForm extends React.Component {
// eslint-disable-line
render() {
return <Form />;
}
}
const Wrapped = Form.create()(TestForm);
let form;
mount(
<Wrapped
wrappedComponentRef={node => {
form = node;
}}
/>,
);
expect(form).toBeInstanceOf(TestForm);
});
});
});

265
components/form-legacy/__tests__/label.test.js

@ -1,265 +0,0 @@
import React from 'react';
import { mount, render } from 'enzyme';
import Form from '..';
describe('Form', () => {
// Mock of `querySelector`
const originQuerySelector = HTMLElement.prototype.querySelector;
HTMLElement.prototype.querySelector = function querySelector(str) {
const match = str.match(/^\[id=('|")(.*)('|")]$/);
const id = match && match[2];
// Use origin logic
if (id) {
const [input] = this.getElementsByTagName('input');
if (input && input.id === id) {
return input;
}
}
return originQuerySelector.call(this, str);
};
afterAll(() => {
HTMLElement.prototype.querySelector = originQuerySelector;
});
it('should remove duplicated user input colon', () => {
const wrapper = mount(
<Form>
<Form.Item label="label:">input</Form.Item>
<Form.Item label="label:">input</Form.Item>
</Form>,
);
expect(
wrapper
.find('.ant-form-item-label label')
.at(0)
.text(),
).not.toContain(':');
expect(
wrapper
.find('.ant-form-item-label label')
.at(1)
.text(),
).not.toContain(':');
});
it('should disable colon when props colon Form is false', () => {
const wrapper = mount(
<Form colon={false}>
<Form.Item label="label">input</Form.Item>
</Form>,
);
expect(
wrapper
.find('.ant-form-item-label label')
.at(0)
.hasClass('ant-form-item-no-colon'),
).toBe(true);
});
it('should props colon of Form.Item override the props colon of Form.', () => {
const wrapper = mount(
<Form colon={false}>
<Form.Item label="label">input</Form.Item>
<Form.Item label="label" colon>
input
</Form.Item>
<Form.Item label="label" colon={false}>
input
</Form.Item>
</Form>,
);
expect(wrapper.render()).toMatchSnapshot();
const testLabel = mount(
<Form colon={false}>
<Form.Item label="label:" colon>
input
</Form.Item>
<Form.Item label="label:" colon>
input
</Form.Item>
</Form>,
);
expect(
testLabel
.find('.ant-form-item-label label')
.at(0)
.text(),
).not.toContain(':');
expect(
testLabel
.find('.ant-form-item-label label')
.at(1)
.text(),
).not.toContain(':');
});
it('should not remove duplicated user input colon when props colon is false', () => {
const wrapper = mount(
<Form>
<Form.Item label="label:" colon={false}>
input
</Form.Item>
<Form.Item label="label:" colon={false}>
input
</Form.Item>
</Form>,
);
expect(
wrapper
.find('.ant-form-item-label label')
.at(0)
.text(),
).toContain(':');
expect(
wrapper
.find('.ant-form-item-label label')
.at(1)
.text(),
).toContain(':');
});
it('should not remove duplicated user input colon when layout is vertical', () => {
const wrapper = mount(
<Form layout="vertical">
<Form.Item label="label:">input</Form.Item>
<Form.Item label="label:">input</Form.Item>
</Form>,
);
expect(
wrapper
.find('.ant-form-item-label label')
.at(0)
.text(),
).toContain(':');
expect(
wrapper
.find('.ant-form-item-label label')
.at(1)
.text(),
).toContain(':');
});
it('should has dom with .ant-form-item-control-wrapper', () => {
const formItemLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 14 },
};
const wrapper = mount(
<Form>
<Form.Item {...formItemLayout}>input</Form.Item>
<Form.Item>input</Form.Item>
</Form>,
);
expect(wrapper.find('.ant-form-item-control-wrapper').hostNodes().length).toBe(2);
expect(wrapper.find('.ant-form-item-control-wrapper.ant-col-14').length).toBe(1);
});
// https://github.com/ant-design/ant-design/issues/7351
it('focus correct input when click label', () => {
const Form1 = Form.create()(({ form }) => (
<Form>
<Form.Item label="label 1">{form.getFieldDecorator('test')(<input />)}</Form.Item>
</Form>
));
const Form2 = Form.create()(({ form }) => (
<Form>
<Form.Item label="label 2">{form.getFieldDecorator('test2')(<input />)}</Form.Item>
</Form>
));
const wrapper = mount(
<div>
<Form1 />
<Form2 />
</div>,
);
wrapper
.find('Form label')
.at(0)
.simulate('click');
expect(
wrapper
.find('Form input')
.at(0)
.getDOMNode(),
).toBe(document.activeElement);
wrapper
.find('Form label')
.at(1)
.simulate('click');
expect(
wrapper
.find('Form input')
.at(1)
.getDOMNode(),
).toBe(document.activeElement);
});
// https://github.com/ant-design/ant-design/issues/7693
it('should not throw error when is not a valid id', () => {
const Form1 = Form.create()(({ form }) => (
<Form>
<Form.Item label="label 1">
{form.getFieldDecorator('member[0].name.firstname')(<input />)}
</Form.Item>
</Form>
));
const wrapper = mount(<Form1 />);
expect(() => {
wrapper
.find('Form label')
.at(0)
.simulate('click');
}).not.toThrow();
expect(
wrapper
.find('Form input')
.at(0)
.getDOMNode(),
).toBe(document.activeElement);
});
it('should `labelAlign` work in FormItem', () => {
// Works in Form.Item
expect(render(<Form.Item label="test" labelAlign="left" />)).toMatchSnapshot();
// Use Form.Item first
expect(
render(
<Form labelAlign="right">
<Form.Item label="test" labelAlign="left" />
</Form>,
),
).toMatchSnapshot();
});
describe('should `htmlFor` work', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
afterEach(() => {
errorSpy.mockReset();
});
afterAll(() => {
errorSpy.mockRestore();
});
it('should warning when use `id`', () => {
mount(<Form.Item id="bamboo" label="bamboo" />);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Form.Item] `id` is deprecated for its label `htmlFor`. Please use `htmlFor` directly.',
);
});
it('use `htmlFor`', () => {
const wrapper = mount(<Form.Item htmlFor="bamboo" label="bamboo" />);
expect(wrapper.find('label').props().htmlFor).toBe('bamboo');
});
});
});

129
components/form-legacy/__tests__/message.test.js

@ -1,129 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import Form from '..';
describe('Form', () => {
it('should display two message', () => {
const rules = [
{
pattern: /^\w+$/,
message: 'Error message 1',
},
{
pattern: /^\w+$/,
message: 'Error message 2',
},
];
let myForm;
const Form1 = Form.create()(({ form }) => {
myForm = form;
return (
<Form>
<Form.Item label="Account">
{form.getFieldDecorator('account', { initialValue: '+=-/', rules })(<input />)}
</Form.Item>
</Form>
);
});
const wrapper = mount(<Form1 />);
myForm.validateFields();
wrapper.update();
expect(wrapper.render()).toMatchSnapshot();
});
it('should display custom message', () => {
const rules = [
{
pattern: /^$/,
message: (
<span>
Account does not exist,{' '}
<a rel="noopener noreferrer" href="https://www.alipay.com/" target="_blank">
Forgot account?
</a>
</span>
),
},
];
let myForm;
const Form1 = Form.create()(({ form }) => {
myForm = form;
return (
<Form>
<Form.Item label="Account">
{form.getFieldDecorator('account', { initialValue: 'antd', rules })(<input />)}
</Form.Item>
</Form>
);
});
const wrapper = mount(<Form1 />);
myForm.validateFields();
wrapper.update();
expect(wrapper.render()).toMatchSnapshot();
});
it('support error message with reactNode', () => {
let myForm;
const Form1 = Form.create()(({ form }) => {
myForm = form;
return (
<Form>
<Form.Item label="Account">{form.getFieldDecorator('account')(<input />)}</Form.Item>
</Form>
);
});
const wrapper = mount(<Form1 />);
myForm.setFields({
account: {
errors: [<div>Error 1</div>, <div>Error 2</div>],
},
});
expect(wrapper.render()).toMatchSnapshot();
});
it('should print warning for not generating help and validateStatus automatically', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const Form1 = Form.create()(({ form }) => {
return (
<Form>
<Form.Item label="Account">
{form.getFieldDecorator('account')(<input />)}
{form.getFieldDecorator('account')(<input />)}
</Form.Item>
</Form>
);
});
mount(<Form1 />);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Form.Item] Cannot generate `validateStatus` and `help` automatically, while there are more than one `getFieldDecorator` in it.',
);
errorSpy.mockRestore();
});
// https://github.com/ant-design/ant-design/issues/14911
it('should not print warning for not generating help and validateStatus automatically when help or validateStatus is specified', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const Form1 = Form.create()(({ form }) => {
return (
<Form>
<Form.Item label="Account" help="custom help information">
{form.getFieldDecorator('account')(<input />)}
{form.getFieldDecorator('account')(<input />)}
</Form.Item>
</Form>
);
});
mount(<Form1 />);
expect(errorSpy).not.toHaveBeenCalled();
errorSpy.mockRestore();
});
});

91
components/form-legacy/__tests__/type.tsx

@ -1,91 +0,0 @@
/* tslint:disable */
import * as React from 'react';
import Form, { FormComponentProps, FormCreateOption } from '../Form';
// test Form.create on component without own props
class WithoutOwnProps extends React.Component<any, any> {
state = {
foo: 'bar',
};
render() {
return <div>foo</div>;
}
}
const WithoutOwnPropsForm = Form.create()(WithoutOwnProps);
<WithoutOwnPropsForm />;
// test Form.create on component with own props
interface WithOwnPropsProps extends FormComponentProps {
name: string;
}
class WithOwnProps extends React.Component<WithOwnPropsProps, any> {
state = {
foo: 'bar',
};
render() {
return <div>foo</div>;
}
}
const WithOwnPropsForm = Form.create<WithOwnPropsProps>()(WithOwnProps);
<WithOwnPropsForm name="foo" />;
// test Form.create with options
interface WithCreateOptionsProps extends FormComponentProps {
username: string;
}
class WithCreateOptions extends React.Component<WithCreateOptionsProps, {}> {
render() {
return <div>foo</div>;
}
}
const mapPropsToFields = (props: WithCreateOptionsProps) => {
const { username } = props;
return {
username: Form.createFormField({ value: username }),
};
};
const formOptions: FormCreateOption<WithCreateOptionsProps> = { mapPropsToFields };
const WithCreateOptionsForm = Form.create(formOptions)(WithCreateOptions);
<WithCreateOptionsForm username="foo" />;
// Should work with forwardRef & wrappedComponentRef
// https://github.com/ant-design/ant-design/issues/16229
if (React.forwardRef) {
interface ForwardProps extends FormComponentProps {
str: string;
}
const ForwardDemo = React.forwardRef(({ str }: ForwardProps, ref: React.Ref<HTMLDivElement>) => {
return <div ref={ref}>{str || ''}</div>;
});
const WrappedForwardDemo = Form.create<ForwardProps>()(ForwardDemo);
<WrappedForwardDemo str="" />;
}
interface WrappedRefProps extends FormComponentProps {
str: string;
test?: () => void;
}
class WrapRefDemo extends React.Component<WrappedRefProps> {
public getForm() {
return this.props.form;
}
public render() {
return <div>{this.props.str || ''}</div>;
}
}
const WrappedWrapRefDemo = Form.create<WrappedRefProps>()(WrapRefDemo);
<WrappedWrapRefDemo str="" wrappedComponentRef={() => null} />;

2
components/form-legacy/constants.tsx

@ -1,2 +0,0 @@
export const FIELD_META_PROP = 'data-__meta';
export const FIELD_DATA_PROP = 'data-__field';

16
components/form-legacy/context.ts

@ -1,16 +0,0 @@
import createReactContext from '@ant-design/create-react-context';
import { ColProps } from '../grid/col';
import { FormLabelAlign } from './FormItem';
export interface FormContextProps {
vertical: boolean;
colon?: boolean;
labelAlign?: FormLabelAlign;
labelCol?: ColProps;
wrapperCol?: ColProps;
}
export const FormContext = createReactContext<FormContextProps>({
labelAlign: 'right',
vertical: false,
});

130
components/form-legacy/demo/advanced-search.md

@ -1,130 +0,0 @@
---
order: 3
title:
zh-CN: 高级搜索
en-US: Advanced search
---
## zh-CN
三列栅格式的表单排列方式,常用于数据表格的高级搜索。
有部分定制的样式代码,由于输入标签长度不确定,需要根据具体情况自行调整。
## en-US
Three columns layout is often used for advanced searching of data table.
Because the width of label is not fixed, you may need to adjust it by customizing its style.
```jsx
import { Form, Row, Col, Input, Button, Icon } from 'antd';
class AdvancedSearchForm extends React.Component {
state = {
expand: false,
};
// To generate mock Form.Item
getFields() {
const count = this.state.expand ? 10 : 6;
const { getFieldDecorator } = this.props.form;
const children = [];
for (let i = 0; i < 10; i++) {
children.push(
<Col span={8} key={i} style={{ display: i < count ? 'block' : 'none' }}>
<Form.Item label={`Field ${i}`}>
{getFieldDecorator(`field-${i}`, {
rules: [
{
required: true,
message: 'Input something!',
},
],
})(<Input placeholder="placeholder" />)}
</Form.Item>
</Col>,
);
}
return children;
}
handleSearch = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
console.log('Received values of form: ', values);
});
};
handleReset = () => {
this.props.form.resetFields();
};
toggle = () => {
const { expand } = this.state;
this.setState({ expand: !expand });
};
render() {
return (
<Form className="ant-advanced-search-form" onSubmit={this.handleSearch}>
<Row gutter={24}>{this.getFields()}</Row>
<Row>
<Col span={24} style={{ textAlign: 'right' }}>
<Button type="primary" htmlType="submit">
Search
</Button>
<Button style={{ marginLeft: 8 }} onClick={this.handleReset}>
Clear
</Button>
<a style={{ marginLeft: 8, fontSize: 12 }} onClick={this.toggle}>
Collapse <Icon type={this.state.expand ? 'up' : 'down'} />
</a>
</Col>
</Row>
</Form>
);
}
}
const WrappedAdvancedSearchForm = Form.create({ name: 'advanced_search' })(AdvancedSearchForm);
ReactDOM.render(
<div>
<WrappedAdvancedSearchForm />
<div className="search-result-list">Search Result List</div>
</div>,
mountNode,
);
```
```css
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
```
<style>
#components-form-demo-advanced-search .ant-form {
max-width: none;
}
#components-form-demo-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
</style>

73
components/form-legacy/demo/coordinated.md

@ -1,73 +0,0 @@
---
order: 11
title:
zh-CN: 表单联动
en-US: Coordinated Controls
---
## zh-CN
使用 `setFieldsValue` 来动态设置其他控件的值。
## en-US
Use `setFieldsValue` to set other control's value programmaticly.
```jsx
import { Form, Select, Input, Button } from 'antd';
const { Option } = Select;
class App extends React.Component {
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};
handleSelectChange = value => {
console.log(value);
this.props.form.setFieldsValue({
note: `Hi, ${value === 'male' ? 'man' : 'lady'}!`,
});
};
render() {
const { getFieldDecorator } = this.props.form;
return (
<Form labelCol={{ span: 5 }} wrapperCol={{ span: 12 }} onSubmit={this.handleSubmit}>
<Form.Item label="Note">
{getFieldDecorator('note', {
rules: [{ required: true, message: 'Please input your note!' }],
})(<Input />)}
</Form.Item>
<Form.Item label="Gender">
{getFieldDecorator('gender', {
rules: [{ required: true, message: 'Please select your gender!' }],
})(
<Select
placeholder="Select a option and change input text above"
onChange={this.handleSelectChange}
>
<Option value="male">male</Option>
<Option value="female">female</Option>
</Select>,
)}
</Form.Item>
<Form.Item wrapperCol={{ span: 12, offset: 5 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
}
const WrappedApp = Form.create({ name: 'coordinated' })(App);
ReactDOM.render(<WrappedApp />, mountNode);
```

147
components/form-legacy/demo/customized-form-controls.md

@ -1,147 +0,0 @@
---
order: 7
title:
zh-CN: 自定义表单控件
en-US: Customized Form Controls
---
## zh-CN
自定义或第三方的表单控件,也可以与 Form 组件一起使用。只要该组件遵循以下的约定:
> - 提供受控属性 `value` 或其它与 [`valuePropName`](http://ant.design/components/form/#getFieldDecorator-参数) 的值同名的属性。
> - 提供 `onChange` 事件或 [`trigger`](http://ant.design/components/form/#getFieldDecorator-参数) 的值同名的事件。
> - 支持 ref:
> - React@16.3.0 之前只有 Class 组件支持。
> - React@16.3.0 及之后可以通过 [forwardRef](https://reactjs.org/docs/forwarding-refs.html) 添加 ref 支持。([示例](https://codesandbox.io/s/7wj199900x))
## en-US
Customized or third-party form controls can be used in Form, too. Controls must follow these conventions:
> - It has a controlled property `value` or other name which is equal to the value of [`valuePropName`](http://ant.design/components/form/?locale=en-US#getFieldDecorator's-parameters).
> - It has event `onChange` or an event which name is equal to the value of [`trigger`](http://ant.design/components/form/?locale=en-US#getFieldDecorator's-parameters).
> - Support ref:
> - Can only use class component before React@16.3.0.
> - Can use [forwardRef](https://reactjs.org/docs/forwarding-refs.html) to add ref support after React@16.3.0. ([Sample](https://codesandbox.io/s/7wj199900x))
```jsx
import { Form, Input, Select, Button } from 'antd';
const { Option } = Select;
class PriceInput extends React.Component {
static getDerivedStateFromProps(nextProps) {
// Should be a controlled component.
if ('value' in nextProps) {
return {
...(nextProps.value || {}),
};
}
return null;
}
constructor(props) {
super(props);
const value = props.value || {};
this.state = {
number: value.number || 0,
currency: value.currency || 'rmb',
};
}
handleNumberChange = e => {
const number = parseInt(e.target.value || 0, 10);
if (Number.isNaN(number)) {
return;
}
if (!('value' in this.props)) {
this.setState({ number });
}
this.triggerChange({ number });
};
handleCurrencyChange = currency => {
if (!('value' in this.props)) {
this.setState({ currency });
}
this.triggerChange({ currency });
};
triggerChange = changedValue => {
// Should provide an event to pass value to Form.
const { onChange } = this.props;
if (onChange) {
onChange(Object.assign({}, this.state, changedValue));
}
};
render() {
const { size } = this.props;
const { state } = this;
return (
<span>
<Input
type="text"
size={size}
value={state.number}
onChange={this.handleNumberChange}
style={{ width: '65%', marginRight: '3%' }}
/>
<Select
value={state.currency}
size={size}
style={{ width: '32%' }}
onChange={this.handleCurrencyChange}
>
<Option value="rmb">RMB</Option>
<Option value="dollar">Dollar</Option>
</Select>
</span>
);
}
}
class Demo extends React.Component {
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};
checkPrice = (rule, value, callback) => {
if (value.number > 0) {
callback();
return;
}
callback('Price must greater than zero!');
};
render() {
const { getFieldDecorator } = this.props.form;
return (
<Form layout="inline" onSubmit={this.handleSubmit}>
<Form.Item label="Price">
{getFieldDecorator('price', {
initialValue: { number: 0, currency: 'rmb' },
rules: [{ validator: this.checkPrice }],
})(<PriceInput />)}
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
}
const WrappedDemo = Form.create({ name: 'customized_form_controls' })(Demo);
ReactDOM.render(<WrappedDemo />, mountNode);
```

144
components/form-legacy/demo/dynamic-form-item.md

@ -1,144 +0,0 @@
---
order: 5
title:
zh-CN: 动态增减表单项
en-US: Dynamic Form Item
---
## zh-CN
动态增加、减少表单项。
## en-US
Add or remove form items dynamically.
```jsx
import { Form, Input, Icon, Button } from 'antd';
let id = 0;
class DynamicFieldSet extends React.Component {
remove = k => {
const { form } = this.props;
// can use data-binding to get
const keys = form.getFieldValue('keys');
// We need at least one passenger
if (keys.length === 1) {
return;
}
// can use data-binding to set
form.setFieldsValue({
keys: keys.filter(key => key !== k),
});
};
add = () => {
const { form } = this.props;
// can use data-binding to get
const keys = form.getFieldValue('keys');
const nextKeys = keys.concat(id++);
// can use data-binding to set
// important! notify form to detect changes
form.setFieldsValue({
keys: nextKeys,
});
};
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
const { keys, names } = values;
console.log('Received values of form: ', values);
console.log('Merged values:', keys.map(key => names[key]));
}
});
};
render() {
const { getFieldDecorator, getFieldValue } = this.props.form;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 4 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 20 },
},
};
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: { span: 24, offset: 0 },
sm: { span: 20, offset: 4 },
},
};
getFieldDecorator('keys', { initialValue: [] });
const keys = getFieldValue('keys');
const formItems = keys.map((k, index) => (
<Form.Item
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
label={index === 0 ? 'Passengers' : ''}
required={false}
key={k}
>
{getFieldDecorator(`names[${k}]`, {
validateTrigger: ['onChange', 'onBlur'],
rules: [
{
required: true,
whitespace: true,
message: "Please input passenger's name or delete this field.",
},
],
})(<Input placeholder="passenger name" style={{ width: '60%', marginRight: 8 }} />)}
{keys.length > 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.remove(k)}
/>
) : null}
</Form.Item>
));
return (
<Form onSubmit={this.handleSubmit}>
{formItems}
<Form.Item {...formItemLayoutWithOutLabel}>
<Button type="dashed" onClick={this.add} style={{ width: '60%' }}>
<Icon type="plus" /> Add field
</Button>
</Form.Item>
<Form.Item {...formItemLayoutWithOutLabel}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
}
const WrappedDynamicFieldSet = Form.create({ name: 'dynamic_form_item' })(DynamicFieldSet);
ReactDOM.render(<WrappedDynamicFieldSet />, mountNode);
```
```css
.dynamic-delete-button {
cursor: pointer;
position: relative;
top: 4px;
font-size: 24px;
color: #999;
transition: all 0.3s;
}
.dynamic-delete-button:hover {
color: #777;
}
.dynamic-delete-button[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
```

92
components/form-legacy/demo/dynamic-rule.md

@ -1,92 +0,0 @@
---
order: 13
title:
zh-CN: 动态校验规则
en-US: Dynamic Rules
---
## zh-CN
根据不同情况执行不同的校验规则。
## en-US
Perform different check rules according to different situations.
```jsx
import { Form, Input, Button, Checkbox } from 'antd';
const formItemLayout = {
labelCol: { span: 4 },
wrapperCol: { span: 8 },
};
const formTailLayout = {
labelCol: { span: 4 },
wrapperCol: { span: 8, offset: 4 },
};
class DynamicRule extends React.Component {
state = {
checkNick: false,
};
check = () => {
this.props.form.validateFields(err => {
if (!err) {
console.info('success');
}
});
};
handleChange = e => {
this.setState(
{
checkNick: e.target.checked,
},
() => {
this.props.form.validateFields(['nickname'], { force: true });
},
);
};
render() {
const { getFieldDecorator } = this.props.form;
return (
<div>
<Form.Item {...formItemLayout} label="Name">
{getFieldDecorator('username', {
rules: [
{
required: true,
message: 'Please input your name',
},
],
})(<Input placeholder="Please input your name" />)}
</Form.Item>
<Form.Item {...formItemLayout} label="Nickname">
{getFieldDecorator('nickname', {
rules: [
{
required: this.state.checkNick,
message: 'Please input your nickname',
},
],
})(<Input placeholder="Please input your nickname" />)}
</Form.Item>
<Form.Item {...formTailLayout}>
<Checkbox checked={this.state.checkNick} onChange={this.handleChange}>
Nickname is required
</Checkbox>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" onClick={this.check}>
Check
</Button>
</Form.Item>
</div>
);
}
}
const WrappedDynamicRule = Form.create({ name: 'dynamic_rule' })(DynamicRule);
ReactDOM.render(<WrappedDynamicRule />, mountNode);
```

113
components/form-legacy/demo/form-in-modal.md

@ -1,113 +0,0 @@
---
order: 4
title:
zh-CN: 弹出层中的新建表单
en-US: Form in Modal to Create
---
## zh-CN
当用户访问一个展示了某个列表的页面,想新建一项但又不想跳转页面时,可以用 Modal 弹出一个表单,用户填写必要信息后创建新的项。
## en-US
When user visit a page with a list of items, and want to create a new item. The page can popup a form in Modal, then let user fill in the form to create an item.
```jsx
import { Button, Modal, Form, Input, Radio } from 'antd';
const CollectionCreateForm = Form.create({ name: 'form_in_modal' })(
// eslint-disable-next-line
class extends React.Component {
render() {
const { visible, onCancel, onCreate, form } = this.props;
const { getFieldDecorator } = form;
return (
<Modal
visible={visible}
title="Create a new collection"
okText="Create"
onCancel={onCancel}
onOk={onCreate}
>
<Form layout="vertical">
<Form.Item label="Title">
{getFieldDecorator('title', {
rules: [{ required: true, message: 'Please input the title of collection!' }],
})(<Input />)}
</Form.Item>
<Form.Item label="Description">
{getFieldDecorator('description')(<Input type="textarea" />)}
</Form.Item>
<Form.Item className="collection-create-form_last-form-item">
{getFieldDecorator('modifier', {
initialValue: 'public',
})(
<Radio.Group>
<Radio value="public">Public</Radio>
<Radio value="private">Private</Radio>
</Radio.Group>,
)}
</Form.Item>
</Form>
</Modal>
);
}
},
);
class CollectionsPage extends React.Component {
state = {
visible: false,
};
showModal = () => {
this.setState({ visible: true });
};
handleCancel = () => {
this.setState({ visible: false });
};
handleCreate = () => {
const { form } = this.formRef.props;
form.validateFields((err, values) => {
if (err) {
return;
}
console.log('Received values of form: ', values);
form.resetFields();
this.setState({ visible: false });
});
};
saveFormRef = formRef => {
this.formRef = formRef;
};
render() {
return (
<div>
<Button type="primary" onClick={this.showModal}>
New Collection
</Button>
<CollectionCreateForm
wrappedComponentRef={this.saveFormRef}
visible={this.state.visible}
onCancel={this.handleCancel}
onCreate={this.handleCreate}
/>
</div>
);
}
}
ReactDOM.render(<CollectionsPage />, mountNode);
```
```css
.collection-create-form_last-form-item {
margin-bottom: 0;
}
```

89
components/form-legacy/demo/global-state.md

@ -1,89 +0,0 @@
---
order: 8
title:
zh-CN: 表单数据存储于上层组件
en-US: Store Form Data into Upper Component
---
## zh-CN
通过使用 `onFieldsChange``mapPropsToFields`,可以把表单的数据存储到上层组件或者 [Redux](https://github.com/reactjs/redux)、[dva](https://github.com/dvajs/dva) 中,更多可参考 [rc-form 示例](http://react-component.github.io/form/examples/redux.html)。
**注意:**`mapPropsToFields` 里面返回的表单域数据必须使用 `Form.createFormField` 包装。
## en-US
We can store form data into upper component or [Redux](https://github.com/reactjs/redux) or [dva](https://github.com/dvajs/dva) by using `onFieldsChange` and `mapPropsToFields`, see more at this [rc-form demo](http://react-component.github.io/form/examples/redux.html).
**Note:** You must wrap field data with `Form.createFormField` in `mapPropsToFields`.
**Note:** Here, errors are passed to higher order component in `onFieldsChange` and passed back in `mapPropsToFields`.
```jsx
import { Form, Input } from 'antd';
const CustomizedForm = Form.create({
name: 'global_state',
onFieldsChange(props, changedFields) {
props.onChange(changedFields);
},
mapPropsToFields(props) {
return {
username: Form.createFormField({
...props.username,
value: props.username.value,
}),
};
},
onValuesChange(_, values) {
console.log(values);
},
})(props => {
const { getFieldDecorator } = props.form;
return (
<Form layout="inline">
<Form.Item label="Username">
{getFieldDecorator('username', {
rules: [{ required: true, message: 'Username is required!' }],
})(<Input />)}
</Form.Item>
</Form>
);
});
class Demo extends React.Component {
state = {
fields: {
username: {
value: 'benjycui',
},
},
};
handleFormChange = changedFields => {
this.setState(({ fields }) => ({
fields: { ...fields, ...changedFields },
}));
};
render() {
const { fields } = this.state;
return (
<div>
<CustomizedForm {...fields} onChange={this.handleFormChange} />
<pre className="language-bash">{JSON.stringify(fields, null, 2)}</pre>
</div>
);
}
}
ReactDOM.render(<Demo />, mountNode);
```
<style>
#components-form-demo-global-state .language-bash {
max-width: 400px;
border-radius: 6px;
margin-top: 24px;
}
</style>

80
components/form-legacy/demo/horizontal-login.md

@ -1,80 +0,0 @@
---
order: 0
title:
zh-CN: 水平登录栏
en-US: Horizontal Login Form
---
## zh-CN
水平登录栏,常用在顶部导航栏中。
## en-US
Horizontal login form is often used in navigation bar.
```jsx
import { Form, Icon, Input, Button } from 'antd';
function hasErrors(fieldsError) {
return Object.keys(fieldsError).some(field => fieldsError[field]);
}
class HorizontalLoginForm extends React.Component {
componentDidMount() {
// To disabled submit button at the beginning.
this.props.form.validateFields();
}
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};
render() {
const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form;
// Only show error after a field is touched.
const usernameError = isFieldTouched('username') && getFieldError('username');
const passwordError = isFieldTouched('password') && getFieldError('password');
return (
<Form layout="inline" onSubmit={this.handleSubmit}>
<Form.Item validateStatus={usernameError ? 'error' : ''} help={usernameError || ''}>
{getFieldDecorator('username', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<Input
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="Username"
/>,
)}
</Form.Item>
<Form.Item validateStatus={passwordError ? 'error' : ''} help={passwordError || ''}>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please input your Password!' }],
})(
<Input
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="Password"
/>,
)}
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={hasErrors(getFieldsError())}>
Log in
</Button>
</Form.Item>
</Form>
);
}
}
const WrappedHorizontalLoginForm = Form.create({ name: 'horizontal_login' })(HorizontalLoginForm);
ReactDOM.render(<WrappedHorizontalLoginForm />, mountNode);
```

72
components/form-legacy/demo/layout.md

@ -1,72 +0,0 @@
---
order: 12
title:
zh-CN: 表单布局
en-US: Form Layout
---
## zh-CN
表单有三种布局。
## en-US
There are three layout for form: `horizontal`, `vertical`, `inline`.
```jsx
import { Form, Input, Button, Radio } from 'antd';
class FormLayoutDemo extends React.Component {
constructor() {
super();
this.state = {
formLayout: 'horizontal',
};
}
handleFormLayoutChange = e => {
this.setState({ formLayout: e.target.value });
};
render() {
const { formLayout } = this.state;
const formItemLayout =
formLayout === 'horizontal'
? {
labelCol: { span: 4 },
wrapperCol: { span: 14 },
}
: null;
const buttonItemLayout =
formLayout === 'horizontal'
? {
wrapperCol: { span: 14, offset: 4 },
}
: null;
return (
<div>
<Form layout={formLayout}>
<Form.Item label="Form Layout" {...formItemLayout}>
<Radio.Group defaultValue="horizontal" onChange={this.handleFormLayoutChange}>
<Radio.Button value="horizontal">Horizontal</Radio.Button>
<Radio.Button value="vertical">Vertical</Radio.Button>
<Radio.Button value="inline">Inline</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="Field A" {...formItemLayout}>
<Input placeholder="input placeholder" />
</Form.Item>
<Form.Item label="Field B" {...formItemLayout}>
<Input placeholder="input placeholder" />
</Form.Item>
<Form.Item {...buttonItemLayout}>
<Button type="primary">Submit</Button>
</Form.Item>
</Form>
</div>
);
}
}
ReactDOM.render(<FormLayoutDemo />, mountNode);
```

87
components/form-legacy/demo/normal-login.md

@ -1,87 +0,0 @@
---
order: 1
title:
zh-CN: 登录框
en-US: Login Form
---
## zh-CN
普通的登录框,可以容纳更多的元素。
## en-US
Normal login form which can contain more elements.
```jsx
import { Form, Icon, Input, Button, Checkbox } from 'antd';
class NormalLoginForm extends React.Component {
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};
render() {
const { getFieldDecorator } = this.props.form;
return (
<Form onSubmit={this.handleSubmit} className="login-form">
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<Input
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="Username"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please input your Password!' }],
})(
<Input
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="Password"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('remember', {
valuePropName: 'checked',
initialValue: true,
})(<Checkbox>Remember me</Checkbox>)}
<a className="login-form-forgot" href="">
Forgot password
</a>
<Button type="primary" htmlType="submit" className="login-form-button">
Log in
</Button>
Or <a href="">register now!</a>
</Form.Item>
</Form>
);
}
}
const WrappedNormalLoginForm = Form.create({ name: 'normal_login' })(NormalLoginForm);
ReactDOM.render(<WrappedNormalLoginForm />, mountNode);
```
```css
#components-form-demo-normal-login .login-form {
max-width: 300px;
}
#components-form-demo-normal-login .login-form-forgot {
float: right;
}
#components-form-demo-normal-login .login-form-button {
width: 100%;
}
```

271
components/form-legacy/demo/register.md

@ -1,271 +0,0 @@
---
order: 2
title:
zh-CN: 注册新用户
en-US: Registration
---
## zh-CN
用户填写必须的信息以注册新用户。
## en-US
Fill in this form to create a new account for you.
```jsx
import {
Form,
Input,
Tooltip,
Icon,
Cascader,
Select,
Row,
Col,
Checkbox,
Button,
AutoComplete,
} from 'antd';
const { Option } = Select;
const AutoCompleteOption = AutoComplete.Option;
const residences = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
},
],
},
],
},
];
class RegistrationForm extends React.Component {
state = {
confirmDirty: false,
autoCompleteResult: [],
};
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFieldsAndScroll((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};
handleConfirmBlur = e => {
const { value } = e.target;
this.setState({ confirmDirty: this.state.confirmDirty || !!value });
};
compareToFirstPassword = (rule, value, callback) => {
const { form } = this.props;
if (value && value !== form.getFieldValue('password')) {
callback('Two passwords that you enter is inconsistent!');
} else {
callback();
}
};
validateToNextPassword = (rule, value, callback) => {
const { form } = this.props;
if (value && this.state.confirmDirty) {
form.validateFields(['confirm'], { force: true });
}
callback();
};
handleWebsiteChange = value => {
let autoCompleteResult;
if (!value) {
autoCompleteResult = [];
} else {
autoCompleteResult = ['.com', '.org', '.net'].map(domain => `${value}${domain}`);
}
this.setState({ autoCompleteResult });
};
render() {
const { getFieldDecorator } = this.props.form;
const { autoCompleteResult } = this.state;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 8 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 24,
offset: 0,
},
sm: {
span: 16,
offset: 8,
},
},
};
const prefixSelector = getFieldDecorator('prefix', {
initialValue: '86',
})(
<Select style={{ width: 70 }}>
<Option value="86">+86</Option>
<Option value="87">+87</Option>
</Select>,
);
const websiteOptions = autoCompleteResult.map(website => (
<AutoCompleteOption key={website}>{website}</AutoCompleteOption>
));
return (
<Form {...formItemLayout} onSubmit={this.handleSubmit}>
<Form.Item label="E-mail">
{getFieldDecorator('email', {
rules: [
{
type: 'email',
message: 'The input is not valid E-mail!',
},
{
required: true,
message: 'Please input your E-mail!',
},
],
})(<Input />)}
</Form.Item>
<Form.Item label="Password" hasFeedback>
{getFieldDecorator('password', {
rules: [
{
required: true,
message: 'Please input your password!',
},
{
validator: this.validateToNextPassword,
},
],
})(<Input.Password />)}
</Form.Item>
<Form.Item label="Confirm Password" hasFeedback>
{getFieldDecorator('confirm', {
rules: [
{
required: true,
message: 'Please confirm your password!',
},
{
validator: this.compareToFirstPassword,
},
],
})(<Input.Password onBlur={this.handleConfirmBlur} />)}
</Form.Item>
<Form.Item
label={
<span>
Nickname&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);
```

158
components/form-legacy/demo/style-check-debug.md

@ -1,158 +0,0 @@
---
order: 99
title:
zh-CN: 提交修改前看看这个对不对
en-US: Please check this before commit
debug: true
---
## zh-CN
提交修改前看看这个对不对。
## en-US
Please check this before commit.
```jsx
import { Button, Modal, Form, Row, Col, Input, Select, InputNumber, Radio, DatePicker } from 'antd';
const ColSpan = { lg: 12, md: 24 };
class App extends React.Component {
constructor() {
super();
this.state = {
visible: false,
};
}
handleClick = () => {
this.setState({
visible: true,
});
};
handleCancel = () => {
this.setState({
visible: false,
});
};
handleSubmit = e => {
e.preventDefault();
const { form } = this.props;
form.validateFields((error, values) => {
console.log(error, values);
});
};
render() {
const {
form: { getFieldDecorator },
} = this.props;
const { Item } = Form;
const itemLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 16 },
};
const span = 12;
return (
<div>
{/* Case 1: Form in modal */}
<Button onClick={this.handleClick}>打开</Button>
<Modal
onOk={this.handleSubmit}
onCancel={this.handleCancel}
title="弹出层"
visible={this.state.visible}
>
<Form layout="horizontal" onSubmit={this.handleSubmit}>
<Row>
<Col span={span}>
<Item colon={false} {...itemLayout} label="测试字段">
{getFieldDecorator('item1', {
rules: [{ required: true, message: '请必须填写此字段' }],
})(<Input />)}
</Item>
</Col>
<Col span={span}>
<Item {...itemLayout} label="测试字段">
{getFieldDecorator('item2', {
rules: [{ required: true, message: '请必须填写此字段' }],
})(<Input />)}
</Item>
</Col>
<Col span={span}>
<Item {...itemLayout} label="测试字段">
{getFieldDecorator('item3')(<Input />)}
</Item>
</Col>
<Col span={span}>
<Item {...itemLayout} label="测试字段">
{getFieldDecorator('item4', {
rules: [{ required: true, message: '请必须填写此字段' }],
})(<Input />)}
</Item>
</Col>
<Col span={span}>
<Item {...itemLayout} label="测试字段">
{getFieldDecorator('item5', {
rules: [{ required: true, message: '请必须填写此字段' }],
})(<Input />)}
</Item>
</Col>
<Col span={span}>
<Item {...itemLayout} label="测试字段">
{getFieldDecorator('item6', {
rules: [{ required: true, message: '请必须填写此字段' }],
})(<Input />)}
</Item>
</Col>
</Row>
</Form>
</Modal>
{/* case 2: Form different item */}
<Form>
<Row gutter={16}>
<Col {...ColSpan}>
<Item colon={false} label="input:64.5px">
<Input />
</Item>
</Col>
<Col {...ColSpan}>
<Item label="select:64px">
<Select />
</Item>
</Col>
<Col {...ColSpan}>
<Item label="InputNumber:64px">
<InputNumber />
</Item>
</Col>
<Col {...ColSpan}>
<Item label="DatePicker: 64.5px">
<DatePicker />
</Item>
</Col>
<Col {...ColSpan}>
<Item label="RadioGroup: 64px">
<Radio.Group>
<Radio value={0}></Radio>
<Radio value={1}></Radio>
</Radio.Group>
</Item>
</Col>
</Row>
</Form>
</div>
);
}
}
const WrapApp = Form.create()(App);
ReactDOM.render(<WrapApp />, mountNode);
```

109
components/form-legacy/demo/time-related-controls.md

@ -1,109 +0,0 @@
---
order: 6
title:
zh-CN: 时间类控件
en-US: Time-related Controls
---
## zh-CN
时间类组件的 `value` 类型为 `moment` 对象,所以在提交服务器前需要预处理。
## en-US
The `value` of time-related components is a `moment` object, which we need to pre-process it before we submit to server.
```jsx
import { Form, DatePicker, TimePicker, Button } from 'antd';
const { MonthPicker, RangePicker } = DatePicker;
class TimeRelatedForm extends React.Component {
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, fieldsValue) => {
if (err) {
return;
}
// Should format date value before submit.
const rangeValue = fieldsValue['range-picker'];
const rangeTimeValue = fieldsValue['range-time-picker'];
const values = {
...fieldsValue,
'date-picker': fieldsValue['date-picker'].format('YYYY-MM-DD'),
'date-time-picker': fieldsValue['date-time-picker'].format('YYYY-MM-DD HH:mm:ss'),
'month-picker': fieldsValue['month-picker'].format('YYYY-MM'),
'range-picker': [rangeValue[0].format('YYYY-MM-DD'), rangeValue[1].format('YYYY-MM-DD')],
'range-time-picker': [
rangeTimeValue[0].format('YYYY-MM-DD HH:mm:ss'),
rangeTimeValue[1].format('YYYY-MM-DD HH:mm:ss'),
],
'time-picker': fieldsValue['time-picker'].format('HH:mm:ss'),
};
console.log('Received values of form: ', values);
});
};
render() {
const { getFieldDecorator } = this.props.form;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 8 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
const config = {
rules: [{ type: 'object', required: true, message: 'Please select time!' }],
};
const rangeConfig = {
rules: [{ type: 'array', required: true, message: 'Please select time!' }],
};
return (
<Form {...formItemLayout} onSubmit={this.handleSubmit}>
<Form.Item label="DatePicker">
{getFieldDecorator('date-picker', config)(<DatePicker />)}
</Form.Item>
<Form.Item label="DatePicker[showTime]">
{getFieldDecorator('date-time-picker', config)(
<DatePicker showTime format="YYYY-MM-DD HH:mm:ss" />,
)}
</Form.Item>
<Form.Item label="MonthPicker">
{getFieldDecorator('month-picker', config)(<MonthPicker />)}
</Form.Item>
<Form.Item label="RangePicker">
{getFieldDecorator('range-picker', rangeConfig)(<RangePicker />)}
</Form.Item>
<Form.Item label="RangePicker[showTime]">
{getFieldDecorator('range-time-picker', rangeConfig)(
<RangePicker showTime format="YYYY-MM-DD HH:mm:ss" />,
)}
</Form.Item>
<Form.Item label="TimePicker">
{getFieldDecorator('time-picker', config)(<TimePicker />)}
</Form.Item>
<Form.Item
wrapperCol={{
xs: { span: 24, offset: 0 },
sm: { span: 16, offset: 8 },
}}
>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
}
const WrappedTimeRelatedForm = Form.create({ name: 'time_related_controls' })(TimeRelatedForm);
ReactDOM.render(<WrappedTimeRelatedForm />, mountNode);
```

217
components/form-legacy/demo/validate-other.md

@ -1,217 +0,0 @@
---
order: 14
title:
zh-CN: 校验其他组件
en-US: Other Form Controls
---
## zh-CN
以上演示没有出现的表单控件对应的校验演示。
## en-US
Demonstration of validation configuration for form controls which are not shown in the demos above.
```jsx
import {
Form,
Select,
InputNumber,
Switch,
Radio,
Slider,
Button,
Upload,
Icon,
Rate,
Checkbox,
Row,
Col,
} from 'antd';
const { Option } = Select;
class Demo extends React.Component {
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};
normFile = e => {
console.log('Upload event:', e);
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
};
render() {
const { getFieldDecorator } = this.props.form;
const formItemLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 14 },
};
return (
<Form {...formItemLayout} onSubmit={this.handleSubmit}>
<Form.Item label="Plain Text">
<span className="ant-form-text">China</span>
</Form.Item>
<Form.Item label="Select" hasFeedback>
{getFieldDecorator('select', {
rules: [{ required: true, message: 'Please select your country!' }],
})(
<Select placeholder="Please select a country">
<Option value="china">China</Option>
<Option value="usa">U.S.A</Option>
</Select>,
)}
</Form.Item>
<Form.Item label="Select[multiple]">
{getFieldDecorator('select-multiple', {
rules: [
{ required: true, message: 'Please select your favourite colors!', type: 'array' },
],
})(
<Select mode="multiple" placeholder="Please select favourite colors">
<Option value="red">Red</Option>
<Option value="green">Green</Option>
<Option value="blue">Blue</Option>
</Select>,
)}
</Form.Item>
<Form.Item label="InputNumber">
{getFieldDecorator('input-number', { initialValue: 3 })(<InputNumber min={1} max={10} />)}
<span className="ant-form-text"> machines</span>
</Form.Item>
<Form.Item label="Switch">
{getFieldDecorator('switch', { valuePropName: 'checked' })(<Switch />)}
</Form.Item>
<Form.Item label="Slider">
{getFieldDecorator('slider')(
<Slider
marks={{
0: 'A',
20: 'B',
40: 'C',
60: 'D',
80: 'E',
100: 'F',
}}
/>,
)}
</Form.Item>
<Form.Item label="Radio.Group">
{getFieldDecorator('radio-group')(
<Radio.Group>
<Radio value="a">item 1</Radio>
<Radio value="b">item 2</Radio>
<Radio value="c">item 3</Radio>
</Radio.Group>,
)}
</Form.Item>
<Form.Item label="Radio.Button">
{getFieldDecorator('radio-button')(
<Radio.Group>
<Radio.Button value="a">item 1</Radio.Button>
<Radio.Button value="b">item 2</Radio.Button>
<Radio.Button value="c">item 3</Radio.Button>
</Radio.Group>,
)}
</Form.Item>
<Form.Item label="Checkbox.Group">
{getFieldDecorator('checkbox-group', {
initialValue: ['A', 'B'],
})(
<Checkbox.Group style={{ width: '100%' }}>
<Row>
<Col span={8}>
<Checkbox value="A">A</Checkbox>
</Col>
<Col span={8}>
<Checkbox disabled value="B">
B
</Checkbox>
</Col>
<Col span={8}>
<Checkbox value="C">C</Checkbox>
</Col>
<Col span={8}>
<Checkbox value="D">D</Checkbox>
</Col>
<Col span={8}>
<Checkbox value="E">E</Checkbox>
</Col>
</Row>
</Checkbox.Group>,
)}
</Form.Item>
<Form.Item label="Rate">
{getFieldDecorator('rate', {
initialValue: 3.5,
})(<Rate />)}
</Form.Item>
<Form.Item label="Upload" extra="longgggggggggggggggggggggggggggggggggg">
{getFieldDecorator('upload', {
valuePropName: 'fileList',
getValueFromEvent: this.normFile,
})(
<Upload name="logo" action="/upload.do" listType="picture">
<Button>
<Icon type="upload" /> Click to upload
</Button>
</Upload>,
)}
</Form.Item>
<Form.Item label="Dragger">
<div className="dropbox">
{getFieldDecorator('dragger', {
valuePropName: 'fileList',
getValueFromEvent: this.normFile,
})(
<Upload.Dragger name="files" action="/upload.do">
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">Click or drag file to this area to upload</p>
<p className="ant-upload-hint">Support for a single or bulk upload.</p>
</Upload.Dragger>,
)}
</div>
</Form.Item>
<Form.Item wrapperCol={{ span: 12, offset: 6 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
}
const WrappedDemo = Form.create({ name: 'validate_other' })(Demo);
ReactDOM.render(<WrappedDemo />, mountNode);
```
```css
#components-form-demo-validate-other .dropbox {
height: 180px;
line-height: 1.5;
}
```

72
components/form-legacy/demo/without-form-create.md

@ -1,72 +0,0 @@
---
order: 9
title:
zh-CN: 自行处理表单数据
en-US: Handle Form Data Manually
---
## zh-CN
使用 `Form.create` 处理后的表单具有自动收集数据并校验的功能,但如果您不需要这个功能,或者默认的行为无法满足业务需求,可以选择不使用 `Form.create` 并自行处理数据。
## en-US
`Form.create` will collect and validate form data automatically. But if you don't need this feature or the default behaviour cannot satisfy your business, you can drop `Form.create` and handle form data manually.
```jsx
import { Form, InputNumber } from 'antd';
function validatePrimeNumber(number) {
if (number === 11) {
return {
validateStatus: 'success',
errorMsg: null,
};
}
return {
validateStatus: 'error',
errorMsg: 'The prime between 8 and 12 is 11!',
};
}
class RawForm extends React.Component {
state = {
number: {
value: 11,
},
};
handleNumberChange = value => {
this.setState({
number: {
...validatePrimeNumber(value),
value,
},
});
};
render() {
const formItemLayout = {
labelCol: { span: 7 },
wrapperCol: { span: 12 },
};
const { number } = this.state;
const tips =
'A prime is a natural number greater than 1 that has no positive divisors other than 1 and itself.';
return (
<Form>
<Form.Item
{...formItemLayout}
label="Prime between 8 & 12"
validateStatus={number.validateStatus}
help={number.errorMsg || tips}
>
<InputNumber min={8} max={12} value={number.value} onChange={this.handleNumberChange} />
</Form.Item>
</Form>
);
}
}
ReactDOM.render(<RawForm />, mountNode);
```

98
components/form-legacy/docs/tutorial.md

@ -1,98 +0,0 @@
# 表单实现原理
---
antd 中的 [Form](https://github.com/ant-design/ant-design/blob/master/components/form/index.zh-CN.md) 组件基于 [rc-form](https://github.com/react-component/form) 实现。本文第一部分将介绍 rc-form 库;第二部分再介绍 antd 中的 Form 组件;第三部分将结合表单组件的使用,回顾前两部分的内容。
---
### rc-form
常规收集表单数据并作校验,只需以 store 实时记录表单数据,校验后重绘表单。这样的思路以业务代码为例,就是,以数据模型 model 集成数据处理操作,再通过 setState 将 model 中的实时数据注入组件中,并驱动组件重绘(除了 setState 方法以外,也可以使用 forceUpdate 方法重绘组件,并在 render 阶段重新访问 model 中的实时数据)。
#### FieldsStore
在 rc-form 中,上述数据模型的具体实现为 FieldsStore 类。如前所述,FieldsStore 实例与视图层的交互逻辑为,在用户行为驱动字段变更时,即时存储字段的值及校验信息(本文以校验信息代指校验文案和校验状态),继而调用表单组件实例的 forceUpdate 方法强制重绘;在绘制过程中,再从 FieldsStore 实例读取实时数据和校验信息。
建模方面,FieldsStore 实例以 fields 属性存储表单的实时数据,即由用户行为或开发者显式更新的数据。本文把实时数据已存入 fields 中的字段称为已收集字段;反之,称为未收集字段。以下是 fields\[name\] 中成员属性的意义(name 为字段名,下同)。
- value 字段的值。
- errors 校验文案,数组形式。
- validating 校验状态。
- dirty 脏值标识。真值时意味着字段的值已作变更,但未作校验。
- touched 用户行为标识。通常情况下,真值时意味着用户行为已促使字段的值发生了变更。
FieldsStore 实例又以 fieldsMeta 属性存储字段的元数据。元数据作为配置项,通常是指定后不再变更的数据,用于操控表单数据转换等行为。元数据通过下文中 getFieldProps, getFieldDecorator 方法的次参 fieldOption 配置。以下是 fieldsMeta\[name\] 中部分成员属性的意义(如不作特殊说明,该成员属性即 fieldOption 中的同名属性)。
- validate 校验规则和触发事件,用于约定在何种事件下以何种方式校验字段的值,\[{ rules, trigger }\] 形式。通过 fieldOption.validate, fieldOption.rules, fieldOption.validateTrigger 配置。
- hidden 设置为 true 时,validateFields 将不会校验该字段,需要开发者手动校验,并且,getFieldsValue, getFieldsError 等方法也将无法获取该字段的数据及校验信息等实时数据。适用场景如勾选协议复选框,当其已勾选时,提交按钮才可点击,该复选框的值不会出现在全量表单数据中。本文假定配置了 hidden 为 true 的字段为虚拟隐藏项。
- getValueFromEvent(event) 用于从 event 对象中获取字段的值,适用场景如处理 input, radio 等原生组件。特别的,当字段组件输出的首参不是 event 对象时,getValueFromEvent 将对首参进行数据转化,以满足特定的场景,比如由开发者指定具体首参内容的自定义字段组件。
- initialValue 字段的初始值。当字段的值未作收集时,初始值用于作为字段的值。
- valuePropName 约定字段的值以何种 props 属性注入字段组件中,适用场景如将字段的值以 props.checked 属性注入 radio 组件中。注入字段组件的值数据 props 由 getFieldValuePropValue 方法生成,下同。
- getValueProps(value) 用于转化字段的值,输出 props 以注入字段组件中。适用场景如当 get, post 请求的数据结构不同时,且字段组件以 post 请求的数据结构输出字段的值、以 get 请求的数据结构接受字段的值,那就可以使用 getValueProps 将 post 请求的数据结构转换为 get 请求的数据结构。
- normalize(newValue, oldValue, values) 用于转化存入 FieldsStore 实例的字段的值。使用案例如[全选按钮](https://codepen.io/afc163/pen/JJVXzG?editors=001)。
在此基础上,FieldsStore 提供了一些便捷的访问器操作。需要说明的是,rc-form 借助 [lodash](https://github.com/lodash/lodash),支持以嵌套结构定义字段名,即使用 '.', '\[|\]' 作为分割符,如 'a.b' 意味着 a 对象下的 b 属性;'c\[0\]' 意味着 c 数组的首项。本文约定匹配字段指,所有以指定字符串起始或等值的字段列表。为此,FieldsStore 提供 isValidNestedFieldName 用于校验字段名不能作为表单中另一个字段名的成员;flattenRegisteredFields 用于传参数据作扁平化处理;getValidFieldsFullName 用于获取匹配字段列表,但不包含虚拟隐藏项。除此以外,FieldsStore 提供了对实时数据和元数据的读取操作,特别的,部分 api 可用于获取匹配字段的实时数据,参见[文档](https://ant.design/components/form-cn/)。
#### BaseForm
与业务 model 不同的是,FieldsStore 仅作为表单实时数据和元数据的存储器,校验数据等方法由 BaseForm 提供。BaseForm 既作为 HOC 容器,能为开发者自定义表单组件(下文用自定义表单替代)注入表单操作函数集,通常是 props.form;又用于装饰字段组件或其 props,以收集字段的元数据、通过绑定函数收集或校验字段的实时数据。因此,可以部分认为,BaseForm 即 FieldsStore 和视图层桥接的控制器。其机制为:
首先,通过 createBaseForm(option, mixins) 创建装饰函数。装饰函数可以为自定义表单包裹上 BaseForm 容器。参数 option 用于配置表单层面的绑定函数、校验文案以及部分字段组件的 props 属性名;mixins 将作为实例方法混入 BaseForm,特别的,createDOMForm 函数即通过这一机制混入了 validateFieldsAndScroll 方法。
其次,在 BaseForm 的 getInitialState 阶段,将创建 FieldsStore 实例,并初始化 clearedFieldMetaCache 等缓存。这些缓存的意义包含缓存字段组件实例,缓存内置的 ref 引用及绑定函数,缓存渲染状态等。特别的,clearedFieldMetaCache 用于在 ref 引用函数执行时缓存字段的实时数据和元数据,以便于在字段重绘过程中的第两次执行 ref 引用时,恢复 FieldsStore 存储的实时数据和元数据(参见[源码](https://github.com/react-component/form/blob/master/src/createBaseForm.js)中的 saveRef 方法)。不然,元数据的丢失将导致数据校验无法正常工作。
其次,执行 render 方法,将表单操作函数集通过 props 注入自定义表单。介于此,在自定义表单中,开发者可以获取到表单的实时数据,或者更新表单数据,或者校验表单,以完成特定的处理逻辑。
其次,由 BaseForm 提供的 getFieldProps 或 getFieldDecorator 实例方法完成字段组件的渲染。以下是 getFieldProps, getFieldDecorator 的意义。
- getFieldProps(name, fieldOption) 用于为 FieldsStore 实例收集字段的元数据,如经转化后的校验规则等;指定字段组件的 ref 引用函数;为字段组件绑定 onCollect, onCollectValidate 实例方法,以在指定事件触发时收集或校验字段的值;最终将输出注入字段组件的 props,包含全量的元数据和实时数据、以及字段的值。
- getFieldDecorator(name, fieldOption) 基于 getFieldProps 方法,直接装饰字段组件,这样就可以操控添加在字段组件上的 ref 引用函数及 props.onChange 等绑定函数。
其次,当用户行为促使字段的值发生变更时,将执行 BaseForm 实例的 onCollect, onCollectValidate 方法,以收集或校验该字段的实时数据,并重绘表单。其中,rc-form 中的数据校验通过 [async-validate](https://github.com/yiminghe/async-validator) 库实现,具体实现为 BaseForm 实例的 validateFieldsInternal 方法。校验字段时,默认将沿用上一次的校验信息;当设置 force 为 true 时,将强制重新校验。
详细的工作流程参见下方的时序图: <img class="preview-img no-padding" src="http://xzfyu.com/2018/11/04/ant%20design/antd-Form%20%E7%BB%84%E4%BB%B6/rc-form%E6%97%B6%E5%BA%8F%E5%9B%BE.png">
### Form 表单
#### 表单
Form 组件本身并不承载逻辑,而是通过 props.className, props.prefixCls, props.layout, props.hideRequiredMark, props.onSubmit 设定注入 form 原生节点的样式类及绑定函数,以影响表单内部节点渲染时的样式。同时,Form 组件将为子组件传入 context.vertical 以区分是水平布局,还是垂直布局。
#### 表单域
FormItem 组件用于设定表单项的布局。如同受控组件和非受控组件,FormItem 组件提供两种使用方式:其一,当未设定校验信息相关的 props 属性时,FormItem 组件将自动根据内部字段组件实例的状况渲染校验文案及校验状态;其二,当设定校验信息相关的 props 属性时,FormItem 组件将根据开发者传入的 props 渲染校验文案及校验状态。在第一种使用方式下,FormItem 组件只可以包含一个字段组件;在第二种使用方式下,FormItem 组件中可以包含多个字段组件,布局也更为灵活。这里说的相关 props 属性包含:校验文案 help, 校验状态 validateStatus(用于绘制反馈图标), 必填标识 required, 字段名 id(影响点击 label 时聚焦哪个字段元素)。
那么,FormItem 又是怎样自动收集字段组件的校验数据呢?因为在 BaseForm 组件提供的 getFieldProp 方法,字段名、元数据和实时数据都将作为特殊的 props 属性传入到字段组件中,所以作为字段组件容器的 FormItem,就可以通过这些特殊的 props 属性判断子组件实例是不是一个字段组件实例。当其为字段组件实例时,进一步收集实时的校验信息,从校验规则中获取是否必填标识,以完成表单域的渲染。
此外,FormItem 可以使用 props.labelCol, props.wrapperCol 属性栅格化布局标签组件和字段组件,其实现借助于 antd 提供的 [Row, Col 组件](https://ant.design/components/grid-cn/)。当点击标签 label 时,FormItem 提供的绑定函数也能自动为字段组件获得焦点。这里不再多加介绍。
### 使用与回顾
#### 创建 HOC 容器
结合上文,antd 使用 Form.create(options)(CustomizedForm) 形式为用户自定义表单包裹上 BaseForm 高阶组件,以此植入 props.form 表单操作函数集。
高阶组件影响对自定义表单设置 ref 引用。默认可使用 BaseForm 实例的 refs.wrappedComponent 属性访问 CustomizedForm 实例,其次也可以通过 props.wrappedComponentRef 为 CustomizedForm 实例配置 ref 引用(参考案例 —— 弹出层中的新建表单)。当自定义表单可切换或者需要对外交互时,设置 ref 引用通常是不可避免的。
高阶组件影响 props 传递。antd 既支持使用 options.mapPropsToFields 将 BaseForm 实例获得的 props 转化成表单的实时数据(需要结合 Form.createFormField 方法),又支持在 CustomizedForm 实例中调用 props.form.setFields 方法更新实时数据。当 options.mapPropsToFields, options.onFieldsChange 方法结合使用时,可用于完成自定义表单和上层组件、或者 view 层和 store 层的交互(参考案例 —— 表单数据存储于上层组件)。
一般认为,对整张表单的控制,需要借助于 Form.create 方法的首参 options 配置实现。
#### 操作函数集
通常情况下,CustomizedForm 实例可通过 props.form 获取到表单操作函数集。当然,开发者也可以通过 options.formPropName 指定操作函数集的 props 属性名。
操作函数集包含 getFieldProps, getFieldDecorator, getFieldInstance, setFields, setFieldsValue, setFieldsInitialValue, resetFields, validateFields, getFieldsValue, getFieldValue, getFieldsError, getFieldError, isFieldsValidating, isFieldValidating, isFieldsTouched, isFieldTouched 方法,即用于装饰字段组件(或其 props)、获取字段组件实例、设置或获取实时数据、重置或校验字段。
如上文所说,getFieldProps, getFieldDecorator 方法即用于自动为字段组件绑定监听函数,这样就可以在指定事件触发时,收集和校验字段的值。同时,可以通过这两个方法将视图中未加显示的字段存入 FieldsStore 中(这时,可以将 FieldsStore 视为一个内置于表单组件的状态管理器)。比如在包含其他选项的单选框场景中,就可以使用 getFieldDecorator 创建虚拟字段,通过绑定函数将单选框和输入框的值赋值到该虚拟字段中,并使用该虚拟字段的校验信息绘制 FormItem。参考案例 —— 动态增减表单项。
上述单选框场景,也可以使用自定义字段组件 CustomizedField 实现。参考案例 —— 自定义字段组件。当使用自定义字段组件时,通过 getFieldInstance 获取 CustomizedField 的实例可能是必不可少的,这样可以把略显复杂的数据校验方法集成在 CustomizedField 中。此外,在字段组件中,既可以通过 props.value 属性获得字段的值,也可以通过 props\['data-**meta'\], props\['data-**field'\] 获得字段的全量元数据和实时数据。
在 CustomizedForm 中,使用 getFieldValue 可以取得字段的实时更新值,这样就能根据指定字段的值控制另一个字段的显隐。此外,通过在字段组件的 onChange 绑定函数中调用 setFieldValue,也能对另一个字段组件加以赋值,这样就可以实现表单的联动效果,参考案例 —— 表单联动。若在字段组件的 onChange 绑定函数中调用 validateFields 方法,就可以实现关联字段的校验,比如表单中存在设置最大最小值的两个输入框,参考案例 —— 动态校验规则。
使用 getFieldError, isFieldValidating 获取到的校验信息可用于直接绘制 FormItem,这样就能更加细微地操控 FormItem 下字段组件的布局,比如放置多个字段组件。当然,对于多个字段组件公用校验信息的场景,也可以使用包含多个字段的 CustomizedField 实现。isFieldTouched 可判断用户是否对字段组件有触发数据收集和校验的行为,参考案例 —— 水平登录栏。介于 BaseForm 默认在 onChange 事件中收集并校验字段的值,在这种情形下,也可以通过 isFieldTouched 判断字段的值是否已作更新,而不需要在 CustomizedForm 中设置特殊的更新标识。
#### 其他
当不使用 Form.create 为字段组件自动绑定校验方法时,可以使用 Form, FormItem 组件设定表单的布局、校验信息的绘制,参考案例 —— 表单布局、自行处理表单数据、自定义校验。

254
components/form-legacy/index.en-US.md

@ -1,254 +0,0 @@
---
category: Components
type: Data Entry
cols: 1
title: Form
---
Form is used to collect, validate, and submit the user input, usually contains various form items including checkbox, radio, input, select, and etc.
## When to use
- When you need to create a instance or collect information.
- When you need to validate fields in certain rules.
## Form Component
You can align the controls of a `form` using the `layout` prop:
- `horizontal`:to horizontally align the `label`s and controls of the fields. (Default)
- `vertical`:to vertically align the `label`s and controls of the fields.
- `inline`:to render form fields in one line.
## Form Item Component
A form consists of one or more form fields whose type includes input, textarea, checkbox, radio, select, tag, and more. A form field is defined using `<Form.Item />`.
```jsx
<Form.Item {...props}>{children}</Form.Item>
```
## API
### Form
**more example [rc-form](http://react-component.github.io/form/)**。
| Property | Description | Type | Default Value |
| --- | --- | --- | --- |
| form | Decorated by `Form.create()` will be automatically set `this.props.form` property | object | n/a |
| hideRequiredMark | Hide required mark of all form items | Boolean | false |
| labelAlign | Label text align | 'left' \| 'right' | 'right' |
| labelCol | (Added in 3.14.0. Previous version can only set on FormItem.) The layout of label. You can set `span` `offset` to something like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` same as with `<Col>` | [object](https://ant.design/components/grid/#Col) | |
| layout | Define form layout | 'horizontal'\|'vertical'\|'inline' | 'horizontal' |
| onSubmit | Defines a function will be called if form data validation is successful. | Function(e:Event) | |
| wrapperCol | (Added in 3.14.0. Previous version can only set on FormItem.) The layout for input controls, same as `labelCol` | [object](https://ant.design/components/grid/#Col) | |
| colon | change default props colon value of Form.Item | boolean | true |
### Form.create(options)
How to use:
```jsx
class CustomizedForm extends React.Component {}
CustomizedForm = Form.create({})(CustomizedForm);
```
The following `options` are available:
| Property | Description | Type |
| --- | --- | --- |
| mapPropsToFields | Convert props to field value(e.g. reading the values from Redux store). And you must mark returned fields with [`Form.createFormField`](#Form.createFormField). Please note that the form fields will become controlled components. Properties like errors will not be automatically mapped and need to be manually passed in. | (props) => ({ \[fieldName\]: FormField { value } }) |
| name | Set the id prefix of fields under form | - |
| validateMessages | Default validate message. And its format is similar with [newMessages](https://github.com/yiminghe/async-validator/blob/master/src/messages.js)'s returned value | Object { \[nested.path]: String } |
| onFieldsChange | Specify a function that will be called when the fields (including errors) of a `Form.Item` gets changed. Usage example: saving the field's value to Redux store. | Function(props, changedFields, allFields) |
| onValuesChange | A handler while value of any field is changed | (props, changedValues, allValues) => void |
If you want to get `ref` after `Form.create`, you can use `wrappedComponentRef` provided by `rc-form`, [details can be viewed here](https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140).
```jsx
class CustomizedForm extends React.Component { ... }
// use wrappedComponentRef
const EnhancedForm = Form.create()(CustomizedForm);
<EnhancedForm wrappedComponentRef={(form) => this.form = form} />
this.form // => The instance of CustomizedForm
```
If the form has been decorated by `Form.create` then it has `this.props.form` property. `this.props.form` provides some APIs as follows:
> Note: Before using `getFieldsValue` `getFieldValue` `setFieldsValue` and so on, please make sure that corresponding field had been registered with `getFieldDecorator`.
| Method | Description | Type |
| --- | --- | --- |
| getFieldDecorator | Two-way binding for form, please read below for details. | |
| getFieldError | Get the error of a field. | Function(name) |
| getFieldsError | Get the specified fields' error. If you don't specify a parameter, you will get all fields' error. | Function(\[names: string\[]]) |
| getFieldsValue | Get the specified fields' values. If you don't specify a parameter, you will get all fields' values. | Function(\[fieldNames: string\[]]) |
| getFieldValue | Get the value of a field. | Function(fieldName: string) |
| isFieldsTouched | Check whether any of fields is touched by `getFieldDecorator`'s `options.trigger` event | (names?: string\[]) => boolean |
| isFieldTouched | Check whether a field is touched by `getFieldDecorator`'s `options.trigger` event | (name: string) => boolean |
| isFieldValidating | Check if the specified field is being validated. | Function(name) |
| resetFields | Reset the specified fields' value(to `initialValue`) and status. If you don't specify a parameter, all the fields will be reset. | Function(\[names: string\[]]) |
| setFields | Set value and error state of fields. [Code Sample](https://github.com/react-component/form/blob/3b9959b57ab30b41d8890ff30c79a7e7c383cad3/examples/server-validate.js#L74-L79) | ({<br />&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>

12
components/form-legacy/index.tsx

@ -1,12 +0,0 @@
import Form from './Form';
export {
FormProps,
FormComponentProps,
FormCreateOption,
ValidateCallback,
ValidationRule,
} from './Form';
export { FormItemProps } from './FormItem';
export default Form;

256
components/form-legacy/index.zh-CN.md

@ -1,256 +0,0 @@
---
category: Components
subtitle: 表单
type: 数据录入
cols: 1
title: Form
---
具有数据收集、校验和提交功能的表单,包含复选框、单选框、输入框、下拉选择框等元素。
## 何时使用
- 用于创建一个实体或收集信息。
- 需要对输入的数据类型进行校验时。
## 表单
我们为 `form` 提供了以下三种排列方式:
- 水平排列:标签和表单控件水平排列;(默认)
- 垂直排列:标签和表单控件上下垂直排列;
- 行内排列:表单项水平行内排列。
## 表单域
表单一定会包含表单域,表单域可以是输入控件,标准表单域,标签,下拉菜单,文本域等。
这里我们封装了表单域 `<Form.Item />`
```jsx
<Form.Item {...props}>{children}</Form.Item>
```
## API
### Form
**更多示例参考 [rc-form](http://react-component.github.io/form/)**。
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| form | 经 `Form.create()` 包装过的组件会自带 `this.props.form` 属性 | object | - |
| hideRequiredMark | 隐藏所有表单项的必选标记 | Boolean | false |
| labelAlign | label 标签的文本对齐方式 | 'left' \| 'right' | 'right' |
| labelCol | (3.14.0 新增,之前的版本只能设置到 FormItem 上。)label 标签布局,同 `<Col>` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}``sm: {span: 3, offset: 12}` | [object](https://ant.design/components/grid/#Col) | |
| layout | 表单布局 | 'horizontal'\|'vertical'\|'inline' | 'horizontal' |
| onSubmit | 数据验证成功后回调事件 | Function(e:Event) | |
| wrapperCol | (3.14.0 新增,之前的版本只能设置到 FormItem 上。)需要为输入控件设置布局样式时,使用该属性,用法同 labelCol | [object](https://ant.design/components/grid/#Col) | |
| colon | 配置 Form.Item 的 colon 的默认值 | boolean | true |
### Form.create(options)
使用方式如下:
```jsx
class CustomizedForm extends React.Component {}
CustomizedForm = Form.create({})(CustomizedForm);
```
`options` 的配置项如下。
| 参数 | 说明 | 类型 |
| --- | --- | --- |
| mapPropsToFields | 把父组件的属性映射到表单项上(如:把 Redux store 中的值读出),需要对返回值中的表单域数据用 [`Form.createFormField`](#Form.createFormField) 标记,注意表单项将变成受控组件, error 等也需要一并手动传入 | (props) => ({ \[fieldName\]: FormField { value } }) |
| name | 设置表单域内字段 id 的前缀 | - |
| validateMessages | 默认校验信息,可用于把默认错误信息改为中文等,格式与 [newMessages](https://github.com/yiminghe/async-validator/blob/master/src/messages.js) 返回值一致 | Object { \[nested.path]: String } |
| onFieldsChange | 当 `Form.Item` 子节点的值(包括 error)发生改变时触发,可以把对应的值转存到 Redux store | Function(props, changedFields, allFields) |
| onValuesChange | 任一表单域的值发生改变时的回调 | (props, changedValues, allValues) => void |
经过 `Form.create` 之后如果要拿到 `ref`,可以使用 `rc-form` 提供的 `wrappedComponentRef`,[详细内容可以查看这里](https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140)。
```jsx
class CustomizedForm extends React.Component { ... }
// use wrappedComponentRef
const EnhancedForm = Form.create()(CustomizedForm);
<EnhancedForm wrappedComponentRef={(form) => this.form = form} />
this.form // => The instance of CustomizedForm
```
经过 `Form.create` 包装的组件将会自带 `this.props.form` 属性,`this.props.form` 提供的 API 如下:
> 注意:使用 `getFieldsValue` `getFieldValue` `setFieldsValue` 等时,应确保对应的 field 已经用 `getFieldDecorator` 注册过了。
| 方法       | 说明                                     | 类型       |
| --- | --- | --- |
| getFieldDecorator | 用于和表单进行双向绑定,详见下方描述 | |
| getFieldError | 获取某个输入控件的 Error | Function(name) |
| getFieldsError | 获取一组输入控件的 Error ,如不传入参数,则获取全部组件的 Error | Function(\[names: string\[]]) |
| getFieldsValue | 获取一组输入控件的值,如不传入参数,则获取全部组件的值 | Function(\[fieldNames: string\[]]) |
| getFieldValue | 获取一个输入控件的值 | Function(fieldName: string) |
| isFieldsTouched | 判断是否任一输入控件经历过 `getFieldDecorator` 的值收集时机 `options.trigger` | (names?: string\[]) => boolean |
| isFieldTouched | 判断一个输入控件是否经历过 `getFieldDecorator` 的值收集时机 `options.trigger` | (name: string) => boolean |
| isFieldValidating | 判断一个输入控件是否在校验状态 | Function(name) |
| resetFields | 重置一组输入控件的值(为 `initialValue`)与状态,如不传入参数,则重置所有组件 | Function(\[names: string\[]]) |
| setFields | 设置一组输入控件的值与错误状态:[代码](https://github.com/react-component/form/blob/3b9959b57ab30b41d8890ff30c79a7e7c383cad3/examples/server-validate.js#L74-L79) | ({<br />&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>

89
components/form-legacy/interface.ts

@ -1,89 +0,0 @@
import * as React from 'react';
import { Omit } from '../_util/type';
import { WrappedFormInternalProps } from './Form';
// Heavily copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/hoist-non-react-statics/index.d.ts
// tslint:disable-next-line:class-name
interface REACT_STATICS {
childContextTypes: true;
contextType: true;
contextTypes: true;
defaultProps: true;
displayName: true;
getDefaultProps: true;
getDerivedStateFromError: true;
getDerivedStateFromProps: true;
mixins: true;
propTypes: true;
type: true;
}
// tslint:disable-next-line:class-name
interface KNOWN_STATICS {
name: true;
length: true;
prototype: true;
caller: true;
callee: true;
arguments: true;
arity: true;
}
// tslint:disable-next-line:class-name
interface MEMO_STATICS {
$$typeof: true;
compare: true;
defaultProps: true;
displayName: true;
propTypes: true;
type: true;
}
// tslint:disable-next-line:class-name
interface FORWARD_REF_STATICS {
$$typeof: true;
render: true;
defaultProps: true;
displayName: true;
propTypes: true;
}
type NonReactStatics<
S extends React.ComponentType<any>,
C extends {
[key: string]: true;
} = {}
> = {
[key in Exclude<
keyof S,
S extends React.MemoExoticComponent<any>
? keyof MEMO_STATICS | keyof C
: S extends React.ForwardRefExoticComponent<any>
? keyof FORWARD_REF_STATICS | keyof C
: keyof REACT_STATICS | keyof KNOWN_STATICS | keyof C
>]: S[key]
};
// Copy from @types/react-redux https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-redux/index.d.ts
export type Matching<InjectedProps, DecorationTargetProps> = {
[P in keyof DecorationTargetProps]: P extends keyof InjectedProps
? InjectedProps[P] extends DecorationTargetProps[P]
? DecorationTargetProps[P]
: InjectedProps[P]
: DecorationTargetProps[P]
};
export type GetProps<C> = C extends React.ComponentType<infer P> ? P : never;
export type ConnectedComponentClass<C extends React.ComponentType<any>, P> = React.ComponentClass<
JSX.LibraryManagedAttributes<C, P>
> &
NonReactStatics<C> & {
WrappedComponent: C;
};
export type FormWrappedProps<TOwnProps extends WrappedFormInternalProps> = <
C extends React.ComponentType<Matching<TOwnProps, GetProps<C>>>
>(
component: C,
) => ConnectedComponentClass<C, Omit<TOwnProps, keyof WrappedFormInternalProps>>;

681
components/form-legacy/style/index.less

@ -1,681 +0,0 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import '../../input/style/mixin';
@import '../../button/style/mixin';
@import '../../grid/style/mixin';
@import './mixin';
@form-prefix-cls: ~'@{ant-prefix}-form';
@form-component-height: @input-height-base;
@form-component-max-height: @input-height-lg;
@form-feedback-icon-size: @font-size-base;
@form-help-margin-top: (@form-component-height - @form-component-max-height) / 2 + 2px;
@form-explain-font-size: @font-size-base;
// Extends additional 1px to fix precision issue.
// https://github.com/ant-design/ant-design/issues/12803
// https://github.com/ant-design/ant-design/issues/8220
@form-explain-precision: 1px;
@form-explain-height: floor(@form-explain-font-size * @line-height-base);
.@{form-prefix-cls} {
.reset-component;
.reset-form;
}
.@{form-prefix-cls}-item-required::before {
display: inline-block;
margin-right: 4px;
color: @label-required-color;
font-size: @font-size-base;
font-family: SimSun, sans-serif;
line-height: 1;
content: '*';
.@{form-prefix-cls}-hide-required-mark & {
display: none;
}
}
.@{form-prefix-cls}-item-label > label {
color: @label-color;
&::after {
& when (@form-item-trailing-colon=true) {
content: ':';
}
& when not (@form-item-trailing-colon=true) {
content: ' ';
}
position: relative;
top: -0.5px;
margin: 0 8px 0 2px;
}
&.@{form-prefix-cls}-item-no-colon::after {
content: ' ';
}
}
// Radio && Checkbox
input[type='radio'],
input[type='checkbox'] {
&[disabled],
&.disabled {
cursor: not-allowed;
}
}
// These classes are used directly on <label>s
.@{ant-prefix}-radio-inline,
.@{ant-prefix}-radio-vertical,
.@{ant-prefix}-checkbox-inline,
.@{ant-prefix}-checkbox-vertical {
&.disabled {
cursor: not-allowed;
}
}
// These classes are used on elements with <label> descendants
.@{ant-prefix}-radio,
.@{ant-prefix}-checkbox {
&.disabled {
label {
cursor: not-allowed;
}
}
}
// Form items
// You should wrap labels and controls in .@{form-prefix-cls}-item for optimum spacing
.@{form-prefix-cls}-item {
label {
position: relative;
> .@{iconfont-css-prefix} {
font-size: @font-size-base;
vertical-align: top;
}
}
.reset-component;
margin-bottom: @form-item-margin-bottom;
vertical-align: top;
&-control {
position: relative;
line-height: @form-component-max-height;
.clearfix;
}
&-children {
position: relative;
}
&-with-help {
margin-bottom: max(0, @form-item-margin-bottom - @form-explain-height - @form-help-margin-top);
}
&-label {
display: inline-block;
overflow: hidden;
line-height: @form-component-max-height - 0.0001px;
white-space: nowrap;
text-align: right;
vertical-align: middle;
&-left {
text-align: left;
}
}
.@{ant-prefix}-switch {
margin: 2px 0 4px;
}
}
.@{form-prefix-cls}-explain,
.@{form-prefix-cls}-extra {
clear: both;
min-height: @form-explain-height + @form-explain-precision;
margin-top: @form-help-margin-top;
color: @text-color-secondary;
font-size: @form-explain-font-size;
line-height: @line-height-base;
transition: color 0.3s @ease-out; // sync input color transition
}
.@{form-prefix-cls}-explain {
margin-bottom: -@form-explain-precision;
}
.@{form-prefix-cls}-extra {
padding-top: 4px;
}
.@{form-prefix-cls}-text {
display: inline-block;
padding-right: 8px;
}
.@{form-prefix-cls}-split {
display: block;
text-align: center;
}
form {
.has-feedback {
.@{ant-prefix}-input {
padding-right: 24px;
}
.@{ant-prefix}-input-password-icon {
margin-right: 18px;
}
// Fix overlapping between feedback icon and <Select>'s arrow.
// https://github.com/ant-design/ant-design/issues/4431
> .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
> .@{ant-prefix}-select .@{ant-prefix}-select-selection__clear,
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
:not(.@{ant-prefix}-input-group-addon)
> .@{ant-prefix}-select
.@{ant-prefix}-select-selection__clear {
right: 28px;
}
> .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value,
:not(.@{ant-prefix}-input-group-addon)
> .@{ant-prefix}-select
.@{ant-prefix}-select-selection-selected-value {
padding-right: 42px;
}
.@{ant-prefix}-cascader-picker {
&-arrow {
margin-right: 17px;
}
&-clear {
right: 28px;
}
}
// Fix issue: https://github.com/ant-design/ant-design/issues/7854
.@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) {
.@{ant-prefix}-input-suffix {
right: 28px;
}
}
// Fix issue: https://github.com/ant-design/ant-design/issues/4783
.@{ant-prefix}-calendar-picker,
.@{ant-prefix}-time-picker {
&-icon,
&-clear {
right: 28px;
}
}
}
.@{ant-prefix}-mentions,
textarea.@{ant-prefix}-input {
height: auto;
margin-bottom: 4px;
}
// input[type=file]
.@{ant-prefix}-upload {
background: transparent;
}
input[type='radio'],
input[type='checkbox'] {
width: 14px;
height: 14px;
}
// Radios and checkboxes on same line
.@{ant-prefix}-radio-inline,
.@{ant-prefix}-checkbox-inline {
display: inline-block;
margin-left: 8px;
font-weight: normal;
vertical-align: middle;
cursor: pointer;
&:first-child {
margin-left: 0;
}
}
.@{ant-prefix}-checkbox-vertical,
.@{ant-prefix}-radio-vertical {
display: block;
}
.@{ant-prefix}-checkbox-vertical + .@{ant-prefix}-checkbox-vertical,
.@{ant-prefix}-radio-vertical + .@{ant-prefix}-radio-vertical {
margin-left: 0;
}
.@{ant-prefix}-input-number {
+ .@{form-prefix-cls}-text {
margin-left: 8px;
}
&-handler-wrap {
z-index: 2; // https://github.com/ant-design/ant-design/issues/6289
}
}
.@{ant-prefix}-select,
.@{ant-prefix}-cascader-picker {
width: 100%;
}
// Don't impact select inside input group
.@{ant-prefix}-input-group .@{ant-prefix}-select,
.@{ant-prefix}-input-group .@{ant-prefix}-cascader-picker {
width: auto;
}
// fix input with addon position. https://github.com/ant-design/ant-design/issues/8243
:not(.@{ant-prefix}-input-group-wrapper) > .@{ant-prefix}-input-group,
.@{ant-prefix}-input-group-wrapper {
position: relative;
top: -1px;
display: inline-block;
vertical-align: middle;
}
}
// Input combined with select
.@{ant-prefix}-input-group-wrap {
.@{ant-prefix}-select-selection {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
&:hover {
border-color: @border-color-base;
}
}
.@{ant-prefix}-select-selection--single {
height: @input-height-lg;
margin-left: -1px;
background-color: fade(@black, 7%);
.@{ant-prefix}-select-selection__rendered {
padding-right: 25px;
padding-left: 8px;
line-height: 30px;
}
}
.@{ant-prefix}-select-open .@{ant-prefix}-select-selection {
border-color: @border-color-base;
box-shadow: none;
}
}
// Form layout
//== Vertical Form
.make-vertical-layout-label() {
display: block;
margin: @form-vertical-label-margin;
padding: @form-vertical-label-padding;
line-height: @line-height-base;
white-space: initial;
text-align: left;
label::after {
display: none;
}
}
.make-vertical-layout() {
.@{form-prefix-cls}-item-label,
.@{form-prefix-cls}-item-control-wrapper {
display: block;
width: 100%;
}
.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
.@{form-prefix-cls}-vertical .@{form-prefix-cls}-item-label,
// when labelCol is 24, it is a vertical form
.@{ant-prefix}-col-24.@{form-prefix-cls}-item-label,
.@{ant-prefix}-col-xl-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
.@{form-prefix-cls}-vertical {
.@{form-prefix-cls}-item {
padding-bottom: 8px;
}
.@{form-prefix-cls}-item-control {
line-height: @line-height-base;
}
.@{form-prefix-cls}-explain {
margin-top: 2px;
margin-bottom: -4px - @form-explain-precision;
}
.@{form-prefix-cls}-extra {
margin-top: 2px;
margin-bottom: -4px;
}
}
@media (max-width: @screen-xs-max) {
.make-vertical-layout();
.@{ant-prefix}-col-xs-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-sm-max) {
.@{ant-prefix}-col-sm-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-md-max) {
.@{ant-prefix}-col-md-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-lg-max) {
.@{ant-prefix}-col-lg-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-xl-max) {
.@{ant-prefix}-col-xl-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
//== Inline Form
.@{form-prefix-cls}-inline {
.@{form-prefix-cls}-item {
display: inline-block;
margin-right: 16px;
margin-bottom: 0;
&-with-help {
margin-bottom: @form-item-margin-bottom;
}
> .@{form-prefix-cls}-item-control-wrapper,
> .@{form-prefix-cls}-item-label {
display: inline-block;
vertical-align: top;
}
}
.@{form-prefix-cls}-text {
display: inline-block;
}
.has-feedback {
display: inline-block;
}
}
// Validation state
.has-success,
.has-warning,
.has-error,
.is-validating {
&.has-feedback .@{form-prefix-cls}-item-children-icon {
position: absolute;
top: 50%;
right: 0;
z-index: 1;
width: @form-component-height;
height: 20px;
margin-top: -10px;
font-size: @form-feedback-icon-size;
line-height: 20px;
text-align: center;
visibility: visible;
animation: zoomIn 0.3s @ease-out-back;
pointer-events: none;
& svg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
}
}
.has-success {
&.has-feedback .@{form-prefix-cls}-item-children-icon {
color: @success-color;
animation-name: diffZoomIn1 !important;
}
}
.has-warning {
.form-control-validation(@warning-color; @warning-color; @form-warning-input-bg;);
&.has-feedback .@{form-prefix-cls}-item-children-icon {
color: @warning-color;
animation-name: diffZoomIn3 !important;
}
//select
.@{ant-prefix}-select {
&-selection {
border-color: @warning-color;
&:hover {
border-color: @warning-color;
}
}
&-open .@{ant-prefix}-select-selection,
&-focused .@{ant-prefix}-select-selection {
.active(@warning-color);
}
}
// arrow and icon
.@{ant-prefix}-calendar-picker-icon::after,
.@{ant-prefix}-time-picker-icon::after,
.@{ant-prefix}-picker-icon::after,
.@{ant-prefix}-select-arrow,
.@{ant-prefix}-cascader-picker-arrow {
color: @warning-color;
}
//input-number, timepicker
.@{ant-prefix}-input-number,
.@{ant-prefix}-time-picker-input {
border-color: @warning-color;
&-focused,
&:focus {
.active(@warning-color);
}
&:not([disabled]):hover {
border-color: @warning-color;
}
}
.@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input {
.active(@warning-color);
}
}
.has-error {
.form-control-validation(@error-color; @error-color; @form-error-input-bg;);
&.has-feedback .@{form-prefix-cls}-item-children-icon {
color: @error-color;
animation-name: diffZoomIn2 !important;
}
//select
.@{ant-prefix}-select {
&-selection {
border-color: @error-color;
&:hover {
border-color: @error-color;
}
}
&-open .@{ant-prefix}-select-selection,
&-focused .@{ant-prefix}-select-selection {
.active(@error-color);
}
}
.@{ant-prefix}-select.@{ant-prefix}-select-auto-complete {
.@{ant-prefix}-input:focus {
border-color: @error-color;
}
}
.@{ant-prefix}-input-group-addon .@{ant-prefix}-select {
&-selection {
border-color: transparent;
box-shadow: none;
}
}
// arrow and icon
.@{ant-prefix}-calendar-picker-icon::after,
.@{ant-prefix}-time-picker-icon::after,
.@{ant-prefix}-picker-icon::after,
.@{ant-prefix}-select-arrow,
.@{ant-prefix}-cascader-picker-arrow {
color: @error-color;
}
//input-number, timepicker
.@{ant-prefix}-input-number,
.@{ant-prefix}-time-picker-input {
border-color: @error-color;
&-focused,
&:focus {
.active(@error-color);
}
&:not([disabled]):hover {
border-color: @error-color;
}
}
.@{ant-prefix}-mention-wrapper {
.@{ant-prefix}-mention-editor {
&,
&:not([disabled]):hover {
border-color: @error-color;
}
}
&.@{ant-prefix}-mention-active:not([disabled]) .@{ant-prefix}-mention-editor,
.@{ant-prefix}-mention-editor:not([disabled]):focus {
.active(@error-color);
}
}
.@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input {
.active(@error-color);
}
// transfer
.@{ant-prefix}-transfer {
&-list {
border-color: @error-color;
&-search:not([disabled]) {
border-color: @input-border-color;
&:hover {
.hover();
}
&:focus {
.active();
}
}
}
}
}
.is-validating {
&.has-feedback .@{form-prefix-cls}-item-children-icon {
display: inline-block;
color: @primary-color;
}
}
.@{ant-prefix}-advanced-search-form {
.@{form-prefix-cls}-item {
margin-bottom: @form-item-margin-bottom;
&-with-help {
margin-bottom: @form-item-margin-bottom - @form-explain-height - @form-help-margin-top;
}
}
}
.show-help-motion(@className, @keyframeName, @duration: @animation-duration-slow) {
.make-motion(@className, @keyframeName, @duration);
.@{className}-enter,
.@{className}-appear {
opacity: 0;
animation-timing-function: @ease-in-out;
}
.@{className}-leave {
animation-timing-function: @ease-in-out;
}
}
.show-help-motion(show-help, antShowHelp, 0.3s);
@keyframes antShowHelpIn {
0% {
transform: translateY(-5px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes antShowHelpOut {
to {
transform: translateY(-5px);
opacity: 0;
}
}
// need there different zoom animation
// otherwise won't trigger anim
@keyframes diffZoomIn1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes diffZoomIn2 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes diffZoomIn3 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}

5
components/form-legacy/style/index.tsx

@ -1,5 +0,0 @@
import '../../style/index.less';
import './index.less';
// style dependencies
import '../../grid/style';

125
components/form-legacy/style/mixin.less

@ -1,125 +0,0 @@
@import '../../input/style/mixin';
.form-control-validation(@text-color: @input-color; @border-color: @input-border-color; @background-color: @input-bg) {
.@{ant-prefix}-form-explain,
.@{ant-prefix}-form-split {
color: @text-color;
}
// 输入框的不同校验状态
.@{ant-prefix}-input {
&,
&:hover {
border-color: @border-color;
}
&:focus {
.active(@border-color);
}
&:not([disabled]):hover {
border-color: @border-color;
}
}
.@{ant-prefix}-calendar-picker-open .@{ant-prefix}-calendar-picker-input {
.active(@border-color);
}
// Input prefix
.@{ant-prefix}-input-affix-wrapper {
.@{ant-prefix}-input {
&,
&:hover {
background-color: @background-color;
border-color: @border-color;
}
&:focus {
.active(@border-color);
}
}
&:hover .@{ant-prefix}-input:not(.@{ant-prefix}-input-disabled) {
border-color: @border-color;
}
}
.@{ant-prefix}-input-prefix {
color: @text-color;
}
.@{ant-prefix}-input-group-addon {
color: @text-color;
background-color: @background-color;
border-color: @border-color;
}
.has-feedback {
color: @text-color;
}
}
// Reset form styles
// -----------------------------
// Based on Bootstrap framework
.reset-form() {
legend {
display: block;
width: 100%;
margin-bottom: 20px;
padding: 0;
color: @text-color-secondary;
font-size: @font-size-lg;
line-height: inherit;
border: 0;
border-bottom: @border-width-base @border-style-base @border-color-base;
}
label {
font-size: @font-size-base;
}
input[type='search'] {
box-sizing: border-box;
}
// Position radios and checkboxes better
input[type='radio'],
input[type='checkbox'] {
line-height: normal;
}
input[type='file'] {
display: block;
}
// Make range inputs behave like textual form controls
input[type='range'] {
display: block;
width: 100%;
}
// Make multiple select elements height not fixed
select[multiple],
select[size] {
height: auto;
}
// Focus for file, radio, and checkbox
input[type='file']:focus,
input[type='radio']:focus,
input[type='checkbox']:focus {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
// Adjust output element
output {
display: block;
padding-top: 15px;
color: @input-color;
font-size: @font-size-base;
line-height: @line-height-base;
}
}

79
components/form/Form.tsx

@ -1,3 +1,80 @@
import Form from 'rc-field-form';
import * as React from 'react';
import omit from 'omit.js';
import classNames from 'classnames';
import FieldForm, { FormInstance, useForm, List } from 'rc-field-form';
import { FormProps as RcFormProps } from 'rc-field-form/lib/Form';
import { ColProps } from '../grid/col';
import { ConfigContext, ConfigConsumerProps } from '../config-provider';
import { FormContext } from './context';
import { FormLabelAlign } from './interface';
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
interface FormProps extends RcFormProps {
prefixCls?: string;
hideRequiredMark?: boolean;
colon?: boolean;
name?: string;
layout?: FormLayout;
labelAlign?: FormLabelAlign;
labelCol?: ColProps;
wrapperCol?: ColProps;
}
const InternalForm: React.FC<FormProps> = (props, ref) => {
const { getPrefixCls }: ConfigConsumerProps = React.useContext(ConfigContext);
const {
colon,
name,
labelAlign,
labelCol,
wrapperCol,
prefixCls: customizePrefixCls,
hideRequiredMark,
className = '',
layout = 'horizontal',
} = props;
const prefixCls = getPrefixCls('form', customizePrefixCls);
const formClassName = classNames(
prefixCls,
{
[`${prefixCls}-${layout}`]: true,
[`${prefixCls}-hide-required-mark`]: hideRequiredMark,
},
className,
);
const formProps = omit(props, [
'prefixCls',
'className',
'layout',
'hideRequiredMark',
'wrapperCol',
'labelAlign',
'labelCol',
'colon',
]);
return (
<FormContext.Provider
value={{
name,
labelAlign,
labelCol,
wrapperCol,
vertical: layout === 'vertical',
colon,
}}
>
<FieldForm id={name} {...formProps} ref={ref} className={formClassName} />
</FormContext.Provider>
);
};
const Form = React.forwardRef<FormInstance>(InternalForm);
export { useForm, List, FormInstance };
export default Form;

219
components/form/FormItem.tsx

@ -0,0 +1,219 @@
import * as React from 'react';
import isEqual from 'lodash/isEqual';
import classNames from 'classnames';
import { Field, FormInstance } from 'rc-field-form';
import { FieldProps as RcFieldProps } from 'rc-field-form/lib/Field';
import Row from '../grid/row';
import { ConfigContext } from '../config-provider';
import { tuple } from '../_util/type';
import warning from '../_util/warning';
import FormItemLabel, { FormItemLabelProps } from './FormItemLabel';
import FormItemInput, { FormItemInputProps } from './FormItemInput';
import { FormContext, FormItemContext } from './context';
import { toArray } from './util';
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
export type ValidateStatus = (typeof ValidateStatuses)[number];
type RenderChildren = (form: FormInstance) => React.ReactElement;
interface FormItemProps extends FormItemLabelProps, FormItemInputProps, RcFieldProps {
prefixCls?: string;
inline?: boolean;
style?: React.CSSProperties;
className?: string;
children: React.ReactElement | RenderChildren;
id?: string;
hasFeedback?: boolean;
validateStatus?: ValidateStatus;
required?: boolean;
/** Auto passed by List render props. User should not use this. */
fieldKey: number;
}
const FormItem: React.FC<FormItemProps> = (props: FormItemProps) => {
const {
name,
fieldKey,
inline,
dependencies,
prefixCls: customizePrefixCls,
style,
className,
shouldUpdate,
hasFeedback,
help,
rules,
validateStatus,
children,
required,
trigger = 'onChange',
validateTrigger = 'onChange',
} = props;
const { getPrefixCls } = React.useContext(ConfigContext);
const formContext = React.useContext(FormContext);
const { updateItemErrors } = React.useContext(FormItemContext);
const [domErrorVisible, setDomErrorVisible] = React.useState(false);
const [inlineErrors, setInlineErrors] = React.useState<Record<string, string[]>>({});
const { name: formName } = formContext;
// Cache Field NamePath
const nameRef = React.useRef<(string | number)[]>([]);
// Should clean up if Field removed
React.useEffect(() => {
return () => {
updateItemErrors(nameRef.current.join('__SPLIT__'), []);
};
}, []);
const prefixCls = getPrefixCls('form', customizePrefixCls);
return (
<Field
{...props}
trigger={trigger}
validateTrigger={validateTrigger}
onReset={() => {
setDomErrorVisible(false);
}}
>
{(control, meta, context) => {
const { errors, name: metaName } = meta;
const mergedName = toArray(name).length ? metaName : [];
// ======================== Errors ========================
// Collect inline Field error to the top FormItem
const updateChildItemErrors = inline
? updateItemErrors
: (subName: string, subErrors: string[]) => {
if (!isEqual(inlineErrors[subName], subErrors)) {
setInlineErrors({
...inlineErrors,
[subName]: subErrors,
});
}
};
if (inline) {
nameRef.current = [...mergedName];
if (fieldKey) {
nameRef.current[nameRef.current.length - 1] = fieldKey;
}
updateItemErrors(nameRef.current.join('__SPLIT__'), errors);
}
let mergedErrors: React.ReactNode[];
if (help) {
mergedErrors = toArray(help);
} else {
mergedErrors = errors;
Object.keys(inlineErrors).forEach(subName => {
const subErrors = inlineErrors[subName] || [];
if (subErrors.length) {
mergedErrors = [...mergedErrors, ...subErrors];
}
});
}
// ======================== Status ========================
let mergedValidateStatus: ValidateStatus = '';
if (validateStatus !== undefined) {
mergedValidateStatus = validateStatus;
} else if (meta.validating) {
mergedValidateStatus = 'validating';
} else if (!help && mergedErrors.length) {
mergedValidateStatus = 'error';
} else if (meta.touched) {
mergedValidateStatus = 'success';
}
// ====================== Class Name ======================
const itemClassName = {
[`${prefixCls}-item`]: true,
[`${prefixCls}-item-with-help`]: domErrorVisible, // TODO: handle this
[`${className}`]: !!className,
// Status
[`${prefixCls}-item-has-feedback`]:
(mergedValidateStatus && hasFeedback) || mergedValidateStatus === 'validating',
[`${prefixCls}-item-has-success`]: mergedValidateStatus === 'success',
[`${prefixCls}-item-has-warning`]: mergedValidateStatus === 'warning',
[`${prefixCls}-item-has-error`]: mergedValidateStatus === 'error',
[`${prefixCls}-item-has-error-leave`]:
!help && domErrorVisible && mergedValidateStatus !== 'error',
[`${prefixCls}-item-is-validating`]: mergedValidateStatus === 'validating',
};
// TODO: Check if user add `required` in RuleRender
const isRequired =
required !== undefined
? required
: !!(rules && rules.some(rule => typeof rule === 'object' && rule.required));
// ======================= Children =======================
const mergedId = mergedName.join('_');
const mergedControl: typeof control = {
...control,
id: formName ? `${formName}_${mergedId}` : mergedId,
};
let childNode;
if (typeof children === 'function' && (!shouldUpdate || !!name)) {
warning(false, 'Form.Item', '`children` of render props only work with `shouldUpdate`.');
} else if (!mergedName.length && !shouldUpdate && !dependencies) {
childNode = children;
} else if (React.isValidElement(children)) {
const childProps = { ...children.props, ...mergedControl };
// We should keep user origin event handler
const triggers = new Set<string>();
[...toArray(trigger), ...toArray(validateTrigger)].forEach(eventName => {
triggers.add(eventName);
});
triggers.forEach(eventName => {
if (eventName in mergedControl && eventName in children.props) {
childProps[eventName] = (...args: any[]) => {
mergedControl[eventName](...args);
children.props[eventName](...args);
};
}
});
childNode = React.cloneElement(children, childProps);
} else if (typeof children === 'function' && shouldUpdate && !name) {
childNode = children(context);
}
if (inline) {
return childNode;
}
return (
<Row type="flex" className={classNames(itemClassName)} style={style} key="row">
{/* Label */}
<FormItemLabel {...props} required={isRequired} prefixCls={prefixCls} />
{/* Input Group */}
<FormItemInput
{...props}
{...meta}
errors={mergedErrors}
prefixCls={prefixCls}
onDomErrorVisibleChange={setDomErrorVisible}
validateStatus={mergedValidateStatus}
>
<FormItemContext.Provider value={{ updateItemErrors: updateChildItemErrors }}>
{childNode}
</FormItemContext.Provider>
</FormItemInput>
</Row>
);
}}
</Field>
);
};
export default FormItem;

110
components/form/FormItemInput.tsx

@ -0,0 +1,110 @@
import * as React from 'react';
import classNames from 'classnames';
import Icon from '../icon';
import CSSMotion from 'rc-animate/lib/CSSMotion';
import Col, { ColProps } from '../grid/col';
import { ValidateStatus } from './FormItem';
import { FormContext } from './context';
import { useCacheErrors } from './util';
interface FormItemInputMiscProps {
prefixCls: string;
children: React.ReactNode;
errors: React.ReactNode[];
touched: boolean;
validating: boolean;
hasFeedback?: boolean;
validateStatus?: ValidateStatus;
onDomErrorVisibleChange: (visible: boolean) => void;
}
export interface FormItemInputProps {
wrapperCol?: ColProps;
help?: React.ReactNode;
extra?: React.ReactNode;
}
function getIconType(validateStatus?: ValidateStatus) {
switch (validateStatus) {
case 'success':
return 'check-circle';
case 'warning':
return 'exclamation-circle';
case 'error':
return 'close-circle';
case 'validating':
return 'loading';
default:
return '';
}
}
const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
prefixCls,
wrapperCol,
children,
errors,
onDomErrorVisibleChange,
hasFeedback,
validateStatus,
extra,
}) => {
const baseClassName = `${prefixCls}-item`;
const formContext = React.useContext(FormContext);
const mergedWrapperCol: ColProps = wrapperCol || formContext.wrapperCol || {};
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className);
const [visible, cacheErrors] = useCacheErrors(errors, changedVisible => {
if (changedVisible) {
onDomErrorVisibleChange(true);
}
});
// Should provides additional icon if `hasFeedback`
const iconType = getIconType(validateStatus);
const icon =
hasFeedback && iconType ? (
<span className={`${baseClassName}-children-icon`}>
<Icon type={iconType} theme={iconType === 'loading' ? 'outlined' : 'filled'} />
</span>
) : null;
// Pass to sub FormItem should not with col info
const subFormContext = { ...formContext };
delete subFormContext.labelCol;
delete subFormContext.wrapperCol;
return (
<FormContext.Provider value={subFormContext}>
<Col {...mergedWrapperCol} className={className}>
<div className={`${baseClassName}-control-input`}>
{children}
{icon}
</div>
<CSSMotion
visible={visible}
motionName="show-help"
onLeaveEnd={() => {
onDomErrorVisibleChange(false);
}}
motionAppear
removeOnLeave
>
{({ className: motionClassName }: { className: string }) => {
return (
<div className={classNames(`${baseClassName}-explain`, motionClassName)} key="help">
{cacheErrors}
</div>
);
}}
</CSSMotion>
{extra && <div className={`${baseClassName}-extra`}>{extra}</div>}
</Col>
</FormContext.Provider>
);
};
export default FormItemInput;

75
components/form/FormItemLabel.tsx

@ -0,0 +1,75 @@
import * as React from 'react';
import classNames from 'classnames';
import Col, { ColProps } from '../grid/col';
import { FormLabelAlign } from './interface';
import { FormContext, FormContextProps } from './context';
export interface FormItemLabelProps {
colon?: boolean;
htmlFor: string;
label?: React.ReactNode;
labelAlign?: FormLabelAlign;
labelCol?: ColProps;
}
const FormItemLabel: React.FC<FormItemLabelProps & { required: boolean; prefixCls: string }> = ({
prefixCls,
label,
htmlFor,
labelCol,
labelAlign,
colon,
required,
}) => {
if (!label) return null;
return (
<FormContext.Consumer key="label">
{({
vertical,
labelAlign: contextLabelAlign,
labelCol: contextLabelCol,
colon: contextColon,
}: FormContextProps) => {
const mergedLabelCol: ColProps = labelCol || contextLabelCol || {};
const mergedLabelAlign: FormLabelAlign | undefined = labelAlign || contextLabelAlign;
const labelClsBasic = `${prefixCls}-item-label`;
const labelColClassName = classNames(
labelClsBasic,
mergedLabelAlign === 'left' && `${labelClsBasic}-left`,
mergedLabelCol.className,
);
let labelChildren = label;
// Keep label is original where there should have no colon
const computedColon = colon === true || (contextColon !== false && colon !== false);
const haveColon = computedColon && !vertical;
// Remove duplicated user input colon
if (haveColon && typeof label === 'string' && (label as string).trim() !== '') {
labelChildren = (label as string).replace(/[:|:]\s*$/, '');
}
const labelClassName = classNames({
[`${prefixCls}-item-required`]: required,
[`${prefixCls}-item-no-colon`]: !computedColon,
});
return (
<Col {...mergedLabelCol} className={labelColClassName}>
<label
htmlFor={htmlFor}
className={labelClassName}
title={typeof label === 'string' ? label : ''}
>
{labelChildren}
</label>
</Col>
);
}}
</FormContext.Consumer>
);
};
export default FormItemLabel;

33
components/form/FormList.tsx

@ -0,0 +1,33 @@
import * as React from 'react';
import warning from '../_util/warning';
import { List } from 'rc-field-form';
interface FieldData {
name: number;
key: number;
fieldKey: number;
}
interface Operation {
add: () => void;
remove: (index: number) => void;
}
interface FormListProps {
name: string | number | (string | number)[];
children: (fields: FieldData[], operation: Operation) => React.ReactNode;
}
const FormList: React.FC<FormListProps> = ({ children, ...props }) => {
warning(!!props.name, 'Form.List', 'Miss `name` prop.');
return (
<List {...props}>
{(fields, operation) => {
return children(fields.map(field => ({ ...field, fieldKey: field.key })), operation);
}}
</List>
);
};
export default FormList;

4992
components/form/__tests__/__snapshots__/demo.test.js.snap

File diff suppressed because it is too large

126
components/form/__tests__/index.test.js

@ -0,0 +1,126 @@
import React from 'react';
import { mount } from 'enzyme';
import Form from '..';
import Input from '../../input';
import Button from '../../button';
const delay = () =>
new Promise(resolve => {
setTimeout(resolve, 0);
});
describe('Form', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
async function change(wrapper, index, value) {
wrapper
.find(Input)
.at(index)
.simulate('change', { target: { value } });
await delay();
wrapper.update();
}
afterEach(() => {
errorSpy.mockReset();
});
afterAll(() => {
errorSpy.mockRestore();
});
describe('List', () => {
function testList(name, renderField) {
it(name, async () => {
const wrapper = mount(
<Form>
<Form.List name="list">
{(fields, { add, remove }) => (
<React.Fragment>
{fields.map(field => renderField(field))}
<Button className="add" onClick={add}>
Add
</Button>
<Button
className="remove"
onClick={() => {
remove(1);
}}
>
Remove
</Button>
</React.Fragment>
)}
</Form.List>
</Form>,
);
async function operate(className) {
wrapper
.find(className)
.last()
.simulate('click');
await delay();
wrapper.update();
}
await operate('.add');
expect(wrapper.find(Input).length).toBe(1);
await operate('.add');
expect(wrapper.find(Input).length).toBe(2);
await change(wrapper, 1, '');
expect(wrapper.find('.ant-form-item-explain').length).toBe(1);
await operate('.remove');
expect(wrapper.find(Input).length).toBe(1);
expect(wrapper.find('.ant-form-item-explain').length).toBe(0);
});
}
testList('operation correctly', field => (
<Form.Item {...field} rules={[{ required: true }]}>
<Input />
</Form.Item>
));
testList('nest inline', field => (
<Form.Item key={field.key}>
<Form.Item inline {...field} rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form.Item>
));
});
it('inline Form.Item', async () => {
const onChange = jest.fn();
const wrapper = mount(
<Form>
<Form.Item>
<Form.Item name="test" rules={[{ required: true }]}>
<Input onChange={onChange} />
</Form.Item>
</Form.Item>
</Form>,
);
await change(wrapper, 0, '');
expect(wrapper.find('.ant-form-item-explain').length).toBe(1);
expect(onChange).toHaveBeenCalled();
});
it('`shouldUpdate` should work with render props', () => {
mount(
<Form>
<Form.Item>{() => null}</Form.Item>
</Form>,
);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Form.Item] `children` of render props only work with `shouldUpdate`.',
);
});
});

91
components/form/__tests__/type.tsx

@ -1,91 +0,0 @@
/* tslint:disable */
import * as React from 'react';
import Form, { FormComponentProps, FormCreateOption } from '../Form';
// test Form.create on component without own props
class WithoutOwnProps extends React.Component<any, any> {
state = {
foo: 'bar',
};
render() {
return <div>foo</div>;
}
}
const WithoutOwnPropsForm = Form.create()(WithoutOwnProps);
<WithoutOwnPropsForm />;
// test Form.create on component with own props
interface WithOwnPropsProps extends FormComponentProps {
name: string;
}
class WithOwnProps extends React.Component<WithOwnPropsProps, any> {
state = {
foo: 'bar',
};
render() {
return <div>foo</div>;
}
}
const WithOwnPropsForm = Form.create<WithOwnPropsProps>()(WithOwnProps);
<WithOwnPropsForm name="foo" />;
// test Form.create with options
interface WithCreateOptionsProps extends FormComponentProps {
username: string;
}
class WithCreateOptions extends React.Component<WithCreateOptionsProps, {}> {
render() {
return <div>foo</div>;
}
}
const mapPropsToFields = (props: WithCreateOptionsProps) => {
const { username } = props;
return {
username: Form.createFormField({ value: username }),
};
};
const formOptions: FormCreateOption<WithCreateOptionsProps> = { mapPropsToFields };
const WithCreateOptionsForm = Form.create(formOptions)(WithCreateOptions);
<WithCreateOptionsForm username="foo" />;
// Should work with forwardRef & wrappedComponentRef
// https://github.com/ant-design/ant-design/issues/16229
if (React.forwardRef) {
interface ForwardProps extends FormComponentProps {
str: string;
}
const ForwardDemo = React.forwardRef(({ str }: ForwardProps, ref: React.Ref<HTMLDivElement>) => {
return <div ref={ref}>{str || ''}</div>;
});
const WrappedForwardDemo = Form.create<ForwardProps>()(ForwardDemo);
<WrappedForwardDemo str="" />;
}
interface WrappedRefProps extends FormComponentProps {
str: string;
test?: () => void;
}
class WrapRefDemo extends React.Component<WrappedRefProps> {
public getForm() {
return this.props.form;
}
public render() {
return <div>{this.props.str || ''}</div>;
}
}
const WrappedWrapRefDemo = Form.create<WrappedRefProps>()(WrapRefDemo);
<WrappedWrapRefDemo str="" wrappedComponentRef={() => null} />;

5
components/form/changelog.md

@ -0,0 +1,5 @@
# Form Dom 变化
- 状态 className 现在移动到顶层,不再是 input only
- 去除 `ant-form-item-control-wrapper` 一层 div
- `.has-success` 等状态样式添加 `ant-form-item` 前缀

47
components/form/context.tsx

@ -0,0 +1,47 @@
import * as React from 'react';
import omit from 'omit.js';
import { FormProvider as RcFormProvider } from 'rc-field-form';
import { FormProviderProps as RcFormProviderProps } from 'rc-field-form/lib/FormContext';
import { ColProps } from '../grid/col';
import { FormLabelAlign } from './interface';
/**
* Form Context
* Set top form style and pass to Form Item usage.
*/
export interface FormContextProps {
vertical: boolean;
name?: string;
colon?: boolean;
labelAlign?: FormLabelAlign;
labelCol?: ColProps;
wrapperCol?: ColProps;
}
export const FormContext = React.createContext<FormContextProps>({
labelAlign: 'right',
vertical: false,
});
/**
* Form Item Context
* Used for Form inline Item error collection
*/
export interface FormItemContextProps {
updateItemErrors: (name: string, errors: string[]) => void;
}
export const FormItemContext = React.createContext<FormItemContextProps>({
updateItemErrors: () => {},
});
/**
* Form Provider
*
*/
export interface FormProviderProps extends Omit<RcFormProviderProps, 'validateMessages'> {}
export const FormProvider: React.FC<FormProviderProps> = props => {
const providerProps = omit(props, ['prefixCls']);
return <RcFormProvider {...providerProps} />;
};

129
components/form/demo/advanced-search.md

@ -0,0 +1,129 @@
---
order: 13
title:
zh-CN: 高级搜索
en-US: Advanced search
---
## zh-CN
三列栅格式的表单排列方式,常用于数据表格的高级搜索。
有部分定制的样式代码,由于输入标签长度不确定,需要根据具体情况自行调整。
## en-US
Three columns layout is often used for advanced searching of data table.
Because the width of label is not fixed, you may need to adjust it by customizing its style.
```tsx
import { Form, Row, Col, Input, Button, Icon } from 'antd';
const AdvancedSearchForm = () => {
const [expand, setExpand] = React.useState(false);
const [form] = Form.useForm();
const getFields = () => {
const count = expand ? 10 : 6;
const children = [];
for (let i = 0; i < count; i++) {
children.push(
<Col span={8} key={i}>
<Form.Item
name={`field-${i}`}
label={`Field ${i}`}
rules={[
{
required: true,
message: 'Input something!',
},
]}
>
<Input placeholder="placeholder" />
</Form.Item>
</Col>,
);
}
return children;
};
const onFinish = values => {
console.log('Received values of form: ', values);
};
return (
<Form
form={form}
name="advanced_search"
className="ant-advanced-search-form"
onFinish={onFinish}
>
<Row gutter={24}>{getFields()}</Row>
<Row>
<Col span={24} style={{ textAlign: 'right' }}>
<Button type="primary" htmlType="submit">
Search
</Button>
<Button
style={{ marginLeft: 8 }}
onClick={() => {
form.resetFields();
}}
>
Clear
</Button>
<a
style={{ marginLeft: 8, fontSize: 12 }}
onClick={() => {
setExpand(!expand);
}}
>
Collapse <Icon type={expand ? 'up' : 'down'} />
</a>
</Col>
</Row>
</Form>
);
};
ReactDOM.render(
<div>
<AdvancedSearchForm />
<div className="search-result-list">Search Result List</div>
</div>,
mountNode,
);
```
```css
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
```
<style>
#components-form-demo-advanced-search .ant-form {
max-width: none;
}
#components-form-demo-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
</style>

66
components/form/demo/basic.md

@ -0,0 +1,66 @@
---
order: 0
title:
zh-CN: 基本使用
en-US: Basic Usage
---
## zh-CN
基本的表单数据域控制展示,包含布局、初始化、验证、提交。
## en-US
Basic Form data control. Includes layout, initial values, validation and submit.
```tsx
import { Form, Input, Button, Checkbox } from 'antd';
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
};
const tailLayout = {
wrapperCol: { offset: 8, span: 16 },
};
const Demo = () => {
const onFinish = values => {
console.log('Success:', values);
};
const onFinishFailed = errorInfo => {
console.log('Failed:', errorInfo);
};
return (
<Form
{...layout}
name="basic"
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item label="Username" name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item label="Password" name="password" rules={[{ required: true }]}>
<Input.Password />
</Form.Item>
<Form.Item {...tailLayout} name="remember" valuePropName="checked">
<Checkbox>Remember me</Checkbox>
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
ReactDOM.render(<Demo />, mountNode);
```

103
components/form/demo/control-hooks.md

@ -0,0 +1,103 @@
---
order: 1
title:
zh-CN: 表单方法调用
en-US: Form methods
---
## zh-CN
通过 `Form.useForm` 对表单数据域进行交互。
## en-US
Call form method with `Form.useForm`.
```tsx
import { Form, Input, Button, Select } from 'antd';
const { Option } = Select;
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
};
const tailLayout = {
wrapperCol: { offset: 8, span: 16 },
};
const Demo = () => {
const [form] = Form.useForm();
const onGenderChange = value => {
form.setFieldsValue({
note: `Hi, ${value === 'male' ? 'man' : 'lady'}!`,
});
};
const onFinish = values => {
console.log(values);
};
const onReset = () => {
form.resetFields();
};
const onFill = () => {
form.setFieldsValue({
note: 'Hello world!',
gender: 'male',
});
};
return (
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
<Form.Item name="note" label="Note" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="gender" label="Gender" rules={[{ required: true }]}>
<Select
placeholder="Select a option and change input text above"
onChange={onGenderChange}
allowClear
>
<Option value="male">male</Option>
<Option value="female">female</Option>
<Option value="other">other</Option>
</Select>
</Form.Item>
<Form.Item
inline
shouldUpdate={(prevValues, currentValues) => prevValues.gender !== currentValues.gender}
>
{({ getFieldValue }) => {
return getFieldValue('gender') === 'other' ? (
<Form.Item name="customizeGender" label="Customize Gender" rules={[{ required: true }]}>
<Input />
</Form.Item>
) : null;
}}
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
Submit
</Button>
<Button htmlType="button" onClick={onReset}>
Reset
</Button>
<Button type="link" htmlType="button" onClick={onFill}>
Fill form
</Button>
</Form.Item>
</Form>
);
};
ReactDOM.render(<Demo />, mountNode);
```
```css
#components-form-demo-control-hooks .ant-btn {
margin-right: 8px;
}
```

109
components/form/demo/control-ref.md

@ -0,0 +1,109 @@
---
order: 2
title:
zh-CN: 表单方法调用(Class component)
en-US: Form methods (Class component)
---
## zh-CN
我们推荐使用 `Form.useForm` 创建表单数据域进行控制。如果是在 class component 下,你也可以通过 `ref` 获取数据域。
## en-US
We recommend use `Form.useForm` to create data control. If you are using class component, you can get it by `ref`.
```tsx
import { Form, Input, Button, Select } from 'antd';
const { Option } = Select;
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
};
const tailLayout = {
wrapperCol: { offset: 8, span: 16 },
};
class Demo extends React.Component {
formRef = React.createRef();
onGenderChange = value => {
this.formRef.current.setFieldsValue({
note: `Hi, ${value === 'male' ? 'man' : 'lady'}!`,
});
};
onFinish = values => {
console.log(values);
};
onReset = () => {
this.formRef.current.resetFields();
};
onFill = () => {
this.formRef.current.setFieldsValue({
note: 'Hello world!',
gender: 'male',
});
};
render() {
return (
<Form {...layout} ref={this.formRef} name="control-ref" onFinish={this.onFinish}>
<Form.Item name="note" label="Note" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="gender" label="Gender" rules={[{ required: true }]}>
<Select
placeholder="Select a option and change input text above"
onChange={this.onGenderChange}
allowClear
>
<Option value="male">male</Option>
<Option value="female">female</Option>
<Option value="other">other</Option>
</Select>
</Form.Item>
<Form.Item
inline
shouldUpdate={(prevValues, currentValues) => prevValues.gender !== currentValues.gender}
>
{({ getFieldValue }) => {
return getFieldValue('gender') === 'other' ? (
<Form.Item
name="customizeGender"
label="Customize Gender"
rules={[{ required: true }]}
>
<Input />
</Form.Item>
) : null;
}}
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
Submit
</Button>
<Button htmlType="button" onClick={this.onReset}>
Reset
</Button>
<Button type="link" htmlType="button" onClick={this.onFill}>
Fill form
</Button>
</Form.Item>
</Form>
);
}
}
ReactDOM.render(<Demo />, mountNode);
```
```css
#components-form-demo-control-ref .ant-btn {
margin-right: 8px;
}
```

118
components/form/demo/customized-form-controls.md

@ -0,0 +1,118 @@
---
order: 6
title:
zh-CN: 自定义表单控件
en-US: Customized Form Controls
---
## zh-CN
自定义或第三方的表单控件,也可以与 Form 组件一起使用。只要该组件遵循以下的约定:
> - 提供受控属性 `value` 或其它与 [`valuePropName`](http://ant.design/components/form/#getFieldDecorator-参数) 的值同名的属性。
> - 提供 `onChange` 事件或 [`trigger`](http://ant.design/components/form/#getFieldDecorator-参数) 的值同名的事件。
## en-US
Customized or third-party form controls can be used in Form, too. Controls must follow these conventions:
> - It has a controlled property `value` or other name which is equal to the value of [`valuePropName`](http://ant.design/components/form/?locale=en-US#getFieldDecorator's-parameters).
> - It has event `onChange` or an event which name is equal to the value of [`trigger`](http://ant.design/components/form/?locale=en-US#getFieldDecorator's-parameters).
```tsx
import { Form, Input, Select, Button } from 'antd';
const { Option } = Select;
interface PriceValue {
number?: number;
currency?: 'rmb' | 'dollar';
}
interface PriceInputProps {
value?: PriceValue;
onChange?: (value: PriceValue) => void;
}
const PriceInput: React.FC<PriceInputProps> = ({ value = {}, onChange }) => {
const [number, setNumber] = React.useState(0);
const [currency, setCurrency] = React.useState('rmb');
const triggerChange = changedValue => {
if (onChange) {
onChange({ number, currency, ...value, ...changedValue });
}
};
const onNumberChange = e => {
const newNumber = parseInt(e.target.value || 0, 10);
if (Number.isNaN(number)) {
return;
}
if (!('number' in value)) {
setNumber(newNumber);
}
triggerChange({ number: newNumber });
};
const onCurrencyChange = newCurrency => {
if (!('currency' in value)) {
setCurrency(newCurrency);
}
triggerChange({ currency: newCurrency });
};
return (
<span>
<Input
type="text"
value={value.number || number}
onChange={onNumberChange}
style={{ width: 100, marginRight: 8 }}
/>
<Select value={value.currency || currency} style={{ width: 80 }} onChange={onCurrencyChange}>
<Option value="rmb">RMB</Option>
<Option value="dollar">Dollar</Option>
</Select>
</span>
);
};
const Demo = () => {
const onFinish = values => {
console.log('Received values of form: ', values);
};
const checkPrice = (rule, value) => {
if (value.number > 0) {
return Promise.resolve();
}
return Promise.reject('Price must greater than zero!');
};
return (
<Form
name="customized_form_controls"
layout="inline"
onFinish={onFinish}
initialValues={{
price: {
number: 0,
currency: 'rmb',
},
}}
>
<Form.Item name="price" label="Price" rules={[{ validator: checkPrice }]}>
<PriceInput />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
ReactDOM.render(<Demo />, mountNode);
```

123
components/form/demo/dynamic-form-item.md

@ -0,0 +1,123 @@
---
order: 4
title:
zh-CN: 动态增减表单项
en-US: Dynamic Form Item
---
## zh-CN
动态增加、减少表单项。
## en-US
Add or remove form items dynamically.
```jsx
import { Form, Input, Icon, Button } from 'antd';
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 4 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 20 },
},
};
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: { span: 24, offset: 0 },
sm: { span: 20, offset: 4 },
},
};
const DynamicFieldSet = () => {
const onFinish = values => {
console.log('Received values of form:', values);
};
return (
<Form name="dynamic_form_item" {...formItemLayoutWithOutLabel} onFinish={onFinish}>
<Form.List name="names">
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
label={index === 0 ? 'Passengers' : ''}
required={false}
key={field.key}
>
<Form.Item
{...field}
validateTrigger={['onChange', 'onBlur']}
rules={[
{
required: true,
whitespace: true,
message: "Please input passenger's name or delete this field.",
},
]}
inline
>
<Input placeholder="passenger name" style={{ width: '60%', marginRight: 8 }} />
</Form.Item>
{fields.length > 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => {
remove(field.name);
}}
/>
) : null}
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: '60%' }}
>
<Icon type="plus" /> Add field
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
ReactDOM.render(<DynamicFieldSet />, mountNode);
```
```css
.dynamic-delete-button {
cursor: pointer;
position: relative;
top: 4px;
font-size: 24px;
color: #999;
transition: all 0.3s;
}
.dynamic-delete-button:hover {
color: #777;
}
.dynamic-delete-button[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
```

92
components/form/demo/dynamic-rule.md

@ -0,0 +1,92 @@
---
order: 23
title:
zh-CN: 动态校验规则
en-US: Dynamic Rules
---
## zh-CN
根据不同情况执行不同的校验规则。
## en-US
Perform different check rules according to different situations.
```tsx
import { Form, Input, Button, Checkbox } from 'antd';
const formItemLayout = {
labelCol: { span: 4 },
wrapperCol: { span: 8 },
};
const formTailLayout = {
labelCol: { span: 4 },
wrapperCol: { span: 8, offset: 4 },
};
const DynamicRule = () => {
const [form] = Form.useForm();
const [checkNick, setCheckNick] = React.useState(false);
React.useEffect(() => {
form.validateFields(['nickname']);
}, [checkNick]);
const onCheckboxChange = e => {
setCheckNick(e.target.checked);
};
const onCheck = async () => {
try {
const values = await form.validateFields();
console.log('Success:', values);
} catch (errorInfo) {
console.log('Failed:', errorInfo);
}
};
return (
<Form form={form} name="dynamic_rule">
<Form.Item
{...formItemLayout}
name="username"
label="Name"
rules={[
{
required: true,
message: 'Please input your name',
},
]}
>
<Input placeholder="Please input your name" />
</Form.Item>
<Form.Item
{...formItemLayout}
name="nickname"
label="Nickname"
rules={[
{
required: checkNick,
message: 'Please input your nickname',
},
]}
>
<Input placeholder="Please input your nickname" />
</Form.Item>
<Form.Item {...formTailLayout}>
<Checkbox checked={checkNick} onChange={onCheckboxChange}>
Nickname is required
</Checkbox>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" onClick={onCheck}>
Check
</Button>
</Form.Item>
</Form>
);
};
ReactDOM.render(<DynamicRule />, mountNode);
```

137
components/form/demo/form-context.md

@ -0,0 +1,137 @@
---
order: 8
title:
zh-CN: 多表单联动
en-US: Control between forms
---
## zh-CN
通过 `Form.Provider` 在表单间处理数据。本例子中,Modal 的确认按钮在 Form 之外,通过 `form.submit` 方法调用表单提交功能。反之,则推荐使用 `<Button htmlType="submit" />` 调用 web 原生提交逻辑。
## en-US
Use `Form.Provider` to process data between forms. In this case, submit button is in the Modal which is out of Form. You can use `form.submit` to submit form. Besides, we recommend native `<Button htmlType="submit" />` to submit a form.
```tsx
import { Form, Input, InputNumber, Modal, Icon, Button, Avatar, Typography } from 'antd';
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
};
const tailLayout = {
wrapperCol: { offset: 8, span: 16 },
};
interface ModalFormProps {
visible: boolean;
onCancel: () => void;
}
const ModalForm: React.FC<ModalFormProps> = ({ visible, onCancel }) => {
const [form] = Form.useForm();
React.useEffect(() => {
form.resetFields();
}, [visible]);
const onOk = () => {
form.submit();
};
return (
<Modal title="Basic Drawer" visible={visible} onOk={onOk} onCancel={onCancel}>
<Form form={form} layout="vertical" name="userForm">
<Form.Item name="name" label="User Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="age" label="User Age" rules={[{ required: true }]}>
<InputNumber />
</Form.Item>
</Form>
</Modal>
);
};
const Demo = () => {
const [visible, setVisible] = React.useState(false);
const showUserModal = () => {
setVisible(true);
};
const hideUserModal = () => {
setVisible(false);
};
const onFinish = values => {
console.log('Finish:', values);
};
return (
<div>
<Form.Provider
onFormFinish={(name, { values, forms }) => {
if (name === 'userForm') {
const { basicForm } = forms;
const users = basicForm.getFieldValue('users') || [];
basicForm.setFieldsValue({ users: [...users, values] });
setVisible(false);
}
}}
>
<Form {...layout} name="basicForm" onFinish={onFinish}>
<Form.Item name="group" label="Group Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item
label="User List"
shouldUpdate={(prevValues, curValues) => prevValues.users !== curValues.users}
>
{({ getFieldValue }) => {
const users = getFieldValue('users') || [];
return users.length ? (
<ul>
{users.map((user, index) => (
<li key={index} className="user">
<Avatar icon="user" />
{user.name} - {user.age}
</li>
))}
</ul>
) : (
<Typography.Text className="ant-form-text" type="secondary">
( <Icon type="smile" /> No user yet. )
</Typography.Text>
);
}}
</Form.Item>
<Form.Item {...tailLayout}>
<Button htmlType="submit" type="primary">
Submit
</Button>
<Button htmlType="button" style={{ marginLeft: 8 }} onClick={showUserModal}>
Add User
</Button>
</Form.Item>
</Form>
<ModalForm visible={visible} onCancel={hideUserModal} />
</Form.Provider>
</div>
);
};
ReactDOM.render(<Demo />, mountNode);
```
```css
#components-form-demo-form-context .user {
margin-bottom: 8px;
}
#components-form-demo-form-context .user .ant-avatar {
margin-right: 8px;
}
```

119
components/form/demo/form-in-modal.md

@ -0,0 +1,119 @@
---
order: 14
title:
zh-CN: 弹出层中的新建表单
en-US: Form in Modal to Create
---
## zh-CN
当用户访问一个展示了某个列表的页面,想新建一项但又不想跳转页面时,可以用 Modal 弹出一个表单,用户填写必要信息后创建新的项。
## en-US
When user visit a page with a list of items, and want to create a new item. The page can popup a form in Modal, then let user fill in the form to create an item.
```tsx
import { Button, Modal, Form, Input, Radio } from 'antd';
interface Values {
title: string;
description: string;
modifier: string;
}
interface CollectionCreateFormProps {
visible: boolean;
onCreate: (values: Values) => void;
onCancel: () => void;
}
const CollectionCreateForm: React.FC<CollectionCreateFormProps> = ({
visible,
onCreate,
onCancel,
}) => {
const [form] = Form.useForm();
return (
<Modal
visible={visible}
title="Create a new collection"
okText="Create"
cancelText="Cancel"
onCancel={onCancel}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
onCreate(values);
})
.catch(info => {
console.log('Validate Failed:', info);
});
}}
>
<Form
form={form}
layout="vertical"
name="form_in_modal"
initialValues={{ modifier: 'public' }}
>
<Form.Item
name="title"
label="Title"
rules={[{ required: true, message: 'Please input the title of collection!' }]}
>
<Input />
</Form.Item>
<Form.Item name="description" label="Description">
<Input type="textarea" />
</Form.Item>
<Form.Item name="modifier" className="collection-create-form_last-form-item">
<Radio.Group>
<Radio value="public">Public</Radio>
<Radio value="private">Private</Radio>
</Radio.Group>
</Form.Item>
</Form>
</Modal>
);
};
const CollectionsPage = () => {
const [visible, setVisible] = React.useState(false);
const onCreate = values => {
console.log('Received values of form: ', values);
setVisible(false);
};
return (
<div>
<Button
type="primary"
onClick={() => {
setVisible(true);
}}
>
New Collection
</Button>
<CollectionCreateForm
visible={visible}
onCreate={onCreate}
onCancel={() => {
setVisible(false);
}}
/>
</div>
);
};
ReactDOM.render(<CollectionsPage />, mountNode);
```
```css
.collection-create-form_last-form-item {
margin-bottom: 0;
}
```

82
components/form/demo/global-state.md

@ -0,0 +1,82 @@
---
order: 7
title:
zh-CN: 表单数据存储于上层组件
en-US: Store Form Data into Upper Component
---
## zh-CN
通过 `onFieldsChange``fields`,可以把表单的数据存储到上层组件或者 [Redux](https://github.com/reactjs/redux)、[dva](https://github.com/dvajs/dva) 中,更多可参考 [rc-field-form 示例](https://rc-field-form.react-component.now.sh/?selectedKind=rc-field-form&selectedStory=StateForm-redux&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybook%2Factions%2Factions-panel)。
**注意:** 将表单数据存储于外部容器[并非好的实践](https://github.com/reduxjs/redux/issues/1287#issuecomment-175351978),如无必要请避免使用。
## en-US
We can store form data into upper component or [Redux](https://github.com/reactjs/redux) or [dva](https://github.com/dvajs/dva) by using `onFieldsChange` and `fields`, see more at this [rc-field-form demo](https://rc-field-form.react-component.now.sh/?selectedKind=rc-field-form&selectedStory=StateForm-redux&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybook%2Factions%2Factions-panel).
**Note:** Save Form data globally [is not a good practice](https://github.com/reduxjs/redux/issues/1287#issuecomment-175351978). You should avoid this if not necessary.
```tsx
import { Form, Input } from 'antd';
interface FieldData {
name: string[];
value: any;
touched: boolean;
validating: boolean;
errors: string[];
}
interface CustomizedFormProps {
onChange: (fields: FieldData[]) => void;
fields: FieldData[];
}
const CustomizedForm: React.FC<CustomizedFormProps> = ({ onChange, fields }) => {
return (
<Form
name="global_state"
layout="inline"
fields={fields}
onFieldsChange={(changedFields, allFields) => {
onChange(allFields);
}}
>
<Form.Item
name="username"
label="Username"
rules={[{ required: true, message: 'Username is required!' }]}
>
<Input />
</Form.Item>
</Form>
);
};
const Demo = () => {
const [fields, setFields] = React.useState([{ name: ['username'], value: 'Ant Design' }]);
return (
<div>
<CustomizedForm
fields={fields}
onChange={newFields => {
setFields(newFields);
}}
/>
<pre className="language-bash">{JSON.stringify(fields, null, 2)}</pre>
</div>
);
};
ReactDOM.render(<Demo />, mountNode);
```
<style>
#components-form-demo-global-state .language-bash {
max-width: 400px;
border-radius: 6px;
margin-top: 24px;
}
</style>

50
components/form/demo/horizontal-login.md

@ -1,5 +1,5 @@
---
order: 0
order: 10
title:
zh-CN: 水平登录栏
en-US: Horizontal Login Form
@ -16,18 +16,54 @@ Horizontal login form is often used in navigation bar.
```tsx
import { Form, Icon, Input, Button } from 'antd';
interface FieldProps {
name: string;
}
const HorizontalLoginForm = () => {
const [form] = Form.useForm();
const [, forceUpdate] = React.useState();
// To disabled submit button at the beginning.
React.useEffect(() => {
forceUpdate({});
}, []);
const onFinish = values => {
console.log('Finish:', values);
};
return (
<Form layout="inline" onFinish={onFinish}>
DDDD2333
<Form form={form} name="horizontal_login" layout="inline" onFinish={onFinish}>
<Form.Item
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="Username"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Please input your password!' }]}
>
<Input
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="Password"
/>
</Form.Item>
<Form.Item shouldUpdate={true}>
{() => (
<Button
type="primary"
htmlType="submit"
disabled={
!form.isFieldsTouched(true) ||
form.getFieldsError().filter(({ errors }) => errors.length).length
}
>
Log in
</Button>
)}
</Form.Item>
</Form>
);
};

65
components/form/demo/layout.md

@ -0,0 +1,65 @@
---
order: 3
title:
zh-CN: 表单布局
en-US: Form Layout
---
## zh-CN
表单有三种布局。
## en-US
There are three layout for form: `horizontal`, `vertical`, `inline`.
```tsx
import { Form, Input, Button, Radio } from 'antd';
const FormLayoutDemo = () => {
const [formLayout, setFormLayout] = React.useState('horizontal');
const onFormLayoutChange = e => {
setFormLayout(e.target.value);
};
const formItemLayout =
formLayout === 'horizontal'
? {
labelCol: { span: 4 },
wrapperCol: { span: 14 },
}
: null;
const buttonItemLayout =
formLayout === 'horizontal'
? {
wrapperCol: { span: 14, offset: 4 },
}
: null;
return (
<div>
<Form layout={formLayout}>
<Form.Item label="Form Layout" {...formItemLayout}>
<Radio.Group defaultValue="horizontal" onChange={onFormLayoutChange}>
<Radio.Button value="horizontal">Horizontal</Radio.Button>
<Radio.Button value="vertical">Vertical</Radio.Button>
<Radio.Button value="inline">Inline</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="Field A" {...formItemLayout}>
<Input placeholder="input placeholder" />
</Form.Item>
<Form.Item label="Field B" {...formItemLayout}>
<Input placeholder="input placeholder" />
</Form.Item>
<Form.Item {...buttonItemLayout}>
<Button type="primary">Submit</Button>
</Form.Item>
</Form>
</div>
);
};
ReactDOM.render(<FormLayoutDemo />, mountNode);
```

67
components/form/demo/nest-messages.md

@ -0,0 +1,67 @@
---
order: 5
title:
zh-CN: 嵌套结构与校验信息
en-US: Nest
---
## zh-CN
`name` 属性支持嵌套数据结构。通过 `validateMessages``message` 自定义校验信息模板,模板内容可参考[此处](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts)。
## en-US
`name` prop support nest data structure. Customize validate message template with `validateMessages` or `message`. Ref [here](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) about message template.
```tsx
import { Form, Input, InputNumber, Button } from 'antd';
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
};
const validateMessages = {
required: 'This field is required!',
types: {
email: 'Not a validate email!',
number: 'Not a validate number!',
},
number: {
range: 'Must be between ${min} and ${max}',
},
};
const Demo = () => {
const onFinish = values => {
console.log(values);
};
return (
<Form {...layout} name="nest-messages" onFinish={onFinish} validateMessages={validateMessages}>
<Form.Item name={['user', 'name']} label="Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name={['user', 'email']} label="Email" rules={[{ type: 'email' }]}>
<Input />
</Form.Item>
<Form.Item name={['user', 'age']} label="Age" rules={[{ type: 'number', min: 0, max: 99 }]}>
<InputNumber />
</Form.Item>
<Form.Item name={['user', 'website']} label="Website">
<Input />
</Form.Item>
<Form.Item name={['user', 'introduction']} label="Introduction">
<Input.TextArea />
</Form.Item>
<Form.Item wrapperCol={{ ...layout.wrapperCol, offset: 8 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
ReactDOM.render(<Demo />, mountNode);
```

83
components/form/demo/normal-login.md

@ -0,0 +1,83 @@
---
order: 11
title:
zh-CN: 登录框
en-US: Login Form
---
## zh-CN
普通的登录框,可以容纳更多的元素。
## en-US
Normal login form which can contain more elements.
```tsx
import { Form, Icon, Input, Button, Checkbox } from 'antd';
const NormalLoginForm = () => {
const onFinish = values => {
console.log('Received values of form: ', values);
};
return (
<Form
name="normal_login"
className="login-form"
initialValues={{ remember: true }}
onFinish={onFinish}
>
<Form.Item
name="username"
rules={[{ required: true, message: 'Please input your Username!' }]}
>
<Input
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="Username"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Please input your Password!' }]}
>
<Input
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="Password"
/>
</Form.Item>
<Form.Item>
<Form.Item name="remember" valuePropName="checked" inline>
<Checkbox>Remember me</Checkbox>
</Form.Item>
<a className="login-form-forgot" href="">
Forgot password
</a>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" className="login-form-button">
Log in
</Button>
Or <a href="">register now!</a>
</Form.Item>
</Form>
);
};
ReactDOM.render(<NormalLoginForm />, mountNode);
```
```css
#components-form-demo-normal-login .login-form {
max-width: 300px;
}
#components-form-demo-normal-login .login-form-forgot {
float: right;
}
#components-form-demo-normal-login .login-form-button {
width: 100%;
}
```

259
components/form/demo/register.md

@ -0,0 +1,259 @@
---
order: 12
title:
zh-CN: 注册新用户
en-US: Registration
---
## zh-CN
用户填写必须的信息以注册新用户。
## en-US
Fill in this form to create a new account for you.
```tsx
import {
Form,
Input,
Tooltip,
Icon,
Cascader,
Select,
Row,
Col,
Checkbox,
Button,
AutoComplete,
} from 'antd';
const { Option } = Select;
const AutoCompleteOption = AutoComplete.Option;
const residences = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
},
],
},
],
},
];
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 8 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 24,
offset: 0,
},
sm: {
span: 16,
offset: 8,
},
},
};
const RegistrationForm = () => {
const onFinish = values => {
console.log('Received values of form: ', values);
};
const prefixSelector = (
<Form.Item name="prefix" inline>
<Select style={{ width: 70 }}>
<Option value="86">+86</Option>
<Option value="87">+87</Option>
</Select>
</Form.Item>
);
const [autoCompleteResult, setAutoCompleteResult] = React.useState([]);
const onWebsiteChange = value => {
if (!value) {
setAutoCompleteResult([]);
} else {
setAutoCompleteResult(['.com', '.org', '.net'].map(domain => `${value}${domain}`));
}
};
const websiteOptions = autoCompleteResult.map(website => (
<AutoCompleteOption key={website}>{website}</AutoCompleteOption>
));
return (
<Form
{...formItemLayout}
name="register"
onFinish={onFinish}
initialValues={{
residence: ['zhejiang', 'hangzhou', 'xihu'],
prefix: '86',
}}
>
<Form.Item
name="email"
label="E-mail"
rules={[
{
type: 'email',
message: 'The input is not valid E-mail!',
},
{
required: true,
message: 'Please input your E-mail!',
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="password"
label="Password"
rules={[
{
required: true,
message: 'Please input your password!',
},
]}
hasFeedback
>
<Input.Password />
</Form.Item>
<Form.Item
name="confirm"
label="Confirm Password"
dependencies={['password']}
hasFeedback
rules={[
{
required: true,
message: 'Please confirm your password!',
},
({ getFieldValue }) => ({
validator(rule, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve();
}
return Promise.reject('Two passwords that you enter is inconsistent!');
},
}),
]}
>
<Input.Password />
</Form.Item>
<Form.Item
name="nickname"
label={
<span>
Nickname&nbsp;
<Tooltip title="What do you want others to call you?">
<Icon type="question-circle-o" />
</Tooltip>
</span>
}
rules={[{ required: true, message: 'Please input your nickname!', whitespace: true }]}
>
<Input />
</Form.Item>
<Form.Item
name="residence"
label="Habitual Residence"
rules={[
{ type: 'array', required: true, message: 'Please select your habitual residence!' },
]}
>
<Cascader options={residences} />
</Form.Item>
<Form.Item
name="phone"
label="Phone Number"
rules={[{ required: true, message: 'Please input your phone number!' }]}
>
<Input addonBefore={prefixSelector} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="website"
label="Website"
rules={[{ required: true, message: 'Please input website!' }]}
>
<AutoComplete dataSource={websiteOptions} onChange={onWebsiteChange} placeholder="website">
<Input />
</AutoComplete>
</Form.Item>
<Form.Item label="Captcha" extra="We must make sure that your are a human.">
<Row gutter={8}>
<Col span={12}>
<Form.Item
name="captcha"
inline
rules={[{ required: true, message: 'Please input the captcha you got!' }]}
>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Button>Get captcha</Button>
</Col>
</Row>
</Form.Item>
<Form.Item name="agreement" valuePropName="checked" {...tailFormItemLayout}>
<Checkbox>
I have read the <a href="">agreement</a>
</Checkbox>
</Form.Item>
<Form.Item {...tailFormItemLayout}>
<Button type="primary" htmlType="submit">
Register
</Button>
</Form.Item>
</Form>
);
};
ReactDOM.render(<RegistrationForm />, mountNode);
```

93
components/form/demo/time-related-controls.md

@ -0,0 +1,93 @@
---
order: 16
title:
zh-CN: 时间类控件
en-US: Time-related Controls
---
## zh-CN
时间类组件的 `value` 类型为 `moment` 对象,所以在提交服务器前需要预处理。
## en-US
The `value` of time-related components is a `moment` object, which we need to pre-process it before we submit to server.
```tsx
import { Form, DatePicker, TimePicker, Button } from 'antd';
const { MonthPicker, RangePicker } = DatePicker;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 8 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
const config = {
rules: [{ type: 'object', required: true, message: 'Please select time!' }],
};
const rangeConfig = {
rules: [{ type: 'array', required: true, message: 'Please select time!' }],
};
const TimeRelatedForm = () => {
const onFinish = fieldsValue => {
// Should format date value before submit.
const rangeValue = fieldsValue['range-picker'];
const rangeTimeValue = fieldsValue['range-time-picker'];
const values = {
...fieldsValue,
'date-picker': fieldsValue['date-picker'].format('YYYY-MM-DD'),
'date-time-picker': fieldsValue['date-time-picker'].format('YYYY-MM-DD HH:mm:ss'),
'month-picker': fieldsValue['month-picker'].format('YYYY-MM'),
'range-picker': [rangeValue[0].format('YYYY-MM-DD'), rangeValue[1].format('YYYY-MM-DD')],
'range-time-picker': [
rangeTimeValue[0].format('YYYY-MM-DD HH:mm:ss'),
rangeTimeValue[1].format('YYYY-MM-DD HH:mm:ss'),
],
'time-picker': fieldsValue['time-picker'].format('HH:mm:ss'),
};
console.log('Received values of form: ', values);
};
return (
<Form name="time_related_controls" {...formItemLayout} onFinish={onFinish}>
<Form.Item name="date-picker" label="DatePicker" {...config}>
<DatePicker />
</Form.Item>
<Form.Item name="date-time-picker" label="DatePicker[showTime]" {...config}>
<DatePicker showTime format="YYYY-MM-DD HH:mm:ss" />
</Form.Item>
<Form.Item name="month-picker" label="MonthPicker" {...config}>
<MonthPicker />
</Form.Item>
<Form.Item name="range-picker" label="RangePicker" {...rangeConfig}>
<RangePicker />
</Form.Item>
<Form.Item name="range-time-picker" label="RangePicker[showTime]" {...rangeConfig}>
<RangePicker showTime format="YYYY-MM-DD HH:mm:ss" />
</Form.Item>
<Form.Item name="time-picker" label="TimePicker" {...config}>
<TimePicker />
</Form.Item>
<Form.Item
wrapperCol={{
xs: { span: 24, offset: 0 },
sm: { span: 16, offset: 8 },
}}
>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
ReactDOM.render(<TimeRelatedForm />, mountNode);
```

204
components/form/demo/validate-other.md

@ -0,0 +1,204 @@
---
order: 24
title:
zh-CN: 校验其他组件
en-US: Other Form Controls
---
## zh-CN
以上演示没有出现的表单控件对应的校验演示。
## en-US
Demonstration of validation configuration for form controls which are not shown in the demos above.
```tsx
import {
Form,
Select,
InputNumber,
Switch,
Radio,
Slider,
Button,
Upload,
Icon,
Rate,
Checkbox,
Row,
Col,
} from 'antd';
const { Option } = Select;
const formItemLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 14 },
};
const normFile = e => {
console.log('Upload event:', e);
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
};
const Demo = () => {
const onFinish = values => {
console.log('Received values of form: ', values);
};
return (
<Form
name="validate_other"
{...formItemLayout}
onFinish={onFinish}
initialValues={{
['input-number']: 3,
['checkbox-group']: ['A', 'B'],
rate: 3.5,
}}
>
<Form.Item label="Plain Text">
<span className="ant-form-text">China</span>
</Form.Item>
<Form.Item
name="select"
label="Select"
hasFeedback
rules={[{ required: true, message: 'Please select your country!' }]}
>
<Select placeholder="Please select a country">
<Option value="china">China</Option>
<Option value="usa">U.S.A</Option>
</Select>
</Form.Item>
<Form.Item
name="select-multiple"
label="Select[multiple]"
rules={[{ required: true, message: 'Please select your favourite colors!', type: 'array' }]}
>
<Select mode="multiple" placeholder="Please select favourite colors">
<Option value="red">Red</Option>
<Option value="green">Green</Option>
<Option value="blue">Blue</Option>
</Select>
</Form.Item>
<Form.Item label="InputNumber">
<Form.Item name="input-number" inline>
<InputNumber min={1} max={10} />
</Form.Item>
<span className="ant-form-text"> machines</span>
</Form.Item>
<Form.Item name="switch" label="Switch" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="slider" label="Slider">
<Slider
marks={{
0: 'A',
20: 'B',
40: 'C',
60: 'D',
80: 'E',
100: 'F',
}}
/>
</Form.Item>
<Form.Item name="radio-group" label="Radio.Group">
<Radio.Group>
<Radio value="a">item 1</Radio>
<Radio value="b">item 2</Radio>
<Radio value="c">item 3</Radio>
</Radio.Group>
</Form.Item>
<Form.Item name="radio-button" label="Radio.Button">
<Radio.Group>
<Radio.Button value="a">item 1</Radio.Button>
<Radio.Button value="b">item 2</Radio.Button>
<Radio.Button value="c">item 3</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item name="checkbox-group" label="Checkbox.Group">
<Checkbox.Group style={{ width: '100%' }}>
<Row>
<Col span={8}>
<Checkbox value="A">A</Checkbox>
</Col>
<Col span={8}>
<Checkbox disabled value="B">
B
</Checkbox>
</Col>
<Col span={8}>
<Checkbox value="C">C</Checkbox>
</Col>
<Col span={8}>
<Checkbox value="D">D</Checkbox>
</Col>
<Col span={8}>
<Checkbox value="E">E</Checkbox>
</Col>
</Row>
</Checkbox.Group>
</Form.Item>
<Form.Item name="rate" label="Rate">
<Rate />
</Form.Item>
<Form.Item
name="upload"
label="Upload"
valuePropName="fileList"
getValueFromEvent={normFile}
extra="longgggggggggggggggggggggggggggggggggg"
>
<Upload name="logo" action="/upload.do" listType="picture">
<Button>
<Icon type="upload" /> Click to upload
</Button>
</Upload>
</Form.Item>
<Form.Item label="Dragger">
<div className="dropbox">
<Form.Item name="dragger" valuePropName="fileList" getValueFromEvent={normFile} inline>
<Upload.Dragger name="files" action="/upload.do">
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">Click or drag file to this area to upload</p>
<p className="ant-upload-hint">Support for a single or bulk upload.</p>
</Upload.Dragger>
</Form.Item>
</div>
</Form.Item>
<Form.Item wrapperCol={{ span: 12, offset: 6 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
ReactDOM.render(<Demo />, mountNode);
```
```css
#components-form-demo-validate-other .dropbox {
height: 180px;
line-height: 1.5;
}
```

8
components/form-legacy/demo/validate-static.md → components/form/demo/validate-static.md

@ -1,5 +1,5 @@
---
order: 10
order: 20
title:
zh-CN: 自定义校验
en-US: Customized Validation
@ -7,7 +7,7 @@ title:
## zh-CN
我们提供了 `validateStatus` `help` `hasFeedback` 等属性,你可以不需要使用 `Form.create``getFieldDecorator`自己定义校验的时机和内容。
我们提供了 `validateStatus` `help` `hasFeedback` 等属性,你可以不通过 Form 自己定义校验的时机和内容。
1. `validateStatus`: 校验状态,可选 'success', 'warning', 'error', 'validating'。
2. `hasFeedback`:用于给输入框添加反馈图标。
@ -15,13 +15,13 @@ title:
## en-US
We provide properties like `validateStatus` `help` `hasFeedback` to customize your own validate status and message, without using `Form.create` and `getFieldDecorator`.
We provide properties like `validateStatus` `help` `hasFeedback` to customize your own validate status and message, without using Form.
1. `validateStatus`: validate status of form components which could be 'success', 'warning', 'error', 'validating'.
2. `hasFeedback`: display feed icon of input control
3. `help`: display validate message.
```jsx
```tsx
import { Form, Input, DatePicker, TimePicker, Select, Cascader, InputNumber } from 'antd';
const { Option } = Select;

67
components/form/demo/without-form-create.md

@ -0,0 +1,67 @@
---
order: 19
title:
zh-CN: 自行处理表单数据
en-US: Handle Form Data Manually
---
## zh-CN
`Form` 具有自动收集数据并校验的功能,但如果您不需要这个功能,或者默认的行为无法满足业务需求,可以选择自行处理数据。
## en-US
`Form` will collect and validate form data automatically. But if you don't need this feature or the default behavior cannot satisfy your business, you can handle form data manually.
```tsx
import { Form, InputNumber } from 'antd';
function validatePrimeNumber(number) {
if (number === 11) {
return {
validateStatus: 'success',
errorMsg: null,
};
}
return {
validateStatus: 'error',
errorMsg: 'The prime between 8 and 12 is 11!',
};
}
const formItemLayout = {
labelCol: { span: 7 },
wrapperCol: { span: 12 },
};
const RawForm = () => {
const [number, setNumber] = React.useState({
value: 11,
});
const tips =
'A prime is a natural number greater than 1 that has no positive divisors other than 1 and itself.';
const onNumberChange = value => {
setNumber({
...validatePrimeNumber(value),
value,
});
};
return (
<Form>
<Form.Item
{...formItemLayout}
label="Prime between 8 & 12"
validateStatus={number.validateStatus}
help={number.errorMsg || tips}
>
<InputNumber min={8} max={12} value={number.value} onChange={onNumberChange} />
</Form.Item>
</Form>
);
};
ReactDOM.render(<RawForm />, mountNode);
```

394
components/form/index.en-US.md

@ -5,243 +5,253 @@ 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.
High performance Form component with data scope management. Including data collection, verification, and styles.
## 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 |
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| 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 |
| component | Set the Form rendering element. Do not create a DOM node for `false` | ComponentType \| false | form |
| colon | Configure the default value of `colon` for Form.Item. Indicates whether the colon after the label is displayed | boolean | true |
| fields | Control of form fields through state management (such as redux). Not recommended for non-strong demand. View [example](#components-form-demo-global-state) | [FieldData](#FieldData)\[] | - |
| form | Form control instance created by `Form.useForm()`. Automatically created when not provided | [FormInstance](#FormInstance) | - |
| hideRequiredMark | Hide required mark for all form items | boolean | false |
| initialValues | Set value by Form initialization or reset | object | - |
| labelAlign | Label text alignment | 'left' \| 'right' | 'right' |
| labelCol | label layout, like `<Col>` component. Set `span` `offset` value like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` | [object](https://ant.design/components/grid/#Col) | |
| layout | Form layout | 'horizontal'\|'vertical'\|'inline' | 'horizontal' |
| name | Form name. Will be the prefix of Field `id` | string | - |
| validateMessages | 验证提示模板,说明[见下](#validateMessages) | [ValidateMessages](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) | - |
| wrapperCol | The layout for input controls, same as `labelCol` | [object](https://ant.design/components/grid/#Col) | |
| onFinish | Trigger after submitting the form and verifying data successfully | Function(values) | - |
| onFinishFailed | Trigger after submitting the form and verifying data failed | Function({ values, errorFields, outOfDate }) | - |
| onFieldsChange | Trigger when field updated | Function(changedFields, allFields) | - |
| onValuesChange | Trigger when value updated | Function(changedValues, allValues) | - |
### validateMessages
Form provides [default verification error messages](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts). You can modify template by configuring `validateMessages` property. A common usage is to configure localization:
### Form.create(options)
```jsx
const validateMessages = {
required: "'${name}' is required!",
// ...
};
How to use:
<Form validateMessages={validateMessages} />;
```
Besides,[ConfigProvider](/components/config-provider/) also provides a global configuration scheme that allows for uniform configuration error notification templates:
```jsx
class CustomizedForm extends React.Component {}
const validateMessages = {
required: "'${name}' is Required!",
// ...
};
CustomizedForm = Form.create({})(CustomizedForm);
<ConfigProvider form={{ validateMessages }}>
<Form />
</ConfigProvider>;
```
The following `options` are available:
## Form.Item
| 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 |
Form field component for data bidirectional binding, validation, layout, and so on.
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).
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| colon | Used with `label`, whether to display `:` after label text. | boolean | true |
| dependencies | Set the dependency field. See [below](#dependencies) | [NamePath](#NamePath)[] | - |
| 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 | - |
| getValueFromEvent | Specify how to get value from event or other onChange arguments | (..args: any[]) => any | - |
| 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 | - |
| inline | No style for `true`, used as a pure field control | boolean | false |
| 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 `labelCol` on Form. If both exists, use Item first | [object](/components/grid/#Col) | - |
| name | Field name, support array | [NamePath](#NamePath) | - |
| normalize | Normalize value to form component | (value, prevValue, prevValues) => any | - |
| required | Whether provided or not, it will be generated by the validation rule | boolean | false |
| rules | Rules for field validation. Click [here](#components-form-demo-basic) to see an example | [Rule](#Rule)[] | - |
| shouldUpdate | Custom field update logic. See [bellow](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false |
| trigger | When to collect the value of children node | string | onChange |
| validateStatus | The validation status. If not provided, it will be generated by validation rule. options: 'success' 'warning' 'error' 'validating' | string | |
| validateTrigger | When to validate the value of children node | string \| string[] | onChange |
| valuePropName | Props of children node, for example, the prop of Switch is 'checked' | string | value |
| wrapperCol | The layout for input controls, same as `labelCol`. You can set `wrapperCol` on Form. If both exists, use Item first | [object](/components/grid/#Col) | |
### dependencies
Used when there are dependencies between fields. If a field has the `dependencies` prop, this field will automatically trigger updates and validations when upstream updated. A common scenario is user register form with "password" and "confirm password" fields. The "Confirm Password" validation depends on the "Password" field. After setting `dependencies`, the "Password" field update will re-trigger the validation of "Check Password". You can refer [examples](#components-form-demo-register).
### shouldUpdate
Form updates only the modified field-related components for performance optimization purposes by incremental update. In most case, you only need to write code or do validation with the [`dependencies`](#dependencies) property. And in some specific case, such as a new field option appears with a filed value changed, or just want to keep some area updating by form update. You can modify the update logic of Form.Item via the `shouldUpdate`.
When `shouldUpdate` is `true`, any Form update will cause the Form.Item to be re-rendered. This is very helpful for custom rendering some areas:
```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.Item shouldUpdate>
{() => {
return <pre>{JSON.stringify(form.getFieldsValue(), null, 2)}</pre>;
}}
</Form.Item>
```
If the form has been decorated by `Form.create` then it has `this.props.form` property. `this.props.form` provides some APIs as follows:
You can ref [example](#components-form-demo-horizontal-login) to see detail.
> 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
When `shouldUpdate` is a function, it will be called by form values update. Providing original values and current value to compare. This is very helpful for rendering additional fields based on values:
```jsx
const {
form: { validateFields },
} = this.props;
validateFields((errors, values) => {
// ...
});
validateFields(['field1', 'field2'], (errors, values) => {
// ...
});
validateFields(['field1', 'field2'], options, (errors, values) => {
// ...
});
<Form.Item shouldUpdate={(prevValues, curValues) => prevValues.additional !== curValues.additional}>
{() => {
return (
<Form.Item name="other">
<Input />
</Form.Item>
);
}}
</Form.Item>
```
| Method | Description | Type | Default |
You can ref [example](#components-form-demo-control-hooks) to see detail.
## Form.List
Provides array management for fields.
| Property | 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"
}
]
}
}
```
| name | Field name, support array | [NamePath](#NamePath) | - |
| children | Render function | (fields: Field[], operation: { add, remove }) => React.ReactNode | - |
- `values`:
```tsx
<Form.List>
{fields => (
<div>
{fields.map(field => (
<Form.Item {...field}>
<Input />
</Form.Item>
))}
</div>
)}
</Form.List>
```
```js
{
"username": "username",
"password": "password",
}
```
## Form.Provider
### Form.createFormField
Provide linkage between forms. If a sub form with `name` prop update, it will auto trigger Provider related events. See [example](#components-form-demo-form-context).
To mark the returned fields data in `mapPropsToFields`, [demo](#components-form-demo-global-state).
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| onFormChange | Triggered when a sub form field updates | Function(formName: string, info: { changedFields, forms }) | - |
| onFormFinish | Triggered when a sub form submits | Function(formName: string, info: { values, forms }) | - |
### this.props.form.getFieldDecorator(id, options)
```jsx
<Form.Provider
onFormFinish={name => {
if (name === 'form1') {
// Do something...
}
}}
>
<Form name="form1">...</Form>
<Form name="form2">...</Form>
</Form.Provider>
```
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:
### FormInstance
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.
| Name | Description | Type |
| --- | --- | --- |
| getFieldValue | Get the value by the field name | (name: [NamePath](#NamePath)) => any |
| getFieldsValue | Get values by a set of field names. Return according to the corresponding structure | (nameList?: [NamePath](#NamePath)[]) => any |
| getFieldError | Get the error messages by the field name | (name: [NamePath](#NamePath)) => string[] |
| getFieldsError | Get the error messages by the fields name. Return as an array | (nameList?: [NamePath](#NamePath)[]) => FieldError[] |
| isFieldTouched | Check if a field has been operated | (name: [NamePath](#NamePath)) => boolean |
| isFieldsTouched | Check if fields have been operated. Check if all fields is touched when `allTouched` is `true` | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean |
| isFieldValidating | Check fields if is in validating | (name: [NamePath](#NamePath)) => boolean |
| resetFields | Reset fields to `initialValues` | (fields?: [NamePath](#NamePath)[]) => void |
| setFields | Set fields status | (fields: FieldData[]) => void |
| setFieldsValue | Set fields value | (values) => void |
| submit | Submit the form. It's same as click `submit` button | () => void |
| validateFields | Validate fields | (nameList?: [NamePath](#NamePath)[]) => Promise |
#### validateFields return sample
#### Special attention
```jsx
validateFields()
.then(values => {
/*
values:
{
username: 'username',
password: 'password',
}
*/
})
.catch(errorInfo => {
/*
errorInfo:
{
values: {
username: 'username',
password: 'password',
},
errorFields: [
{ password: ['username'], errors: ['Please input your Password!'] },
],
outOfDate: false,
}
*/
});
```
If you use `react@<15.3.0`, then, you can't use `getFieldDecorator` in stateless component: <https://github.com/facebook/react/pull/6534>
### Interface
#### getFieldDecorator(id, options) parameters
#### NamePath
| 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
`string | number | (string | number)[]`
```tsx
import { Form } from 'antd';
import { FormComponentProps } from 'antd/lib/form';
#### FieldData
interface UserFormProps extends FormComponentProps {
age: number;
name: string;
}
| Name | Description | Type |
| ---------- | ------------------------ | ----------------------- |
| touched | Whether is operated | boolean |
| validating | Whether is in validating | boolean |
| errors | Error messages | string[] |
| name | Field name path | [NamePath](#NamePath)[] |
| value | Field value | any |
class UserForm extends React.Component<UserFormProps, any> {
// ...
}
#### Rule
const App = Form.create<UserFormProps>({
// ...
})(UserForm);
```
| Name | Description | Type |
| --- | --- | --- |
| enum | Match enum value | any[] |
| len | Length of string, number, array | number |
| max | Max length of string, number, array | number |
| message | Error message. Will auto generate by [template](#validateMessages) if not provided | string |
| min | Min length of string, number, array | number |
| pattern | Regex pattern | RegExp |
| required | Required field | boolean |
| transform | Transform value to the rule before validation | (value) => any |
| type | Normally `string` \|`number` \|`boolean` \|`url` \| `email`. More type to ref [here](https://github.com/yiminghe/async-validator#type) | string |
| validator | Customize validation rule. Accept Promise as return. [example](#components-form-demo-register)参考 | ([rule](#Rule), value) => Promise |
| whitespace | Failed if only has whitespace | boolean |
| validateTrigger | Set validate trigger event. Must be the sub set of `validateTrigger` in Form.Item | string \| string[] |
## Migrate to v4
If you are the user of v3, you can ref [migrate doc](/components/form/v3)。
<style>
.code-box-demo .ant-form:not(.ant-form-inline):not(.ant-form-vertical) {

22
components/form/index.tsx

@ -1,3 +1,23 @@
import Form from './Form';
import InternalForm, { useForm, FormInstance } from './Form';
import Item from './FormItem';
import List from './FormList';
import { FormProvider } from './context';
type InternalForm = typeof InternalForm;
interface Form extends InternalForm {
useForm: typeof useForm;
Item: typeof Item;
List: typeof List;
Provider: typeof FormProvider;
}
const Form: Form = InternalForm as Form;
Form.Item = Item;
Form.List = List;
Form.useForm = useForm;
Form.Provider = FormProvider;
export { FormInstance };
export default Form;

387
components/form/index.zh-CN.md

@ -6,245 +6,254 @@ 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 |
| component | 设置 Form 渲染元素,为 `false` 则不创建 DOM 节点 | ComponentType \| false | form |
| colon | 配置 Form.Item 的 `colon` 的默认值。表示是否显示 label 后面的冒号 | boolean | true |
| fields | 通过状态管理(如 redux)控制表单字段,如非强需求不推荐使用。查看[示例](#components-form-demo-global-state) | [FieldData](#FieldData)\[] | - |
| form | 经 `Form.useForm()` 创建的 form 控制实例,不提供时会自动创建 | [FormInstance](#FormInstance) | - |
| hideRequiredMark | 隐藏所有表单项的必选标记 | boolean | false |
| initialValues | 表单默认值,只有初始化以及重置时生效 | object | - |
| 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) | |
| labelCol | 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 |
| name | 表单名称,会作为表单字段 `id` 前缀使用 | string | - |
| validateMessages | 验证提示模板,说明[见下](#validateMessages) | [ValidateMessages](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) | - |
| wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol | [object](https://ant.design/components/grid/#Col) | |
| onFinish | 提交表单且数据验证成功后回调事件 | Function(values) | - |
| onFinishFailed | 提交表单且数据验证失败后回调事件 | Function({ values, errorFields, outOfDate }) | - |
| onFieldsChange | 字段更新时触发回调事件 | Function(changedFields, allFields) | - |
| onValuesChange | 字段值更新时触发回调事件 | Function(changedValues, allValues) | - |
### Form.create(options)
### validateMessages
使用方式如下:
Form 为验证提供了[默认的错误提示信息](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts),你可以通过配置 `validateMessages` 属性,修改对应的提示模板。一种常见的使用方式,是配置国际化提示信息
```jsx
class CustomizedForm extends React.Component {}
const validateMessages = {
required: "'${name}' 是必选字段",
// ...
};
CustomizedForm = Form.create({})(CustomizedForm);
<Form validateMessages={validateMessages} />;
```
`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)。
此外,[ConfigProvider](/components/config-provider/) 也提供了全局化配置方案,允许统一配置错误提示模板:
```jsx
class CustomizedForm extends React.Component { ... }
const validateMessages = {
required: "'${name}' 是必选字段",
// ...
};
// use wrappedComponentRef
const EnhancedForm = Form.create()(CustomizedForm);
<EnhancedForm wrappedComponentRef={(form) => this.form = form} />
this.form // => The instance of CustomizedForm
<ConfigProvider form={{ validateMessages }}>
<Form />
</ConfigProvider>;
```
经过 `Form.create` 包装的组件将会自带 `this.props.form` 属性,`this.props.form` 提供的 API 如下:
> 注意:使用 `getFieldsValue` `getFieldValue` `setFieldsValue` 等时,应确保对应的 field 已经用 `getFieldDecorator` 注册过了。
## Form.Item
| 方法       | 说明                                     | 类型       |
| --- | --- | --- |
| 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"
}
]
}
}
```
| colon | 配合 `label` 属性使用,表示是否显示 `label` 后面的冒号 | boolean | true |
| dependencies | 设置依赖字段,说明[见下](#dependencies) | [NamePath](#NamePath)[] | - |
| extra | 额外的提示信息,和 `help` 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 | string\|ReactNode | - |
| getValueFromEvent | 设置如何将 event 的值转换成字段值 | (..args: any[]) => any | - |
| hasFeedback | 配合 `validateStatus` 属性使用,展示校验状态图标,建议只配合 Input 组件使用 | boolean | false |
| help | 提示信息,如不设置,则会根据校验规则自动生成 | string\|ReactNode | - |
| htmlFor | 设置子元素 label `htmlFor` 属性 | string | - |
| inline | 为 `true` 时不带样式,作为纯字段控件使用 | boolean | false |
| label | `label` 标签的文本 | string\|ReactNode | - |
| labelCol | `label` 标签布局,同 `<Col>` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}``sm: {span: 3, offset: 12}`。你可以通过 Form 的 `labelCol` 进行统一设置。当和 Form 同时设置时,以 Item 为准 | [object](/components/grid/#Col) | - |
| name | 字段名,支持数组 | [NamePath](#NamePath) | - |
| normalize | 转换字段值给控件 | (value, prevValue, prevValues) => any | - |
| required | 是否必填,如不设置,则会根据校验规则自动生成 | boolean | false |
| rules | 校验规则,设置字段的校验逻辑。点击[此处](#components-form-demo-basic)查看示例 | [Rule](#Rule)[] | - |
| shouldUpdate | 自定义字段更新逻辑,说明[见下](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false |
| trigger | 设置收集字段值变更的时机 | string | onChange |
| validateStatus | 校验状态,如不设置,则会根据校验规则自动生成,可选:'success' 'warning' 'error' 'validating' | string | |
| validateTrigger | 设置字段校验的时机 | string \| string[] | onChange |
| valuePropName | 子节点的值的属性,如 Switch 的是 'checked' | string | value |
| wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 `labelCol`。你可以通过 Form 的 `wrapperCol` 进行统一设置。当和 Form 同时设置时,以 Item 为准。 | [object](/components/grid/#Col) | |
### dependencies
当字段间存在依赖关系时使用。如果一个字段设置了 `dependencies` 属性。那么它所依赖的字段更新时,该字段将自动触发更新与校验。一种常见的场景,就是注册用户表单的“密码”与“确认密码”字段。“确认密码”校验依赖于“密码”字段,设置 `dependencies` 后,“密码”字段更新会重新触发“校验密码”的校验逻辑。你可以参考[具体例子](#components-form-demo-register)。
### shouldUpdate
Form 通过增量更新方式,只更新被修改的字段相关组件以达到性能优化目的。大部分场景下,你只需要编写代码或者与 [`dependencies`](#dependencies) 属性配合校验即可。而在某些特定场景,例如修改某个字段值后出现新的字段选项、或者纯粹希望表单任意变化都对某一个区域进行渲染。你可以通过 `shouldUpdate` 修改 Form.Item 的更新逻辑。
`shouldUpdate``true` 时,Form 的任意变化都会使该 Form.Item 重新渲染。这对于自定义渲染一些区域十分有帮助:
- `values`:
```jsx
<Form.Item shouldUpdate>
{() => {
return <pre>{JSON.stringify(form.getFieldsValue(), null, 2)}</pre>;
}}
</Form.Item>
```
```js
{
"username": "username",
"password": "password",
}
```
你可以参考[示例](#components-form-demo-horizontal-login)查看具体使用场景。
### Form.createFormField
`shouldUpdate` 为方法时,表单的每次数值更新都会调用该方法,提供原先的值与当前的值以供你比较是否需要更新。这对于是否根据值来渲染额外字段十分有帮助:
用于标记 `mapPropsToFields` 返回的表单域数据,[例子](#components-form-demo-global-state)。
```jsx
<Form.Item shouldUpdate={(prevValues, curValues) => prevValues.additional !== curValues.additional}>
{() => {
return (
<Form.Item name="other">
<Input />
</Form.Item>
);
}}
</Form.Item>
```
### this.props.form.getFieldDecorator(id, options)
你可以参考[示例](#components-form-demo-control-hooks)查看具体使用场景。
经过 `getFieldDecorator` 包装的控件,表单控件会自动添加 `value`(或 `valuePropName` 指定的其他属性) `onChange`(或 `trigger` 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:
## Form.List
1. 你**不再需要也不应该**用 `onChange` 来做同步,但还是可以继续监听 `onChange` 等事件。
2. 你不能用控件的 `value` `defaultValue` 等属性来设置表单域的值,默认值可以用 `getFieldDecorator` 里的 `initialValue`
3. 你不应该用 `setState`,可以使用 `this.props.form.setFieldsValue` 来动态改变表单值。
为字段提供数组化管理。
#### 特别注意
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| name | 字段名,支持数组 | [NamePath](#NamePath) | - |
| children | 渲染函数 | (fields: Field[], operation: { add, remove }) => React.ReactNode | - |
如果使用的是 `react@<15.3.0`,则 `getFieldDecorator` 调用不能位于纯函数组件中: <https://github.com/facebook/react/pull/6534>
```tsx
<Form.List>
{fields => (
<div>
{fields.map(field => (
<Form.Item {...field}>
<Input />
</Form.Item>
))}
</div>
)}
</Form.List>
```
#### getFieldDecorator(id, options) 参数
## Form.Provider
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| 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) | | |
### 校验规则
提供表单间联动功能,其下设置 `name` 的 Form 更新时,会自动触发对应事件。查看[示例](#components-form-demo-form-context)。
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| 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 中使用
| onFormChange | 子表单字段更新时触发 | Function(formName: string, info: { changedFields, forms }) | - |
| onFormFinish | 子表单提交时触发 | Function(formName: string, info: { values, forms }) | - |
```tsx
import { Form } from 'antd';
import { FormComponentProps } from 'antd/lib/form';
```jsx
<Form.Provider
onFormFinish={name => {
if (name === 'form1') {
// Do something...
}
}}
>
<Form name="form1">...</Form>
<Form name="form2">...</Form>
</Form.Provider>
```
interface UserFormProps extends FormComponentProps {
age: number;
name: string;
}
### FormInstance
class UserForm extends React.Component<UserFormProps, any> {
// ...
}
| 名称 | 说明 | 类型 |
| --- | --- | --- |
| getFieldValue | 获取对应字段名的值 | (name: [NamePath](#NamePath)) => any |
| getFieldsValue | 获取一组字段名对应的值,会按照对应结构返回 | (nameList?: [NamePath](#NamePath)[]) => any |
| getFieldError | 获取对应字段名的错误信息 | (name: [NamePath](#NamePath)) => string[] |
| getFieldsError | 获取一组字段名对应的错误信息,返回为数组形式 | (nameList?: [NamePath](#NamePath)[]) => FieldError[] |
| isFieldTouched | 检查对应字段是否被用户操作过 | (name: [NamePath](#NamePath)) => boolean |
| isFieldsTouched | 检查一组字段是否被用户操作过,`allTouched` 为 `true` 时检查是否所有字段都被操作过 | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean |
| isFieldValidating | 检查一组字段是否正在校验 | (name: [NamePath](#NamePath)) => boolean |
| resetFields | 重置一组字段到 `initialValues` | (fields?: [NamePath](#NamePath)[]) => void |
| setFields | 设置一组字段状态 | (fields: FieldData[]) => void |
| setFieldsValue | 设置表单的值 | (values) => void |
| submit | 提交表单,与点击 `submit` 按钮效果相同 | () => void |
| validateFields | 触发表单验证 | (nameList?: [NamePath](#NamePath)[]) => Promise |
#### validateFields 返回示例
const App = Form.create<UserFormProps>({
// ...
})(UserForm);
```jsx
validateFields()
.then(values => {
/*
values:
{
username: 'username',
password: 'password',
}
*/
})
.catch(errorInfo => {
/*
errorInfo:
{
values: {
username: 'username',
password: 'password',
},
errorFields: [
{ password: ['username'], errors: ['Please input your Password!'] },
],
outOfDate: false,
}
*/
});
```
### Interface
#### NamePath
`string | number | (string | number)[]`
#### FieldData
| 名称 | 说明 | 类型 |
| ---------- | ---------------- | ----------------------- |
| touched | 是否被用户操作过 | boolean |
| validating | 是否正在校验 | boolean |
| errors | 错误信息 | string[] |
| name | 字段名称 | [NamePath](#NamePath)[] |
| value | 字段对应值 | any |
#### Rule
| 名称 | 说明 | 类型 |
| --- | --- | --- |
| enum | 是否匹配枚举中的值 | any[] |
| len | string 类型时为字符串长度;number 类型时为确定数字; array 类型时为数组长度 | number |
| max | string 类型为字符串最大长度;number 类型时为最大值;array 类型时为数组最大长度 | number |
| message | 错误信息,不设置时会通过[模板](#validateMessages)自动生成 | string |
| min | string 类型为字符串最小长度;number 类型时为最小值;array 类型时为数组最小长度 | number |
| pattern | 正则表达式匹配 | RegExp |
| required | 是否为必选字段 | boolean |
| transform | 将字段值转换成目标值后进行校验 | (value) => any |
| type | 类型,常见有 `string` \|`number` \|`boolean` \|`url` \| `email`。更多请参考[此处](https://github.com/yiminghe/async-validator#type) | string |
| validator | 自定义校验,接收 Promise 作为返回值。[示例](#components-form-demo-register)参考 | ([rule](#Rule), value) => Promise |
| whitespace | 如果字段仅包含空格则校验不通过 | boolean |
| validateTrigger | 设置触发验证时机,必须是 Form.Item 的 `validateTrigger` 的子集 | string \| string[] |
## 从 v3 升级到 v4
如果你是 antd v3 的用户,你可以参考[迁移示例](/components/form/v3)。
<style>
.code-box-demo .ant-form:not(.ant-form-inline):not(.ant-form-vertical) {
max-width: 600px;

1
components/form/interface.ts

@ -0,0 +1 @@
export type FormLabelAlign = 'left' | 'right';

76
components/form/style/components.less

@ -0,0 +1,76 @@
@import './index';
// ================================================================
// = Children Component =
// ================================================================
.@{form-item-prefix-cls} {
.@{ant-prefix}-mentions,
textarea.@{ant-prefix}-input {
height: auto;
margin-bottom: 4px;
}
// input[type=file]
.@{ant-prefix}-upload {
background: transparent;
}
input[type='radio'],
input[type='checkbox'] {
width: 14px;
height: 14px;
}
// Radios and checkboxes on same line
.@{ant-prefix}-radio-inline,
.@{ant-prefix}-checkbox-inline {
display: inline-block;
margin-left: 8px;
font-weight: normal;
vertical-align: middle;
cursor: pointer;
&:first-child {
margin-left: 0;
}
}
.@{ant-prefix}-checkbox-vertical,
.@{ant-prefix}-radio-vertical {
display: block;
}
.@{ant-prefix}-checkbox-vertical + .@{ant-prefix}-checkbox-vertical,
.@{ant-prefix}-radio-vertical + .@{ant-prefix}-radio-vertical {
margin-left: 0;
}
.@{ant-prefix}-input-number {
+ .@{form-prefix-cls}-text {
margin-left: 8px;
}
&-handler-wrap {
z-index: 2; // https://github.com/ant-design/ant-design/issues/6289
}
}
.@{ant-prefix}-select,
.@{ant-prefix}-cascader-picker {
width: 100%;
}
// Don't impact select inside input group
.@{ant-prefix}-input-group .@{ant-prefix}-select,
.@{ant-prefix}-input-group .@{ant-prefix}-cascader-picker {
width: auto;
}
// fix input with addon position. https://github.com/ant-design/ant-design/issues/8243
:not(.@{ant-prefix}-input-group-wrapper) > .@{ant-prefix}-input-group,
.@{ant-prefix}-input-group-wrapper {
position: relative;
top: -1px;
display: inline-block;
vertical-align: middle;
}
}

1
components/form/style/horizontal.less

@ -0,0 +1 @@
@import './index';

446
components/form/style/index.less

@ -3,6 +3,452 @@
@import '../../input/style/mixin';
@import '../../button/style/mixin';
@import '../../grid/style/mixin';
@import './components';
@import './inline';
@import './horizontal';
@import './vertical';
@import './mixin';
@form-prefix-cls: ~'@{ant-prefix}-form';
@form-item-prefix-cls: ~'@{form-prefix-cls}-item';
@form-font-height: ceil(@font-size-base * @line-height-base);
@form-label-margin-top: floor((@input-height-base - @form-font-height) / 2);
.@{form-prefix-cls} {
.reset-component;
.reset-form;
.@{form-prefix-cls}-text {
display: inline-block;
margin-top: @form-label-margin-top;
padding-right: 8px;
}
}
// ================================================================
// = Item =
// ================================================================
.@{form-item-prefix-cls} {
.reset-component;
margin-bottom: @form-item-margin-bottom;
vertical-align: top;
&-with-help {
margin-bottom: 0;
}
// ==============================================================
// = Label =
// ==============================================================
&-label {
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-align: right;
vertical-align: middle;
&-left {
text-align: left;
}
> label {
position: relative;
display: inline-block;
margin-top: @form-label-margin-top;
color: @label-color;
> .@{iconfont-css-prefix} {
font-size: @font-size-base;
vertical-align: top;
}
&.@{form-item-prefix-cls}-required::before {
display: inline-block;
margin-right: 4px;
color: @label-required-color;
font-size: @font-size-base;
font-family: SimSun, sans-serif;
line-height: 1;
content: '*';
.@{form-prefix-cls}-hide-required-mark & {
display: none;
}
}
&::after {
& when (@form-item-trailing-colon=true) {
content: ':';
}
& when not (@form-item-trailing-colon=true) {
content: ' ';
}
position: relative;
top: -0.5px;
margin: 0 8px 0 2px;
}
&.@{form-item-prefix-cls}-no-colon::after {
content: ' ';
}
}
}
// ==============================================================
// = Input =
// ==============================================================
&-control {
display: flex;
flex-direction: column;
&:first-child:not([class^=~"'@{ant-prefix}-col-'"]):not([class*=~"' @{ant-prefix}-col-'"]) {
width: 100%;
}
}
&-control-input {
position: relative;
.clearfix;
}
&-explain,
&-extra {
clear: both;
min-height: @form-item-margin-bottom;
padding-top: floor((@form-item-margin-bottom - @form-font-height) / 2);
color: @text-color-secondary;
font-size: @font-size-base;
line-height: @line-height-base;
transition: color 0.3s @ease-out; // sync input color transition
}
// ================================================================
// = Status =
// ================================================================
/* Some non-status related component style is in `components.less` */
&-has-feedback {
// ========================= Input =========================
.@{ant-prefix}-input {
padding-right: 24px;
}
// Fix issue: https://github.com/ant-design/ant-design/issues/7854
.@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) {
.@{ant-prefix}-input-suffix {
right: 28px;
}
}
.@{ant-prefix}-input-password-icon {
margin-right: 18px;
}
// ======================== Switch =========================
.@{ant-prefix}-switch {
margin: 2px 0 4px;
}
// ======================== Select =========================
// Fix overlapping between feedback icon and <Select>'s arrow.
// https://github.com/ant-design/ant-design/issues/4431
> .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
> .@{ant-prefix}-select .@{ant-prefix}-select-selection__clear,
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
:not(.@{ant-prefix}-input-group-addon)
> .@{ant-prefix}-select
.@{ant-prefix}-select-selection__clear {
right: 28px;
}
> .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value,
:not(.@{ant-prefix}-input-group-addon)
> .@{ant-prefix}-select
.@{ant-prefix}-select-selection-selected-value {
padding-right: 42px;
}
// ======================= Cascader ========================
.@{ant-prefix}-cascader-picker {
&-arrow {
margin-right: 17px;
}
&-clear {
right: 28px;
}
}
// ======================== Picker =========================
// Fix issue: https://github.com/ant-design/ant-design/issues/4783
.@{ant-prefix}-calendar-picker,
.@{ant-prefix}-time-picker {
&-icon,
&-clear {
right: 28px;
}
}
// ===================== Status Group ======================
&.@{form-item-prefix-cls} {
&-has-success,
&-has-warning,
&-has-error,
&-is-validating {
// ====================== Icon ======================
.@{form-item-prefix-cls}-children-icon {
position: absolute;
top: 50%;
right: 0;
z-index: 1;
width: @input-height-base;
height: 20px;
margin-top: -10px;
font-size: @font-size-base;
line-height: 20px;
text-align: center;
visibility: visible;
animation: zoomIn 0.3s @ease-out-back;
pointer-events: none;
& svg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
}
}
}
}
// ======================== Success ========================
&-has-success {
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
color: @success-color;
animation-name: diffZoomIn1 !important;
}
}
// ======================== Warning ========================
&-has-warning {
.form-control-validation(@warning-color; @warning-color; @form-warning-input-bg);
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
color: @warning-color;
animation-name: diffZoomIn3 !important;
}
//select
.@{ant-prefix}-select {
&-selection {
border-color: @warning-color;
&:hover {
border-color: @warning-color;
}
}
&-open .@{ant-prefix}-select-selection,
&-focused .@{ant-prefix}-select-selection {
.active(@warning-color);
}
}
// arrow and icon
.@{ant-prefix}-calendar-picker-icon::after,
.@{ant-prefix}-time-picker-icon::after,
.@{ant-prefix}-picker-icon::after,
.@{ant-prefix}-select-arrow,
.@{ant-prefix}-cascader-picker-arrow {
color: @warning-color;
}
//input-number, timepicker
.@{ant-prefix}-input-number,
.@{ant-prefix}-time-picker-input {
border-color: @warning-color;
&-focused,
&:focus {
.active(@warning-color);
}
&:not([disabled]):hover {
border-color: @warning-color;
}
}
.@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input {
.active(@warning-color);
}
}
// ========================= Error =========================
&-has-error {
.form-control-validation(@error-color; @error-color; @form-error-input-bg);
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
color: @error-color;
animation-name: diffZoomIn2 !important;
}
//select
.@{ant-prefix}-select {
&-selection {
border-color: @error-color;
&:hover {
border-color: @error-color;
}
}
&-open .@{ant-prefix}-select-selection,
&-focused .@{ant-prefix}-select-selection {
.active(@error-color);
}
}
.@{ant-prefix}-select.@{ant-prefix}-select-auto-complete {
.@{ant-prefix}-input:focus {
border-color: @error-color;
}
}
.@{ant-prefix}-input-group-addon .@{ant-prefix}-select {
&-selection {
border-color: transparent;
box-shadow: none;
}
}
// arrow and icon
.@{ant-prefix}-calendar-picker-icon::after,
.@{ant-prefix}-time-picker-icon::after,
.@{ant-prefix}-picker-icon::after,
.@{ant-prefix}-select-arrow,
.@{ant-prefix}-cascader-picker-arrow {
color: @error-color;
}
//input-number, timepicker
.@{ant-prefix}-input-number,
.@{ant-prefix}-time-picker-input {
border-color: @error-color;
&-focused,
&:focus {
.active(@error-color);
}
&:not([disabled]):hover {
border-color: @error-color;
}
}
.@{ant-prefix}-mention-wrapper {
.@{ant-prefix}-mention-editor {
&,
&:not([disabled]):hover {
border-color: @error-color;
}
}
&.@{ant-prefix}-mention-active:not([disabled]) .@{ant-prefix}-mention-editor,
.@{ant-prefix}-mention-editor:not([disabled]):focus {
.active(@error-color);
}
}
.@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input {
.active(@error-color);
}
// transfer
.@{ant-prefix}-transfer {
&-list {
border-color: @error-color;
&-search:not([disabled]) {
border-color: @input-border-color;
&:hover {
.hover();
}
&:focus {
.active();
}
}
}
}
}
// Patch to keep error explain color
&-has-error-leave {
.@{form-item-prefix-cls}-explain {
color: @error-color;
}
}
// ====================== Validating =======================
&-is-validating {
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
display: inline-block;
color: @primary-color;
}
}
}
.show-help-motion(@className, @keyframeName, @duration: @animation-duration-slow) {
.make-motion(@className, @keyframeName, @duration);
.@{className}-enter,
.@{className}-appear {
opacity: 0;
animation-timing-function: @ease-in-out;
}
.@{className}-leave {
animation-timing-function: @ease-in-out;
}
}
.show-help-motion(show-help, antShowHelp, 0.3s);
@keyframes antShowHelpIn {
0% {
transform: translateY(-5px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes antShowHelpOut {
to {
transform: translateY(-5px);
opacity: 0;
}
}
// need there different zoom animation
// otherwise won't trigger anim
@keyframes diffZoomIn1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes diffZoomIn2 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes diffZoomIn3 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}

31
components/form/style/inline.less

@ -0,0 +1,31 @@
@import './index';
.@{form-prefix-cls}-inline {
display: flex;
flex-wrap: wrap;
.@{form-prefix-cls}-item {
margin-right: 16px;
margin-bottom: 0;
flex-wrap: nowrap;
flex: none;
&-with-help {
margin-bottom: @form-item-margin-bottom;
}
> .@{form-item-prefix-cls}-label,
> .@{form-item-prefix-cls}-control {
display: inline-block;
vertical-align: top;
}
.@{form-prefix-cls}-text {
display: inline-block;
}
.@{form-item-prefix-cls}-has-feedback {
display: inline-block;
}
}
}

124
components/form/style/mixin.less

@ -1 +1,125 @@
@import '../../input/style/mixin';
.form-control-validation(@text-color: @input-color; @border-color: @input-border-color; @background-color: @input-bg) {
.@{ant-prefix}-form-item-explain,
.@{ant-prefix}-form-item-split {
color: @text-color;
}
// 输入框的不同校验状态
.@{ant-prefix}-input {
&,
&:hover {
border-color: @border-color;
}
&:focus {
.active(@border-color);
}
&:not([disabled]):hover {
border-color: @border-color;
}
}
.@{ant-prefix}-calendar-picker-open .@{ant-prefix}-calendar-picker-input {
.active(@border-color);
}
// Input prefix
.@{ant-prefix}-input-affix-wrapper {
.@{ant-prefix}-input {
&,
&:hover {
background-color: @background-color;
border-color: @border-color;
}
&:focus {
.active(@border-color);
}
}
&:hover .@{ant-prefix}-input:not(.@{ant-prefix}-input-disabled) {
border-color: @border-color;
}
}
.@{ant-prefix}-input-prefix {
color: @text-color;
}
.@{ant-prefix}-input-group-addon {
color: @text-color;
background-color: @background-color;
border-color: @border-color;
}
.has-feedback {
color: @text-color;
}
}
// Reset form styles
// -----------------------------
// Based on Bootstrap framework
.reset-form() {
legend {
display: block;
width: 100%;
margin-bottom: 20px;
padding: 0;
color: @text-color-secondary;
font-size: @font-size-lg;
line-height: inherit;
border: 0;
border-bottom: @border-width-base @border-style-base @border-color-base;
}
label {
font-size: @font-size-base;
}
input[type='search'] {
box-sizing: border-box;
}
// Position radios and checkboxes better
input[type='radio'],
input[type='checkbox'] {
line-height: normal;
}
input[type='file'] {
display: block;
}
// Make range inputs behave like textual form controls
input[type='range'] {
display: block;
width: 100%;
}
// Make multiple select elements height not fixed
select[multiple],
select[size] {
height: auto;
}
// Focus for file, radio, and checkbox
input[type='file']:focus,
input[type='radio']:focus,
input[type='checkbox']:focus {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
// Adjust output element
output {
display: block;
padding-top: 15px;
color: @input-color;
font-size: @font-size-base;
line-height: @line-height-base;
}
}

61
components/form/style/vertical.less

@ -0,0 +1,61 @@
@import './index';
// ================== Label ==================
.make-vertical-layout-label() {
margin: @form-vertical-label-margin;
padding: @form-vertical-label-padding;
line-height: @line-height-base;
white-space: initial;
text-align: left;
> label {
margin: 0;
&::after {
display: none;
}
}
}
.@{form-prefix-cls}-vertical {
.@{form-item-prefix-cls} {
flex-direction: column;
}
}
.@{form-prefix-cls}-vertical .@{form-item-prefix-cls}-label,
// when labelCol is 24, it is a vertical form
.@{ant-prefix}-col-24.@{form-item-prefix-cls}-label,
.@{ant-prefix}-col-xl-24.@{form-item-prefix-cls}-label {
.make-vertical-layout-label();
}
@media (max-width: @screen-xs-max) {
.@{ant-prefix}-col-xs-24.@{form-item-prefix-cls}-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-sm-max) {
.@{ant-prefix}-col-sm-24.@{form-item-prefix-cls}-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-md-max) {
.@{ant-prefix}-col-md-24.@{form-item-prefix-cls}-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-lg-max) {
.@{ant-prefix}-col-lg-24.@{form-item-prefix-cls}-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-xl-max) {
.@{ant-prefix}-col-xl-24.@{form-item-prefix-cls}-label {
.make-vertical-layout-label();
}
}

60
components/form/util.ts

@ -0,0 +1,60 @@
import * as React from 'react';
/**
* We will remove light way shake like:
* errors -> none -> errors (in 100 ms)
* to
* errors (in 100 ms)
*/
function useDebounce<T>(content: T[], delay: number = 10): T[] {
const [cache, setCache] = React.useState(content);
// React hooks still have bug with unmount setState
// https://github.com/facebook/react/issues/15057
React.useEffect(() => {
if (content.length) {
setCache(content);
} else {
const timeout = setTimeout(() => {
setCache(content);
}, delay);
return () => {
clearTimeout(timeout);
};
}
}, [content]);
return cache;
}
/**
* Cache latest errors and trigger change event if visible change
*/
export function useCacheErrors(
errors: React.ReactNode[],
changeTrigger: (visible: boolean) => void,
) {
const debounceErrors = useDebounce(errors);
const [cacheErrors, setCacheErrors] = React.useState(debounceErrors);
const [visible, setVisible] = React.useState(false);
React.useEffect(() => {
const newVisible = !!debounceErrors.length;
if (debounceErrors.length) {
setCacheErrors(debounceErrors);
}
if (newVisible !== visible) {
changeTrigger(newVisible);
}
setVisible(newVisible);
}, [debounceErrors]);
return [visible, cacheErrors];
}
export function toArray<T>(candidate?: T | T[] | false): T[] {
if (candidate === undefined || candidate === false) return [];
return Array.isArray(candidate) ? candidate : [candidate];
}

270
components/form/v3.en-US.md

@ -0,0 +1,270 @@
---
title: Migrate to v4
skip: true
---
### Remove Form.create
Form of v4 is not need create context by `Form.create()`. Form is now have own data scope and you don't need `getFieldDecorator` anymore. Just use Form.Item directly:
```jsx
// antd v3
const Demo = ({ form: { getFieldDecorator } }) => (
<Form>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
})(<Input />)}
</Form.Item>
</Form>
);
const WrappedDemo = Form.create()(Demo);
```
To:
```jsx
// antd v4
const Demo = () => (
<Form>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
```
Since `Form.create()` is removed, origin `onFieldsChange` or other methods moved to Form, and control by `fields`. ref [example](/components/form/#components-form-demo-global-state)。
### Form control
If you want to control form, you can use `Form.useForm()` to create Form instance for operation:
```jsx
// antd v3
const Demo = ({ form: { setFieldsValue } }) => {
React.useEffect(() => {
setFieldsValue({
username: 'Bamboo',
});
}, []);
return (
<Form>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
})(<Input />)}
</Form.Item>
</Form>
);
};
const WrappedDemo = Form.create()(Demo);
```
To:
```jsx
// antd v4
const Demo = () => {
const [form] = Form.useForm();
React.useEffect(() => {
form.setFieldsValue({
username: 'Bamboo',
});
}, []);
return (
<Form form={form}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
};
```
For class component, you can use `ref` to access instance:
```jsx
// antd v4
class Demo extends React.Component {
formRef = React.createRef();
componentDidMount() {
this.formRef.setFieldsValue({
username: 'Bamboo',
});
}
render() {
return (
<Form ref={formRef}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
}
}
```
If you don't want to use the Item style, you can use `inline` prop to remove it:
```jsx
// antd v3
const Demo = ({ form: { setFieldsValue } }) => {
return <Form>{getFieldDecorator('username')(<Input />)}</Form>;
};
const WrappedDemo = Form.create()(Demo);
```
To:
```jsx
// antd v4
const Demo = () => {
return (
<Form>
<Form.Item name="username" inline>
<Input />
</Form.Item>
</Form>
);
};
```
### Linkage with field
New Form use incremental update which only update related field. So if there are some linkage between fields or update with whole form. You can use [`dependencies`](/components/form/#dependencies) or [`shouldUpdate`](/components/form/#shouldUpdate) to handle that.
### replace onSubmit with onFinish
You need to listen `onSubmit` and call `validateFields` to handle validation in old Form. New Form provides `onFinish` which only trigger when validation is passed:
```jsx
// antd v3
const Demo = ({ form: { getFieldDecorator, validateFields } }) => {
const onSubmit = e => {
e.preventDefault();
validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};
return (
<Form onSubmit={onSubmit}>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
})(<Input />)}
</Form.Item>
</Form>
);
};
const WrappedDemo = Form.create()(Demo);
```
To:
```jsx
// antd v4
const Demo = () => {
const onFinish = values => {
console.log('Received values of form: ', values);
};
return (
<Form onFinish={onFinish}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
};
```
### Initialization
Besides, we move `initialValue` into Form to avoid field with same name both using `initialValue` to cause conflict:
```jsx
// antd v3
const Demo = ({ form: { getFieldDecorator } }) => (
<Form>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
initialValue: 'Bamboo',
})(<Input />)}
</Form.Item>
</Form>
);
const WrappedDemo = Form.create()(Demo);
```
To:
```jsx
// antd v4
const Demo = () => (
<Form initialValues={{ username: 'Bamboo' }}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
```
In v3, modifying the `initialValue` of un-operated field will update the field value synchronously, which is a bug. However, since it has been used as a feature for a long time, we have not fixed it. In v4, the bug has been fixed. `initialValues` only takes effect when initializing and resetting the form.
### Remove field no longer removes field value
In v3, we will clean up the values when the fields removed. However, it has been found that clearing fields can cause greater inconvenience than retention in practice. For example like some switching operations, the user has to retain the field value in a hidden way. Therefore, the new version of Form will always keep the fields value, and the validation rules for removing field will be ignored. So `preserve` is no longer needed.
### Nested field paths using arrays
In the past versions we used `.` to represent nested paths (such as `user.name` to represent `{ user: { name: '' } }`). However, in some backend systems, `.` is also included in the variable name. This causes the user to need additional code to convert, so in the new version, nested paths are represented by arrays to avoid unexpected behavior (eg `['user', 'name']`).
Therefore, paths returned by methods such as `getFieldsError` are always in an array form for the user to handle:
```jsx
form.getFieldsError();
/*
[
{ name: ['user', 'name'], errors: [] },
{ name: ['user', 'age'], errors: ['Some error message'] },
]
*/
```
### Remove callback in validateFields
`validateFields` will return a Promise, so you can handle the error with `async/await` or `then/catch`. It is no longer necessary to determine if `errors` is empty:
```jsx
// antd v3
validateFields((err, value) => {
if (!err) {
// Do something with value
}
});
```
To
```jsx
// antd v4
validateFields().then(values => {
// Do something with value
});
```

272
components/form/v3.zh-CN.md

@ -0,0 +1,272 @@
---
title: 从 v3 到 v4
skip: true
---
新版本 Form 对使用方式进行了简化,因而如果你是从 v3 迁移上来。你可以参考本文做迁移工作。
## 去除 Form.create
v4 的 Form 不再需要通过 `Form.create()` 创建上下文。Form 组件现在自带数据域,因而 `getFieldDecorator` 也不再需要,直接写入 Form.Item 即可:
```jsx
// antd v3
const Demo = ({ form: { getFieldDecorator } }) => (
<Form>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
})(<Input />)}
</Form.Item>
</Form>
);
const WrappedDemo = Form.create()(Demo);
```
改成:
```jsx
// antd v4
const Demo = () => (
<Form>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
```
由于移除了 `Form.create()`,原本的 `onFieldsChange` 等方法移入 Form 中,通过 `fields` 对 Form 进行控制。参考[示例](/components/form/#components-form-demo-global-state)。
## 表单控制调整
Form 自带表单控制实体,如需要调用 form 方法,可以通过 `Form.useForm()` 创建 Form 实体进行操作:
```jsx
// antd v3
const Demo = ({ form: { setFieldsValue } }) => {
React.useEffect(() => {
setFieldsValue({
username: 'Bamboo',
});
}, []);
return (
<Form>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
})(<Input />)}
</Form.Item>
</Form>
);
};
const WrappedDemo = Form.create()(Demo);
```
改成:
```jsx
// antd v4
const Demo = () => {
const [form] = Form.useForm();
React.useEffect(() => {
form.setFieldsValue({
username: 'Bamboo',
});
}, []);
return (
<Form form={form}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
};
```
对于 class component,也可以通过 `ref` 获得实体:
```jsx
// antd v4
class Demo extends React.Component {
formRef = React.createRef();
componentDidMount() {
this.formRef.setFieldsValue({
username: 'Bamboo',
});
}
render() {
return (
<Form ref={formRef}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
}
}
```
由于 Form.Item 内置字段绑定,如果需要不带样式的表单绑定,可以使用 `inline` 属性移除额外样式:
```jsx
// antd v3
const Demo = ({ form: { setFieldsValue } }) => {
return <Form>{getFieldDecorator('username')(<Input />)}</Form>;
};
const WrappedDemo = Form.create()(Demo);
```
改成:
```jsx
// antd v4
const Demo = () => {
return (
<Form>
<Form.Item name="username" inline>
<Input />
</Form.Item>
</Form>
);
};
```
## 字段联动调整
新版 Form 采用增量更新方式,仅会更新需要更新的字段。因而如果有字段关联更新,或者跟随整个表单更新而更新。可以使用 [`dependencies`](/components/form/#dependencies) 或 [`shouldUpdate`](/components/form/#shouldUpdate)。
## onFinish 替代 onSubmit
对于表单校验,过去版本需要通过监听 `onSubmit` 事件手工触发 `validateFields`。新版直接使用 `onFinish` 事件,该事件仅当校验通过后才会执行:
```jsx
// antd v3
const Demo = ({ form: { getFieldDecorator, validateFields } }) => {
const onSubmit = e => {
e.preventDefault();
validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};
return (
<Form onSubmit={onSubmit}>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
})(<Input />)}
</Form.Item>
</Form>
);
};
const WrappedDemo = Form.create()(Demo);
```
改成:
```jsx
// antd v4
const Demo = () => {
const onFinish = values => {
console.log('Received values of form: ', values);
};
return (
<Form onFinish={onFinish}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
};
```
## 初始化调整
此外,我们将 `initialValue` 从字段中移到 Form 中。以避免同名字段设置 `initialValue` 的冲突问题:
```jsx
// antd v3
const Demo = ({ form: { getFieldDecorator } }) => (
<Form>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
initialValue: 'Bamboo',
})(<Input />)}
</Form.Item>
</Form>
);
const WrappedDemo = Form.create()(Demo);
```
改成:
```jsx
// antd v4
const Demo = () => (
<Form initialValues={{ username: 'Bamboo' }}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
```
在 v3 版本中,修改未操作的字段 `initialValue` 会同步更新字段值,这是一个 BUG。但是由于被长期作为一个 feature 使用,因而我们一直没有修复。在 v4 中,该 BUG 已被修复。`initialValue` 只有在初始化以及重置表单时生效。
## 移除字段不再移除字段值
v3 版本中,我们会将移除的字段对应的值同步清理。但是在实践中,发现清除字段相比保留会造成更大的不便。例如一些切换操作,用户不得不通过隐藏的方式保留字段值。因而新版本的 Form 总是会保留字段值,而移除字段的校验规则会被忽略。也因此 `preserve` 不再需要。
## 嵌套字段路径使用数组
过去版本我们通过 `.` 代表嵌套路径(诸如 `user.name` 来代表 `{ user: { name: '' } }`)。然而在一些后台系统中,变量名中也会带上 `.`。这造成用户需要额外的代码进行转化,因而新版中,嵌套路径通过数组来表示以避免错误的处理行为(如 `['user', 'name']`)。
也因此,诸如 `getFieldsError` 等方法返回的路径总是数组形式以便于用户处理:
```jsx
form.getFieldsError();
/*
[
{ name: ['user', 'name'], errors: [] },
{ name: ['user', 'age'], errors: ['Some error message'] },
]
*/
```
## validateFields 不再支持 callback
`validateFields` 会返回 Promise 对象,因而你可以通过 `async/await` 或者 `then/catch` 来执行对应的错误处理。不再需要判断 `errors` 是否为空:
```jsx
// antd v3
validateFields((err, value) => {
if (!err) {
// Do something with value
}
});
```
改成:
```jsx
// antd v4
validateFields().then(values => {
// Do something with value
});
```

39
components/input/__tests__/index.test.js

@ -175,29 +175,28 @@ describe('TextArea', () => {
describe('As Form Control', () => {
it('should be reset when wrapped in form.getFieldDecorator without initialValue', () => {
class Demo extends React.Component {
reset = () => {
const { form } = this.props;
const Demo = () => {
const [form] = Form.useForm();
const reset = () => {
form.resetFields();
};
render() {
const {
form: { getFieldDecorator },
} = this.props;
return (
<Form>
<Form.Item>{getFieldDecorator('input')(<Input />)}</Form.Item>
<Form.Item>{getFieldDecorator('textarea')(<Input.TextArea />)}</Form.Item>
<button type="button" onClick={this.reset}>
reset
</button>
</Form>
);
}
}
const DemoForm = Form.create()(Demo);
const wrapper = mount(<DemoForm />);
return (
<Form form={form}>
<Form.Item name="input">
<Input />
</Form.Item>
<Form.Item name="textarea">
<Input.TextArea />
</Form.Item>
<button type="button" onClick={reset}>
reset
</button>
</Form>
);
};
const wrapper = mount(<Demo />);
wrapper.find('input').simulate('change', { target: { value: '111' } });
wrapper.find('textarea').simulate('change', { target: { value: '222' } });
expect(wrapper.find('input').prop('value')).toBe('111');

111
components/mention/__tests__/__snapshots__/demo.test.js.snap

@ -254,79 +254,74 @@ exports[`renders ./components/mention/demo/controlled.md correctly 1`] = `
class="ant-form ant-form-horizontal"
>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-col-6 ant-form-item-label"
>
<label
class=""
for="control-mention"
title="Top coders"
>
Top coders
</label>
</div>
<div
class="ant-col ant-col-16 ant-form-item-control-wrapper"
class="ant-col ant-col-16 ant-form-item-control"
>
<div
class="ant-form-item-control has-success"
class="ant-form-item-control-input"
>
<span
class="ant-form-item-children"
<div
class="ant-mention-wrapper"
>
<div
class="ant-mention-wrapper"
class="ant-mention-editor oneline"
>
<div
class="ant-mention-editor oneline"
class="ant-mention-toolbar"
/>
<div
class="ant-mention-editor-wrapper"
>
<div
class="ant-mention-toolbar"
/>
<div
class="ant-mention-editor-wrapper"
class="DraftEditor-root"
>
<div
class="DraftEditor-root"
class="DraftEditor-editorContainer"
>
<div
class="DraftEditor-editorContainer"
aria-describedby="placeholder-123"
class="notranslate public-DraftEditor-content"
contenteditable="true"
role="textbox"
spellcheck="false"
style="outline:none;user-select:text;-webkit-user-select:text;white-space:pre-wrap;word-wrap:break-word"
>
<div
aria-describedby="placeholder-123"
class="notranslate public-DraftEditor-content"
contenteditable="true"
role="textbox"
spellcheck="false"
style="outline:none;user-select:text;-webkit-user-select:text;white-space:pre-wrap;word-wrap:break-word"
data-contents="true"
>
<div
data-contents="true"
class=""
data-block="true"
data-editor="123"
data-offset-key="123-0-0"
>
<div
class=""
data-block="true"
data-editor="123"
class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
data-offset-key="123-0-0"
>
<div
class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
data-offset-key="123-0-0"
>
<span>
<span>
<span
data-offset-key="123-0-0"
>
<span
data-offset-key="123-0-0"
data-text="true"
>
<span
data-text="true"
>
@afc163
</span>
@afc163
</span>
</span>
</div>
</span>
</div>
</div>
</div>
@ -335,40 +330,36 @@ exports[`renders ./components/mention/demo/controlled.md correctly 1`] = `
</div>
</div>
</div>
</span>
</div>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-col-14 ant-col-offset-6 ant-form-item-control-wrapper"
class="ant-col ant-col-14 ant-col-offset-6 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="button"
>
<span>
Submit
</span>
</button>
   
<button
class="ant-btn"
type="button"
>
<span>
Reset
</span>
</button>
</span>
<span>
Submit
</span>
</button>
   
<button
class="ant-btn"
type="button"
>
<span>
Reset
</span>
</button>
</div>
</div>
</div>

12
components/mention/__tests__/index.test.js

@ -75,11 +75,6 @@ describe('Mention', () => {
jest.runAllTimers();
expect(onChange).toHaveBeenCalled();
expect(onSelect).not.toHaveBeenCalled();
// enzyme cannot find .ant-mention-dropdown-item in react 15
// I don't know why
if (process.env.REACT === '15') {
return;
}
wrapper
.find('.ant-mention-dropdown-item')
.at(0)
@ -98,10 +93,6 @@ describe('Mention', () => {
});
it('defaultSuggestion filter', () => {
if (process.env.REACT === '15') {
return;
}
const wrapper = mount(<Mention defaultSuggestions={['light', 'bamboo']} />);
wrapper.find('DraftEditorContents').simulate('focus');
@ -115,9 +106,6 @@ describe('Mention', () => {
});
it('check filteredSuggestions', () => {
if (process.env.REACT === '15') {
return;
}
const wrapper = mount(<Mention defaultSuggestions={[<Mention.Nav value="light" />]} />);
wrapper.find('DraftEditorContents').simulate('focus');
const ed = wrapper.find('.public-DraftEditor-content');

102
components/mention/demo/controlled.md

@ -13,37 +13,17 @@ title:
Controlled mode, for example, to work with `Form`.
```jsx
```tsx
import { Mention, Form, Button } from 'antd';
const { toContentState, getMentions } = Mention;
const FormItem = Form.Item;
class App extends React.Component {
state = {
initValue: toContentState('@afc163'),
};
handleReset = e => {
e.preventDefault();
this.props.form.resetFields();
};
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((errors, values) => {
if (errors) {
console.log('Errors in the form!!!');
return;
}
console.log('Submit!!!');
console.log(values);
});
};
const App = () => {
const [form] = Form.useForm();
checkMention = (rule, value, callback) => {
const { getFieldValue } = this.props.form;
const mentions = getMentions(getFieldValue('mention'));
const checkMention = (rule, value, callback) => {
const mentions = getMentions(form.getFieldValue('mention'));
if (mentions.length < 2) {
callback(new Error('More than one must be selected!'));
} else {
@ -51,39 +31,45 @@ class App extends React.Component {
}
};
render() {
const { getFieldDecorator, getFieldValue } = this.props.form;
console.log('>> render', getFieldValue('mention') === this.state.initValue);
return (
<Form layout="horizontal">
<FormItem
id="control-mention"
label="Top coders"
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
>
{getFieldDecorator('mention', {
rules: [{ validator: this.checkMention }],
initialValue: this.state.initValue,
})(
<Mention
defaultSuggestions={['afc163', 'benjycui', 'yiminghe', 'RaoHai', '中文', 'にほんご']}
/>,
)}
</FormItem>
<FormItem wrapperCol={{ span: 14, offset: 6 }}>
<Button type="primary" onClick={this.handleSubmit}>
Submit
</Button>
&nbsp;&nbsp;&nbsp;
<Button onClick={this.handleReset}>Reset</Button>
</FormItem>
</Form>
);
}
}
const onReset = () => {
form.resetFields();
};
const onFinish = values => {
console.log('Submit:', values);
};
const FormDemo = Form.create()(App);
return (
<Form
form={form}
layout="horizontal"
initialValues={{ mention: toContentState('@afc163') }}
onFinish={onFinish}
>
<FormItem
name="mention"
id="control-mention"
label="Top coders"
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
rules={[{ validator: checkMention }]}
>
<Mention
defaultSuggestions={['afc163', 'benjycui', 'yiminghe', 'RaoHai', '中文', 'にほんご']}
/>
</FormItem>
<FormItem wrapperCol={{ span: 14, offset: 6 }}>
<Button htmlType="submit" type="primary">
Submit
</Button>
&nbsp;&nbsp;&nbsp;
<Button htmlType="button" onClick={onReset}>
Reset
</Button>
</FormItem>
</Form>
);
};
ReactDOM.render(<FormDemo />, mountNode);
ReactDOM.render(<App />, mountNode);
```

69
components/mentions/__tests__/__snapshots__/demo.test.js.snap

@ -29,72 +29,61 @@ exports[`renders ./components/mentions/demo/form.md correctly 1`] = `
class="ant-form ant-form-horizontal"
>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-col-6 ant-form-item-label"
>
<label
class=""
for="mention"
title="Top coders"
>
Top coders
</label>
</div>
<div
class="ant-col ant-col-16 ant-form-item-control-wrapper"
class="ant-col ant-col-16 ant-form-item-control"
>
<div
class="ant-form-item-control"
class="ant-form-item-control-input"
>
<span
class="ant-form-item-children"
<div
class="ant-mentions"
>
<div
class="ant-mentions"
>
<textarea
data-__field="[object Object]"
data-__meta="[object Object]"
id="mention"
rows="3"
/>
</div>
</span>
<textarea
id="mention"
rows="3"
/>
</div>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-col-14 ant-col-offset-6 ant-form-item-control-wrapper"
class="ant-col ant-col-14 ant-col-offset-6 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="button"
>
<span>
Submit
</span>
</button>
   
<button
class="ant-btn"
type="button"
>
<span>
Reset
</span>
</button>
</span>
<span>
Submit
</span>
</button>
   
<button
class="ant-btn"
type="button"
>
<span>
Reset
</span>
</button>
</div>
</div>
</div>

95
components/mentions/demo/form.md

@ -13,69 +13,62 @@ title:
Controlled mode, for example, to work with `Form`.
```jsx
```tsx
import { Mentions, Form, Button } from 'antd';
const { Option, getMentions } = Mentions;
class App extends React.Component {
handleReset = e => {
e.preventDefault();
this.props.form.resetFields();
const App = () => {
const [form] = Form.useForm();
const onReset = () => {
form.resetFields();
};
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((errors, values) => {
if (errors) {
console.log('Errors in the form!!!');
return;
}
console.log('Submit!!!');
console.log(values);
});
const onFinish = async () => {
try {
const values = await form.validateFields();
console.log('Submit:', values);
} catch (errInfo) {
console.log('Error:', errInfo);
}
};
checkMention = (rule, value, callback) => {
const checkMention = async (rule, value, callback) => {
const mentions = getMentions(value);
if (mentions.length < 2) {
callback(new Error('More than one must be selected!'));
} else {
callback();
throw new Error('More than one must be selected!');
}
};
render() {
const {
form: { getFieldDecorator },
} = this.props;
return (
<Form layout="horizontal">
<Form.Item label="Top coders" labelCol={{ span: 6 }} wrapperCol={{ span: 16 }}>
{getFieldDecorator('mention', {
rules: [{ validator: this.checkMention }],
})(
<Mentions rows="3">
<Option value="afc163">afc163</Option>
<Option value="zombieJ">zombieJ</Option>
<Option value="yesmeck">yesmeck</Option>
</Mentions>,
)}
</Form.Item>
<Form.Item wrapperCol={{ span: 14, offset: 6 }}>
<Button type="primary" onClick={this.handleSubmit}>
Submit
</Button>
&nbsp;&nbsp;&nbsp;
<Button onClick={this.handleReset}>Reset</Button>
</Form.Item>
</Form>
);
}
}
const FormDemo = Form.create()(App);
return (
<Form form={form} layout="horizontal" onFinish={onFinish}>
<Form.Item
name="mention"
label="Top coders"
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
rules={[{ validator: checkMention }]}
>
<Mentions rows="3">
<Option value="afc163">afc163</Option>
<Option value="zombieJ">zombieJ</Option>
<Option value="yesmeck">yesmeck</Option>
</Mentions>
</Form.Item>
<Form.Item wrapperCol={{ span: 14, offset: 6 }}>
<Button htmlType="submit" type="primary">
Submit
</Button>
&nbsp;&nbsp;&nbsp;
<Button htmlType="button" onClick={onReset}>
Reset
</Button>
</Form.Item>
</Form>
);
};
ReactDOM.render(<FormDemo />, mountNode);
ReactDOM.render(<App />, mountNode);
```

20
components/modal/__tests__/confirm.test.js

@ -80,17 +80,15 @@ describe('Modal.confirm triggers callbacks correctly', () => {
});
});
if (process.env.REACT !== '15') {
it('shows animation when close', () => {
jest.useFakeTimers();
open();
$$('.ant-btn')[0].click();
expect($$('.ant-modal-confirm')).toHaveLength(1);
jest.runAllTimers();
expect($$('.ant-modal-confirm')).toHaveLength(0);
jest.useRealTimers();
});
}
it('shows animation when close', () => {
jest.useFakeTimers();
open();
$$('.ant-btn')[0].click();
expect($$('.ant-modal-confirm')).toHaveLength(1);
jest.runAllTimers();
expect($$('.ant-modal-confirm')).toHaveLength(0);
jest.useRealTimers();
});
it('ok only', () => {
open({ okCancel: false });

532
components/table/__tests__/__snapshots__/demo.test.js.snap

@ -1877,7 +1877,7 @@ exports[`renders ./components/table/demo/dynamic-settings.md correctly 1`] = `
class="ant-form ant-form-inline"
>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-label"
@ -1890,30 +1890,26 @@ exports[`renders ./components/table/demo/dynamic-settings.md correctly 1`] = `
</label>
</div>
<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
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<button
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<span
class="ant-switch-inner"
/>
</button>
</span>
<span
class="ant-switch-inner"
/>
</button>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-label"
@ -1926,30 +1922,26 @@ exports[`renders ./components/table/demo/dynamic-settings.md correctly 1`] = `
</label>
</div>
<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
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<button
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<span
class="ant-switch-inner"
/>
</button>
</span>
<span
class="ant-switch-inner"
/>
</button>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-label"
@ -1962,30 +1954,26 @@ exports[`renders ./components/table/demo/dynamic-settings.md correctly 1`] = `
</label>
</div>
<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
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<button
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<span
class="ant-switch-inner"
/>
</button>
</span>
<span
class="ant-switch-inner"
/>
</button>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-label"
@ -1998,31 +1986,27 @@ exports[`renders ./components/table/demo/dynamic-settings.md correctly 1`] = `
</label>
</div>
<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
aria-checked="true"
checked=""
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<button
aria-checked="true"
checked=""
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<span
class="ant-switch-inner"
/>
</button>
</span>
<span
class="ant-switch-inner"
/>
</button>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-label"
@ -2035,31 +2019,27 @@ exports[`renders ./components/table/demo/dynamic-settings.md correctly 1`] = `
</label>
</div>
<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
aria-checked="true"
checked=""
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<button
aria-checked="true"
checked=""
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<span
class="ant-switch-inner"
/>
</button>
</span>
<span
class="ant-switch-inner"
/>
</button>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-label"
@ -2072,31 +2052,27 @@ exports[`renders ./components/table/demo/dynamic-settings.md correctly 1`] = `
</label>
</div>
<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
aria-checked="true"
checked=""
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<button
aria-checked="true"
checked=""
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<span
class="ant-switch-inner"
/>
</button>
</span>
<span
class="ant-switch-inner"
/>
</button>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-label"
@ -2109,31 +2085,27 @@ exports[`renders ./components/table/demo/dynamic-settings.md correctly 1`] = `
</label>
</div>
<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
aria-checked="true"
checked=""
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<button
aria-checked="true"
checked=""
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<span
class="ant-switch-inner"
/>
</button>
</span>
<span
class="ant-switch-inner"
/>
</button>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-label"
@ -2146,30 +2118,26 @@ exports[`renders ./components/table/demo/dynamic-settings.md correctly 1`] = `
</label>
</div>
<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
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<button
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<span
class="ant-switch-inner"
/>
</button>
</span>
<span
class="ant-switch-inner"
/>
</button>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-label"
@ -2182,31 +2150,27 @@ exports[`renders ./components/table/demo/dynamic-settings.md correctly 1`] = `
</label>
</div>
<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
aria-checked="true"
checked=""
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<button
aria-checked="true"
checked=""
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<span
class="ant-switch-inner"
/>
</button>
</span>
<span
class="ant-switch-inner"
/>
</button>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-label"
@ -2219,82 +2183,78 @@ exports[`renders ./components/table/demo/dynamic-settings.md correctly 1`] = `
</label>
</div>
<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"
<div
class="ant-radio-group ant-radio-group-outline ant-radio-group-default"
>
<div
class="ant-radio-group ant-radio-group-outline ant-radio-group-default"
<label
class="ant-radio-button-wrapper ant-radio-button-wrapper-checked"
>
<label
class="ant-radio-button-wrapper ant-radio-button-wrapper-checked"
<span
class="ant-radio-button ant-radio-button-checked"
>
<input
checked=""
class="ant-radio-button-input"
type="radio"
value="default"
/>
<span
class="ant-radio-button ant-radio-button-checked"
>
<input
checked=""
class="ant-radio-button-input"
type="radio"
value="default"
/>
<span
class="ant-radio-button-inner"
/>
</span>
<span>
Default
</span>
</label>
<label
class="ant-radio-button-wrapper"
class="ant-radio-button-inner"
/>
</span>
<span>
Default
</span>
</label>
<label
class="ant-radio-button-wrapper"
>
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
value="middle"
/>
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
value="middle"
/>
<span
class="ant-radio-button-inner"
/>
</span>
<span>
Middle
</span>
</label>
<label
class="ant-radio-button-wrapper"
class="ant-radio-button-inner"
/>
</span>
<span>
Middle
</span>
</label>
<label
class="ant-radio-button-wrapper"
>
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
value="small"
/>
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
value="small"
/>
<span
class="ant-radio-button-inner"
/>
</span>
<span>
Small
</span>
</label>
</div>
</span>
class="ant-radio-button-inner"
/>
</span>
<span>
Small
</span>
</label>
</div>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
class="ant-row-flex ant-form-item"
>
<div
class="ant-col ant-form-item-label"
@ -2307,96 +2267,92 @@ exports[`renders ./components/table/demo/dynamic-settings.md correctly 1`] = `
</label>
</div>
<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"
<div
class="ant-radio-group ant-radio-group-outline"
>
<div
class="ant-radio-group ant-radio-group-outline"
<label
class="ant-radio-button-wrapper"
>
<label
class="ant-radio-button-wrapper"
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
value="top"
/>
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
value="top"
/>
<span
class="ant-radio-button-inner"
/>
</span>
<span>
Top
</span>
</label>
<label
class="ant-radio-button-wrapper ant-radio-button-wrapper-checked"
class="ant-radio-button-inner"
/>
</span>
<span>
Top
</span>
</label>
<label
class="ant-radio-button-wrapper ant-radio-button-wrapper-checked"
>
<span
class="ant-radio-button ant-radio-button-checked"
>
<input
checked=""
class="ant-radio-button-input"
type="radio"
value="bottom"
/>
<span
class="ant-radio-button ant-radio-button-checked"
>
<input
checked=""
class="ant-radio-button-input"
type="radio"
value="bottom"
/>
<span
class="ant-radio-button-inner"
/>
</span>
<span>
Bottom
</span>
</label>
<label
class="ant-radio-button-wrapper"
class="ant-radio-button-inner"
/>
</span>
<span>
Bottom
</span>
</label>
<label
class="ant-radio-button-wrapper"
>
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
value="both"
/>
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
value="both"
/>
<span
class="ant-radio-button-inner"
/>
</span>
<span>
Both
</span>
</label>
<label
class="ant-radio-button-wrapper"
class="ant-radio-button-inner"
/>
</span>
<span>
Both
</span>
</label>
<label
class="ant-radio-button-wrapper"
>
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
value="none"
/>
<span
class="ant-radio-button"
>
<input
class="ant-radio-button-input"
type="radio"
value="none"
/>
<span
class="ant-radio-button-inner"
/>
</span>
<span>
None
</span>
</label>
</div>
</span>
class="ant-radio-button-inner"
/>
</span>
<span>
None
</span>
</label>
</div>
</div>
</div>
</div>

4
components/table/__tests__/demo.test.js

@ -1,5 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('table', {
skip: process.env.REACT === '15' ? ['edit-row', 'drag-sorting'] : [],
});
demoTest('table');

151
components/table/demo/edit-cell.md

@ -13,93 +13,102 @@ title:
Table with editable cells.
```jsx
```tsx
import { Table, Input, Button, Popconfirm, Form } from 'antd';
const EditableContext = React.createContext();
const EditableContext = React.createContext<any>();
const EditableRow = ({ form, index, ...props }) => (
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
);
interface Item {
key: string;
name: string;
age: string;
address: string;
}
const EditableFormRow = Form.create()(EditableRow);
interface EditableRowProps {
index: number;
}
class EditableCell extends React.Component {
state = {
editing: false,
};
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
</Form>
);
};
interface EditableCellProps {
title: React.ReactNode;
editable: boolean;
children: React.ReactNode;
dataIndex: string;
record: Item;
handleSave: (record: Item) => void;
}
toggleEdit = () => {
const editing = !this.state.editing;
this.setState({ editing }, () => {
if (editing) {
this.input.focus();
}
});
const EditableCell: React.FC<EditableCellProps> = ({
title,
editable,
children,
dataIndex,
record,
handleSave,
...restProps
}) => {
const [editing, setEditing] = React.useState(false);
const inputRef = React.useRef();
const form = React.useContext(EditableContext);
React.useEffect(() => {
if (editing) {
inputRef.current.focus();
}
}, [editing]);
const toggleEdit = () => {
setEditing(!editing);
form.setFieldsValue({ [dataIndex]: record[dataIndex] });
};
save = e => {
const { record, handleSave } = this.props;
this.form.validateFields((error, values) => {
if (error && error[e.currentTarget.id]) {
return;
}
this.toggleEdit();
const save = async e => {
try {
const values = await form.validateFields();
toggleEdit();
handleSave({ ...record, ...values });
});
} catch (errInfo) {
console.log('Save failed:', errInfo);
}
};
renderCell = form => {
this.form = form;
const { children, dataIndex, record, title } = this.props;
const { editing } = this.state;
return editing ? (
<Form.Item style={{ margin: 0 }}>
{form.getFieldDecorator(dataIndex, {
rules: [
{
required: true,
message: `${title} is required.`,
},
],
initialValue: record[dataIndex],
})(<Input ref={node => (this.input = node)} onPressEnter={this.save} onBlur={this.save} />)}
let childNode = children;
if (editable) {
childNode = editing ? (
<Form.Item
style={{ margin: 0 }}
name={dataIndex}
rules={[
{
required: true,
message: `${title} is required.`,
},
]}
>
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
</Form.Item>
) : (
<div
className="editable-cell-value-wrap"
style={{ paddingRight: 24 }}
onClick={this.toggleEdit}
>
<div className="editable-cell-value-wrap" style={{ paddingRight: 24 }} onClick={toggleEdit}>
{children}
</div>
);
};
render() {
const {
editable,
dataIndex,
title,
record,
index,
handleSave,
children,
...restProps
} = this.props;
return (
<td {...restProps}>
{editable ? (
<EditableContext.Consumer>{this.renderCell}</EditableContext.Consumer>
) : (
children
)}
</td>
);
}
}
return <td {...restProps}>{childNode}</td>;
};
class EditableTable extends React.Component {
constructor(props) {
@ -184,7 +193,7 @@ class EditableTable extends React.Component {
const { dataSource } = this.state;
const components = {
body: {
row: EditableFormRow,
row: EditableRow,
cell: EditableCell,
},
};

314
components/table/demo/edit-row.md

@ -13,133 +13,90 @@ title:
Table with editable rows.
```jsx
```tsx
import { Table, Input, InputNumber, Popconfirm, Form } from 'antd';
const data = [];
interface Item {
key: string;
name: string;
age: number;
address: string;
}
const originData: Item[] = [];
for (let i = 0; i < 100; i++) {
data.push({
originData.push({
key: i.toString(),
name: `Edrward ${i}`,
age: 32,
address: `London Park no. ${i}`,
});
}
const EditableContext = React.createContext();
interface EditableCellProps {
editing: boolean;
dataIndex: string;
title: React.ReactNode;
inputType: 'number' | 'text';
record: Item;
index: number;
children: React.ReactNode;
}
class EditableCell extends React.Component {
getInput = () => {
if (this.props.inputType === 'number') {
return <InputNumber />;
}
return <Input />;
const EditableCell: React.FC<EditableCellProps> = ({
editing,
dataIndex,
title,
inputType,
record,
index,
children,
...restProps
}) => {
const inputNode = inputType === 'number' ? <InputNumber /> : <Input />;
return (
<td {...restProps}>
{editing ? (
<Form.Item
name={dataIndex}
style={{ margin: 0 }}
rules={[
{
required: true,
message: `Please Input ${title}!`,
},
]}
>
{inputNode}
</Form.Item>
) : (
children
)}
</td>
);
};
const EditableTable = () => {
const [form] = Form.useForm();
const [data, setData] = React.useState(originData);
const [editingKey, setEditingKey] = React.useState('');
const isEditing = record => record.key === editingKey;
const edit = record => {
form.setFieldsValue({ ...record });
setEditingKey(record.key);
};
renderCell = ({ getFieldDecorator }) => {
const {
editing,
dataIndex,
title,
inputType,
record,
index,
children,
...restProps
} = this.props;
return (
<td {...restProps}>
{editing ? (
<Form.Item style={{ margin: 0 }}>
{getFieldDecorator(dataIndex, {
rules: [
{
required: true,
message: `Please Input ${title}!`,
},
],
initialValue: record[dataIndex],
})(this.getInput())}
</Form.Item>
) : (
children
)}
</td>
);
const cancel = () => {
setEditingKey('');
};
render() {
return <EditableContext.Consumer>{this.renderCell}</EditableContext.Consumer>;
}
}
class EditableTable extends React.Component {
constructor(props) {
super(props);
this.state = { data, editingKey: '' };
this.columns = [
{
title: 'name',
dataIndex: 'name',
width: '25%',
editable: true,
},
{
title: 'age',
dataIndex: 'age',
width: '15%',
editable: true,
},
{
title: 'address',
dataIndex: 'address',
width: '40%',
editable: true,
},
{
title: 'operation',
dataIndex: 'operation',
render: (text, record) => {
const { editingKey } = this.state;
const editable = this.isEditing(record);
return editable ? (
<span>
<EditableContext.Consumer>
{form => (
<a
href="javascript:;"
onClick={() => this.save(form, record.key)}
style={{ marginRight: 8 }}
>
Save
</a>
)}
</EditableContext.Consumer>
<Popconfirm title="Sure to cancel?" onConfirm={() => this.cancel(record.key)}>
<a>Cancel</a>
</Popconfirm>
</span>
) : (
<a disabled={editingKey !== ''} onClick={() => this.edit(record.key)}>
Edit
</a>
);
},
},
];
}
isEditing = record => record.key === this.state.editingKey;
cancel = () => {
this.setState({ editingKey: '' });
};
const save = async key => {
try {
const row = await form.validateFields();
save(form, key) {
form.validateFields((error, row) => {
if (error) {
return;
}
const newData = [...this.state.data];
const newData = [...data];
const index = newData.findIndex(item => key === item.key);
if (index > -1) {
const item = newData[index];
@ -147,61 +104,100 @@ class EditableTable extends React.Component {
...item,
...row,
});
this.setState({ data: newData, editingKey: '' });
setData(newData);
setEditingKey('');
} else {
newData.push(row);
this.setState({ data: newData, editingKey: '' });
setData(newData);
setEditingKey('');
}
});
}
edit(key) {
this.setState({ editingKey: key });
}
} catch (errInfo) {
console.log('Validate Failed:', errInfo);
return;
}
};
render() {
const components = {
body: {
cell: EditableCell,
const columns = [
{
title: 'name',
dataIndex: 'name',
width: '25%',
editable: true,
},
{
title: 'age',
dataIndex: 'age',
width: '15%',
editable: true,
},
{
title: 'address',
dataIndex: 'address',
width: '40%',
editable: true,
},
{
title: 'operation',
dataIndex: 'operation',
render: (text, record) => {
const editable = isEditing(record);
return editable ? (
<span>
<a href="javascript:;" onClick={() => save(record.key)} style={{ marginRight: 8 }}>
Save
</a>
<Popconfirm title="Sure to cancel?" onConfirm={() => cancel(record.key)}>
<a>Cancel</a>
</Popconfirm>
</span>
) : (
<a disabled={editingKey !== ''} onClick={() => edit(record)}>
Edit
</a>
);
},
};
},
];
const columns = this.columns.map(col => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: record => ({
record,
inputType: col.dataIndex === 'age' ? 'number' : 'text',
dataIndex: col.dataIndex,
title: col.title,
editing: this.isEditing(record),
}),
};
});
return (
<EditableContext.Provider value={this.props.form}>
<Table
components={components}
bordered
dataSource={this.state.data}
columns={columns}
rowClassName="editable-row"
pagination={{
onChange: this.cancel,
}}
/>
</EditableContext.Provider>
);
}
}
const components = {
body: {
cell: EditableCell,
},
};
const EditableFormTable = Form.create()(EditableTable);
const mergedColumns = columns.map(col => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: record => ({
record,
inputType: col.dataIndex === 'age' ? 'number' : 'text',
dataIndex: col.dataIndex,
title: col.title,
editing: isEditing(record),
}),
};
});
ReactDOM.render(<EditableFormTable />, mountNode);
return (
<Form form={form} component={false}>
<Table
components={components}
bordered
dataSource={data}
columns={mergedColumns}
rowClassName="editable-row"
pagination={{
onChange: cancel,
}}
/>
</Form>
);
};
ReactDOM.render(<EditableTable />, mountNode);
```
```css

2
components/tabs/__tests__/demo.test.js

@ -1,3 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('tabs', { skip: process.env.REACT === '15' ? ['custom-tab-bar-node.md'] : [] });
demoTest('tabs');

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save