Form.Item support layout (#49119)

* feat: FormItem support layout

* feat: doc

* feat: test

* feat: test

* feat: test

* feat: test

* feat: itemPrefixCls
This commit is contained in:
叶枫 2024-05-31 10:50:47 +08:00 committed by GitHub
parent e244a0b816
commit 5932005ce4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 407 additions and 6 deletions

View File

@ -29,6 +29,7 @@ export type RequiredMark =
| 'optional'
| ((labelNode: React.ReactNode, info: { required: boolean }) => React.ReactNode);
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
export type FormItemLayout = 'horizontal' | 'vertical';
export interface FormProps<Values = any> extends Omit<RcFormProps<Values>, 'form'> {
prefixCls?: string;

View File

@ -47,11 +47,13 @@ export default function ItemHolder(props: ItemHolderProps) {
required,
isRequired,
onSubItemMetaChange,
layout,
...restProps
} = props;
const itemPrefixCls = `${prefixCls}-item`;
const { requiredMark } = React.useContext(FormContext);
const { requiredMark, vertical: formVertical } = React.useContext(FormContext);
const vertical = formVertical || layout === 'vertical';
// ======================== Margin ========================
const itemRef = React.useRef<HTMLDivElement>(null);
@ -99,6 +101,9 @@ export default function ItemHolder(props: ItemHolderProps) {
[`${itemPrefixCls}-has-error`]: mergedValidateStatus === 'error',
[`${itemPrefixCls}-is-validating`]: mergedValidateStatus === 'validating',
[`${itemPrefixCls}-hidden`]: hidden,
// Layout
[`${itemPrefixCls}-${layout}`]: layout,
});
return (
@ -145,6 +150,7 @@ export default function ItemHolder(props: ItemHolderProps) {
requiredMark={requiredMark}
required={required ?? isRequired}
prefixCls={prefixCls}
vertical={vertical}
/>
{/* Input Group */}
<FormItemInput

View File

@ -11,7 +11,7 @@ import { devUseWarning } from '../../_util/warning';
import { ConfigContext } from '../../config-provider';
import useCSSVarCls from '../../config-provider/hooks/useCSSVarCls';
import { FormContext, NoStyleItemContext } from '../context';
import type { FormInstance } from '../Form';
import type { FormInstance, FormItemLayout } from '../Form';
import type { FormItemInputProps } from '../FormItemInput';
import type { FormItemLabelProps, LabelTooltipType } from '../FormItemLabel';
import useChildren from '../hooks/useChildren';
@ -102,6 +102,7 @@ export interface FormItemProps<Values = any>
tooltip?: LabelTooltipType;
/** @deprecated No need anymore */
fieldKey?: React.Key | React.Key[];
layout?: FormItemLayout;
}
function genEmptyMeta(): Meta {
@ -132,6 +133,7 @@ function InternalFormItem<Values = any>(props: FormItemProps<Values>): React.Rea
validateTrigger,
hidden,
help,
layout,
} = props;
const { getPrefixCls } = React.useContext(ConfigContext);
const { name: formName } = React.useContext(FormContext);
@ -273,6 +275,7 @@ function InternalFormItem<Values = any>(props: FormItemProps<Values>): React.Rea
warnings={mergedWarnings}
meta={meta}
onSubItemMetaChange={onSubItemMetaChange}
layout={layout}
>
{baseChildren}
</ItemHolder>

View File

@ -44,6 +44,7 @@ export interface FormItemLabelProps {
*/
requiredMark?: RequiredMark;
tooltip?: LabelTooltipType;
vertical?: boolean;
}
const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixCls: string }> = ({
@ -56,11 +57,11 @@ const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixC
required,
requiredMark,
tooltip,
vertical,
}) => {
const [formLocale] = useLocale('Form');
const {
vertical,
labelAlign: contextLabelAlign,
labelCol: contextLabelCol,
labelWrap,

View File

@ -8790,6 +8790,92 @@ exports[`renders components/form/demo/layout-can-wrap.tsx extend context correct
exports[`renders components/form/demo/layout-can-wrap.tsx extend context correctly 2`] = `[]`;
exports[`renders components/form/demo/layout-multiple.tsx extend context correctly 1`] = `
<form
class="ant-form ant-form-horizontal"
id="layout-multiple"
>
<div
class="ant-form-item"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class="ant-form-item-required"
for="layout-multiple_name"
title="name"
>
name
</label>
</div>
<div
class="ant-col ant-col-20 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input ant-input-outlined"
id="layout-multiple_name"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item ant-form-item-vertical"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-24 ant-form-item-label"
>
<label
class="ant-form-item-required"
for="layout-multiple_age"
title="loooooooooooooooooooooooooooooooong"
>
loooooooooooooooooooooooooooooooong
</label>
</div>
<div
class="ant-col ant-col-24 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input ant-input-outlined"
id="layout-multiple_age"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
</form>
`;
exports[`renders components/form/demo/layout-multiple.tsx extend context correctly 2`] = `[]`;
exports[`renders components/form/demo/nest-messages.tsx extend context correctly 1`] = `
<form
class="ant-form ant-form-horizontal"

View File

@ -5138,6 +5138,90 @@ exports[`renders components/form/demo/layout-can-wrap.tsx correctly 1`] = `
</form>
`;
exports[`renders components/form/demo/layout-multiple.tsx correctly 1`] = `
<form
class="ant-form ant-form-horizontal"
id="layout-multiple"
>
<div
class="ant-form-item"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class="ant-form-item-required"
for="layout-multiple_name"
title="name"
>
name
</label>
</div>
<div
class="ant-col ant-col-20 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input ant-input-outlined"
id="layout-multiple_name"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item ant-form-item-vertical"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-24 ant-form-item-label"
>
<label
class="ant-form-item-required"
for="layout-multiple_age"
title="loooooooooooooooooooooooooooooooong"
>
loooooooooooooooooooooooooooooooong
</label>
</div>
<div
class="ant-col ant-col-24 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input ant-input-outlined"
id="layout-multiple_age"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
</form>
`;
exports[`renders components/form/demo/nest-messages.tsx correctly 1`] = `
<form
class="ant-form ant-form-horizontal"

View File

@ -1085,6 +1085,124 @@ exports[`Form form should support disabled 1`] = `
</form>
`;
exports[`Form form.item should support layout 1`] = `
<form
class="ant-form ant-form-horizontal"
>
<div
class="ant-form-item"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class=""
for="name"
title="name"
>
name
</label>
</div>
<div
class="ant-col ant-col-14 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input ant-input-outlined"
id="name"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item ant-form-item-horizontal"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class=""
for="horizontal"
title="horizontal"
>
horizontal
</label>
</div>
<div
class="ant-col ant-col-14 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input ant-input-outlined"
id="horizontal"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item ant-form-item-vertical"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-col-4 ant-form-item-label"
>
<label
class=""
for="vertical"
title="vertical"
>
vertical
</label>
</div>
<div
class="ant-col ant-col-14 ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input ant-input-outlined"
id="vertical"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
</div>
</form>
`;
exports[`Form rtl render component should be rendered correctly in RTL direction 1`] = `
<form
class="ant-form ant-form-horizontal ant-form-rtl"

View File

@ -1320,6 +1320,24 @@ describe('Form', () => {
expect(container.firstChild).toMatchSnapshot();
});
it('form.item should support layout', () => {
const App: React.FC = () => (
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 14 }} layout="horizontal">
<Form.Item label="name" name="name">
<Input />
</Form.Item>
<Form.Item label="horizontal" name="horizontal" layout="horizontal">
<Input />
</Form.Item>
<Form.Item label="vertical" name="vertical" layout="vertical">
<Input />
</Form.Item>
</Form>
);
const { container } = render(<App />);
expect(container.firstChild).toMatchSnapshot();
});
it('_internalItemRender api test', () => {
const { container } = render(
<Form>

View File

@ -0,0 +1,7 @@
## zh-CN
`Form.Item` 上单独定义 `layout`,可以做到一个表单多种布局。
## en-US
Defining a separate `layout` on `Form.Item` can achieve multiple layouts for a single form.

View File

@ -0,0 +1,22 @@
import React from 'react';
import { Form, Input } from 'antd';
const App: React.FC = () => (
<Form name="layout-multiple" layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
<Form.Item label="name" name="name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item
layout="vertical"
label="loooooooooooooooooooooooooooooooong"
name="age"
rules={[{ required: true }]}
labelCol={{ span: 24 }}
wrapperCol={{ span: 24 }}
>
<Input />
</Form.Item>
</Form>
);
export default App;

View File

@ -18,6 +18,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA
<code src="./demo/basic.tsx">Basic Usage</code>
<code src="./demo/control-hooks.tsx">Form methods</code>
<code src="./demo/layout.tsx">Form Layout</code>
<code src="./demo/layout-multiple.tsx">Form mix layout</code>
<code src="./demo/disabled.tsx">Form disabled</code>
<code src="./demo/variant.tsx" version="5.13.0">Form variants</code>
<code src="./demo/required-mark.tsx">Required style</code>
@ -90,6 +91,7 @@ Common props ref[Common props](/docs/react/common-props)
| onFinishFailed | Trigger after submitting the form and verifying data failed | function({ values, errorFields, outOfDate }) | - | |
| onValuesChange | Trigger when value updated | function(changedValues, allValues) | - | |
| clearOnDestroy | Clear form values when the form is uninstalled | boolean | false | 5.18.0 |
| layout | Form item layout | `horizontal` \| `vertical` | - | 5.20.0 |
### validateMessages

View File

@ -19,6 +19,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA
<code src="./demo/basic.tsx">基本使用</code>
<code src="./demo/control-hooks.tsx">表单方法调用</code>
<code src="./demo/layout.tsx">表单布局</code>
<code src="./demo/layout-multiple.tsx">表单混合布局</code>
<code src="./demo/disabled.tsx">表单禁用</code>
<code src="./demo/variant.tsx" version="5.13.0">表单变体</code>
<code src="./demo/required-mark.tsx">必选样式</code>
@ -91,6 +92,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA
| onFinishFailed | 提交表单且数据验证失败后回调事件 | function({ values, errorFields, outOfDate }) | - | |
| onValuesChange | 字段值更新时触发回调事件 | function(changedValues, allValues) | - | |
| clearOnDestroy | 当表单被卸载时清空表单值 | boolean | false | 5.18.0 |
| layout | 表单项布局 | `horizontal` \| `vertical` \| | - | 5.20.0 |
### validateMessages

View File

@ -499,15 +499,15 @@ const genVerticalStyle: GenerateStyle<FormToken> = (token) => {
return {
[`${componentCls}-vertical`]: {
[formItemCls]: {
'&-row': {
[`${formItemCls}-row`]: {
flexDirection: 'column',
},
'&-label > label': {
[`${formItemCls}-label > label`]: {
height: 'auto',
},
[`${componentCls}-item-control`]: {
[`${formItemCls}-control`]: {
width: '100%',
},
},
@ -546,6 +546,56 @@ const genVerticalStyle: GenerateStyle<FormToken> = (token) => {
};
};
const genItemVerticalStyle: GenerateStyle<FormToken> = (token) => {
const { formItemCls, rootPrefixCls } = token;
return {
[`${formItemCls}-vertical`]: {
[`${formItemCls}-row`]: {
flexDirection: 'column',
},
[`${formItemCls}-label > label`]: {
height: 'auto',
},
[`${formItemCls}-control`]: {
width: '100%',
},
},
[`${formItemCls}-vertical ${formItemCls}-label,
.${rootPrefixCls}-col-24${formItemCls}-label,
.${rootPrefixCls}-col-xl-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
[`@media (max-width: ${unit(token.screenXSMax)})`]: [
makeVerticalLayout(token),
{
[formItemCls]: {
[`.${rootPrefixCls}-col-xs-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
],
[`@media (max-width: ${unit(token.screenSMMax)})`]: {
[formItemCls]: {
[`.${rootPrefixCls}-col-sm-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
[`@media (max-width: ${unit(token.screenMDMax)})`]: {
[formItemCls]: {
[`.${rootPrefixCls}-col-md-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
[`@media (max-width: ${unit(token.screenLGMax)})`]: {
[formItemCls]: {
[`.${rootPrefixCls}-col-lg-24${formItemCls}-label`]: makeVerticalLayoutLabel(token),
},
},
};
};
// ============================== Export ==============================
export const prepareComponentToken: GetDefaultToken<'Form'> = (token) => ({
labelRequiredMarkColor: token.colorError,
@ -584,6 +634,7 @@ export default genStyleHooks(
genHorizontalStyle(formToken),
genInlineStyle(formToken),
genVerticalStyle(formToken),
genItemVerticalStyle(formToken),
genCollapseMotion(formToken),
zoomIn,
];