mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-25 03:29:59 +08:00
fix: Form.Item keep render even it's not a Field (#20963)
* Not use Field when Form.Item is pure * Add test case * clean up * add delay update for test
This commit is contained in:
parent
55bbe3d113
commit
b67a1bee7d
@ -3,6 +3,7 @@ import isEqual from 'lodash/isEqual';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Field, FormInstance } from 'rc-field-form';
|
import { Field, FormInstance } from 'rc-field-form';
|
||||||
import { FieldProps } from 'rc-field-form/lib/Field';
|
import { FieldProps } from 'rc-field-form/lib/Field';
|
||||||
|
import { Meta } from 'rc-field-form/lib/interface';
|
||||||
import omit from 'omit.js';
|
import omit from 'omit.js';
|
||||||
import Row from '../grid/row';
|
import Row from '../grid/row';
|
||||||
import { ConfigContext } from '../config-provider';
|
import { ConfigContext } from '../config-provider';
|
||||||
@ -18,6 +19,8 @@ export type ValidateStatus = typeof ValidateStatuses[number];
|
|||||||
|
|
||||||
type RenderChildren = (form: FormInstance) => React.ReactElement;
|
type RenderChildren = (form: FormInstance) => React.ReactElement;
|
||||||
type RcFieldProps = Omit<FieldProps, 'children'>;
|
type RcFieldProps = Omit<FieldProps, 'children'>;
|
||||||
|
type ChildrenType = React.ReactElement | RenderChildren | React.ReactElement[] | null;
|
||||||
|
type ChildrenNodeType = Exclude<ChildrenType, RenderChildren>;
|
||||||
|
|
||||||
export interface FormItemProps
|
export interface FormItemProps
|
||||||
extends FormItemLabelProps,
|
extends FormItemLabelProps,
|
||||||
@ -27,7 +30,7 @@ export interface FormItemProps
|
|||||||
noStyle?: boolean;
|
noStyle?: boolean;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactElement | RenderChildren | React.ReactElement[];
|
children: ChildrenType;
|
||||||
id?: string;
|
id?: string;
|
||||||
hasFeedback?: boolean;
|
hasFeedback?: boolean;
|
||||||
validateStatus?: ValidateStatus;
|
validateStatus?: ValidateStatus;
|
||||||
@ -37,7 +40,7 @@ export interface FormItemProps
|
|||||||
fieldKey?: number;
|
fieldKey?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormItem: React.FC<FormItemProps> = (props: FormItemProps) => {
|
function FormItem(props: FormItemProps): React.ReactNode {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
fieldKey,
|
fieldKey,
|
||||||
@ -77,19 +80,6 @@ const FormItem: React.FC<FormItemProps> = (props: FormItemProps) => {
|
|||||||
|
|
||||||
const prefixCls = getPrefixCls('form', customizePrefixCls);
|
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 ========================
|
// ======================== Errors ========================
|
||||||
// Collect noStyle Field error to the top FormItem
|
// Collect noStyle Field error to the top FormItem
|
||||||
const updateChildItemErrors = noStyle
|
const updateChildItemErrors = noStyle
|
||||||
@ -103,19 +93,22 @@ const FormItem: React.FC<FormItemProps> = (props: FormItemProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function renderLayout(
|
||||||
|
baseChildren: ChildrenNodeType,
|
||||||
|
fieldId?: string,
|
||||||
|
meta?: Meta,
|
||||||
|
isRequired?: boolean,
|
||||||
|
) {
|
||||||
if (noStyle) {
|
if (noStyle) {
|
||||||
nameRef.current = [...mergedName];
|
return baseChildren;
|
||||||
if (fieldKey) {
|
|
||||||
nameRef.current[nameRef.current.length - 1] = fieldKey;
|
|
||||||
}
|
|
||||||
updateItemErrors(nameRef.current.join('__SPLIT__'), errors);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================== Errors ========================
|
||||||
let mergedErrors: React.ReactNode[];
|
let mergedErrors: React.ReactNode[];
|
||||||
if (help) {
|
if (help) {
|
||||||
mergedErrors = toArray(help);
|
mergedErrors = toArray(help);
|
||||||
} else {
|
} else {
|
||||||
mergedErrors = errors;
|
mergedErrors = meta ? meta.errors : [];
|
||||||
Object.keys(inlineErrors).forEach(subName => {
|
Object.keys(inlineErrors).forEach(subName => {
|
||||||
const subErrors = inlineErrors[subName] || [];
|
const subErrors = inlineErrors[subName] || [];
|
||||||
if (subErrors.length) {
|
if (subErrors.length) {
|
||||||
@ -128,15 +121,14 @@ const FormItem: React.FC<FormItemProps> = (props: FormItemProps) => {
|
|||||||
let mergedValidateStatus: ValidateStatus = '';
|
let mergedValidateStatus: ValidateStatus = '';
|
||||||
if (validateStatus !== undefined) {
|
if (validateStatus !== undefined) {
|
||||||
mergedValidateStatus = validateStatus;
|
mergedValidateStatus = validateStatus;
|
||||||
} else if (meta.validating) {
|
} else if (meta && meta.validating) {
|
||||||
mergedValidateStatus = 'validating';
|
mergedValidateStatus = 'validating';
|
||||||
} else if (!help && mergedErrors.length) {
|
} else if (!help && mergedErrors.length) {
|
||||||
mergedValidateStatus = 'error';
|
mergedValidateStatus = 'error';
|
||||||
} else if (meta.touched) {
|
} else if (meta && meta.touched) {
|
||||||
mergedValidateStatus = 'success';
|
mergedValidateStatus = 'success';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================== Class Name ======================
|
|
||||||
const itemClassName = {
|
const itemClassName = {
|
||||||
[`${prefixCls}-item`]: true,
|
[`${prefixCls}-item`]: true,
|
||||||
[`${prefixCls}-item-with-help`]: domErrorVisible || help,
|
[`${prefixCls}-item-with-help`]: domErrorVisible || help,
|
||||||
@ -152,81 +144,7 @@ const FormItem: React.FC<FormItemProps> = (props: FormItemProps) => {
|
|||||||
[`${prefixCls}-item-is-validating`]: mergedValidateStatus === 'validating',
|
[`${prefixCls}-item-is-validating`]: mergedValidateStatus === 'validating',
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRequired =
|
|
||||||
required !== undefined
|
|
||||||
? required
|
|
||||||
: !!(
|
|
||||||
rules &&
|
|
||||||
rules.some(rule => {
|
|
||||||
if (rule && typeof rule === 'object' && rule.required) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (typeof rule === 'function') {
|
|
||||||
const ruleEntity = rule(context);
|
|
||||||
return ruleEntity && ruleEntity.required;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// ======================= Children =======================
|
// ======================= Children =======================
|
||||||
const fieldId = getFieldId(mergedName, formName);
|
|
||||||
const mergedControl: typeof control = {
|
|
||||||
...control,
|
|
||||||
id: fieldId,
|
|
||||||
};
|
|
||||||
|
|
||||||
let childNode;
|
|
||||||
if (Array.isArray(children) && !!name) {
|
|
||||||
warning(false, 'Form.Item', '`children` is array of render props cannot have `name`.');
|
|
||||||
childNode = children;
|
|
||||||
} else if (typeof children === 'function' && (!shouldUpdate || !!name)) {
|
|
||||||
warning(
|
|
||||||
!!shouldUpdate,
|
|
||||||
'Form.Item',
|
|
||||||
'`children` of render props only work with `shouldUpdate`.',
|
|
||||||
);
|
|
||||||
warning(
|
|
||||||
!name,
|
|
||||||
'Form.Item',
|
|
||||||
"Do not use `name` with `children` of render props since it's not a field.",
|
|
||||||
);
|
|
||||||
} 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);
|
|
||||||
} else {
|
|
||||||
warning(
|
|
||||||
!mergedName.length,
|
|
||||||
'Form.Item',
|
|
||||||
'`name` is only used for validate React element. If you are using Form.Item as layout display, please remove `name` instead.',
|
|
||||||
);
|
|
||||||
childNode = children;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noStyle) {
|
|
||||||
return childNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
className={classNames(itemClassName)}
|
className={classNames(itemClassName)}
|
||||||
@ -251,12 +169,7 @@ const FormItem: React.FC<FormItemProps> = (props: FormItemProps) => {
|
|||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
{/* Label */}
|
{/* Label */}
|
||||||
<FormItemLabel
|
<FormItemLabel htmlFor={fieldId} {...props} required={isRequired} prefixCls={prefixCls} />
|
||||||
htmlFor={fieldId}
|
|
||||||
{...props}
|
|
||||||
required={isRequired}
|
|
||||||
prefixCls={prefixCls}
|
|
||||||
/>
|
|
||||||
{/* Input Group */}
|
{/* Input Group */}
|
||||||
<FormItemInput
|
<FormItemInput
|
||||||
{...props}
|
{...props}
|
||||||
@ -267,14 +180,120 @@ const FormItem: React.FC<FormItemProps> = (props: FormItemProps) => {
|
|||||||
validateStatus={mergedValidateStatus}
|
validateStatus={mergedValidateStatus}
|
||||||
>
|
>
|
||||||
<FormItemContext.Provider value={{ updateItemErrors: updateChildItemErrors }}>
|
<FormItemContext.Provider value={{ updateItemErrors: updateChildItemErrors }}>
|
||||||
{childNode}
|
{baseChildren}
|
||||||
</FormItemContext.Provider>
|
</FormItemContext.Provider>
|
||||||
</FormItemInput>
|
</FormItemInput>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRenderProps = typeof children === 'function';
|
||||||
|
|
||||||
|
if (!name && !isRenderProps && !dependencies) {
|
||||||
|
return renderLayout(children as ChildrenNodeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...props}
|
||||||
|
trigger={trigger}
|
||||||
|
validateTrigger={validateTrigger}
|
||||||
|
onReset={() => {
|
||||||
|
setDomErrorVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(control, meta, context) => {
|
||||||
|
const { errors } = meta;
|
||||||
|
|
||||||
|
const mergedName = toArray(name).length && meta ? meta.name : [];
|
||||||
|
const fieldId = getFieldId(mergedName, formName);
|
||||||
|
|
||||||
|
if (noStyle) {
|
||||||
|
nameRef.current = [...mergedName];
|
||||||
|
if (fieldKey) {
|
||||||
|
nameRef.current[nameRef.current.length - 1] = fieldKey;
|
||||||
|
}
|
||||||
|
updateItemErrors(nameRef.current.join('__SPLIT__'), errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRequired =
|
||||||
|
required !== undefined
|
||||||
|
? required
|
||||||
|
: !!(
|
||||||
|
rules &&
|
||||||
|
rules.some(rule => {
|
||||||
|
if (rule && typeof rule === 'object' && rule.required) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof rule === 'function') {
|
||||||
|
const ruleEntity = rule(context);
|
||||||
|
return ruleEntity && ruleEntity.required;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// ======================= Children =======================
|
||||||
|
const mergedControl: typeof control = {
|
||||||
|
...control,
|
||||||
|
id: fieldId,
|
||||||
|
};
|
||||||
|
|
||||||
|
let childNode: ChildrenNodeType = null;
|
||||||
|
if (Array.isArray(children) && !!name) {
|
||||||
|
warning(false, 'Form.Item', '`children` is array of render props cannot have `name`.');
|
||||||
|
childNode = children;
|
||||||
|
} else if (isRenderProps && (!shouldUpdate || !!name)) {
|
||||||
|
warning(
|
||||||
|
!!shouldUpdate,
|
||||||
|
'Form.Item',
|
||||||
|
'`children` of render props only work with `shouldUpdate`.',
|
||||||
|
);
|
||||||
|
warning(
|
||||||
|
!name,
|
||||||
|
'Form.Item',
|
||||||
|
"Do not use `name` with `children` of render props since it's not a field.",
|
||||||
|
);
|
||||||
|
} else if (dependencies && !isRenderProps && !name) {
|
||||||
|
warning(
|
||||||
|
false,
|
||||||
|
'Form.Item',
|
||||||
|
'Must set `name` or use render props when `dependencies` is set.',
|
||||||
|
);
|
||||||
|
} 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 (isRenderProps && shouldUpdate && !name) {
|
||||||
|
childNode = (children as RenderChildren)(context);
|
||||||
|
} else {
|
||||||
|
warning(
|
||||||
|
!mergedName.length,
|
||||||
|
'Form.Item',
|
||||||
|
'`name` is only used for validate React element. If you are using Form.Item as layout display, please remove `name` instead.',
|
||||||
|
);
|
||||||
|
childNode = children as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderLayout(childNode, fieldId, meta, isRequired);
|
||||||
}}
|
}}
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FormItem;
|
export default FormItem;
|
||||||
|
@ -18,8 +18,6 @@ interface FormItemInputMiscProps {
|
|||||||
prefixCls: string;
|
prefixCls: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
errors: React.ReactNode[];
|
errors: React.ReactNode[];
|
||||||
touched: boolean;
|
|
||||||
validating: boolean;
|
|
||||||
hasFeedback?: boolean;
|
hasFeedback?: boolean;
|
||||||
validateStatus?: ValidateStatus;
|
validateStatus?: ValidateStatus;
|
||||||
onDomErrorVisibleChange: (visible: boolean) => void;
|
onDomErrorVisibleChange: (visible: boolean) => void;
|
||||||
@ -59,12 +57,16 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
|
|||||||
|
|
||||||
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className);
|
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className);
|
||||||
|
|
||||||
const [visible, cacheErrors] = useCacheErrors(errors, changedVisible => {
|
const [visible, cacheErrors] = useCacheErrors(
|
||||||
|
errors,
|
||||||
|
changedVisible => {
|
||||||
if (changedVisible) {
|
if (changedVisible) {
|
||||||
onDomErrorVisibleChange(true);
|
onDomErrorVisibleChange(true);
|
||||||
}
|
}
|
||||||
forceUpdate({});
|
forceUpdate({});
|
||||||
}, !!help);
|
},
|
||||||
|
!!help,
|
||||||
|
);
|
||||||
|
|
||||||
const memoErrors = useMemo(
|
const memoErrors = useMemo(
|
||||||
() => cacheErrors,
|
() => cacheErrors,
|
||||||
|
@ -12,7 +12,7 @@ export interface FormItemLabelProps {
|
|||||||
labelCol?: ColProps;
|
labelCol?: ColProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormItemLabel: React.FC<FormItemLabelProps & { required: boolean; prefixCls: string }> = ({
|
const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixCls: string }> = ({
|
||||||
prefixCls,
|
prefixCls,
|
||||||
label,
|
label,
|
||||||
htmlFor,
|
htmlFor,
|
||||||
|
@ -89,9 +89,11 @@ describe('Form', () => {
|
|||||||
expect(wrapper.find(Input).length).toBe(2);
|
expect(wrapper.find(Input).length).toBe(2);
|
||||||
|
|
||||||
await change(wrapper, 1, '');
|
await change(wrapper, 1, '');
|
||||||
|
wrapper.update();
|
||||||
expect(wrapper.find('.ant-form-item-explain').length).toBe(1);
|
expect(wrapper.find('.ant-form-item-explain').length).toBe(1);
|
||||||
|
|
||||||
await operate('.remove');
|
await operate('.remove');
|
||||||
|
wrapper.update();
|
||||||
expect(wrapper.find(Input).length).toBe(1);
|
expect(wrapper.find(Input).length).toBe(1);
|
||||||
expect(wrapper.find('.ant-form-item-explain').length).toBe(0);
|
expect(wrapper.find('.ant-form-item-explain').length).toBe(0);
|
||||||
});
|
});
|
||||||
@ -341,7 +343,61 @@ describe('Form', () => {
|
|||||||
|
|
||||||
const wrapper = mount(<App />);
|
const wrapper = mount(<App />);
|
||||||
wrapper.find('button').simulate('click');
|
wrapper.find('button').simulate('click');
|
||||||
expect(wrapper.find('.ant-form-item').first().hasClass('ant-form-item-with-help')).toBeTruthy();
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('.ant-form-item')
|
||||||
|
.first()
|
||||||
|
.hasClass('ant-form-item-with-help'),
|
||||||
|
).toBeTruthy();
|
||||||
expect(wrapper.find('.ant-form-item-explain').text()).toEqual('bamboo');
|
expect(wrapper.find('.ant-form-item-explain').text()).toEqual('bamboo');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('warning when use `dependencies` but `name` is empty & children is not a render props', () => {
|
||||||
|
mount(
|
||||||
|
<Form>
|
||||||
|
<Form.Item dependencies={[]}>text</Form.Item>
|
||||||
|
</Form>,
|
||||||
|
);
|
||||||
|
expect(errorSpy).toHaveBeenCalledWith(
|
||||||
|
'Warning: [antd: Form.Item] Must set `name` or use render props when `dependencies` is set.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://github.com/ant-design/ant-design/issues/20948
|
||||||
|
it('not repeat render when Form.Item is not a real Field', async () => {
|
||||||
|
const shouldNotRender = jest.fn();
|
||||||
|
const StaticInput = () => {
|
||||||
|
shouldNotRender();
|
||||||
|
return <Input />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldRender = jest.fn();
|
||||||
|
const DynamicInput = () => {
|
||||||
|
shouldRender();
|
||||||
|
return <Input />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formRef = React.createRef();
|
||||||
|
|
||||||
|
mount(
|
||||||
|
<div>
|
||||||
|
<Form ref={formRef}>
|
||||||
|
<Form.Item>
|
||||||
|
<StaticInput />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="light">
|
||||||
|
<DynamicInput />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(shouldNotRender).toHaveBeenCalledTimes(1);
|
||||||
|
expect(shouldRender).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
formRef.current.setFieldsValue({ light: 'bamboo' });
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(shouldNotRender).toHaveBeenCalledTimes(1);
|
||||||
|
expect(shouldRender).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user