mirror of
https://github.com/ant-design/ant-design.git
synced 2025-08-06 07:56:28 +08:00
feat: Form.List support rules (#26676)
* Add ErrorList component * move class to ErrorList * support cache of status * update doc * update snapshot * patch test case * clean up * docs: add faq
This commit is contained in:
parent
faa534c1c0
commit
1faabb219d
@ -14190,7 +14190,7 @@ exports[`ConfigProvider components Form configProvider 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="config-form-item-explain"
|
||||
class="config-form-item-explain config-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -14227,7 +14227,7 @@ exports[`ConfigProvider components Form configProvider componentSize large 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="config-form-item-explain"
|
||||
class="config-form-item-explain config-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -14264,7 +14264,7 @@ exports[`ConfigProvider components Form configProvider componentSize middle 1`]
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="config-form-item-explain"
|
||||
class="config-form-item-explain config-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -14301,7 +14301,7 @@ exports[`ConfigProvider components Form configProvider virtual and dropdownMatch
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -14338,7 +14338,7 @@ exports[`ConfigProvider components Form normal 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -14375,7 +14375,7 @@ exports[`ConfigProvider components Form prefixCls 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="prefix-Form-item-explain"
|
||||
class="prefix-Form-item-explain prefix-Form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
|
95
components/form/ErrorList.tsx
Normal file
95
components/form/ErrorList.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import CSSMotion from 'rc-motion';
|
||||
import useMemo from 'rc-util/lib/hooks/useMemo';
|
||||
import useCacheErrors from './hooks/useCacheErrors';
|
||||
import useForceUpdate from '../_util/hooks/useForceUpdate';
|
||||
import { FormItemPrefixContext } from './context';
|
||||
|
||||
const EMPTY_LIST: React.ReactNode[] = [];
|
||||
|
||||
export interface ErrorListProps {
|
||||
errors?: React.ReactNode[];
|
||||
/** @private Internal usage. Do not use in your production */
|
||||
help?: React.ReactNode;
|
||||
/** @private Internal usage. Do not use in your production */
|
||||
onDomErrorVisibleChange?: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export default function ErrorList({
|
||||
errors = EMPTY_LIST,
|
||||
help,
|
||||
onDomErrorVisibleChange,
|
||||
}: ErrorListProps) {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const { prefixCls, status } = React.useContext(FormItemPrefixContext);
|
||||
|
||||
const [visible, cacheErrors] = useCacheErrors(
|
||||
errors,
|
||||
changedVisible => {
|
||||
if (changedVisible) {
|
||||
/**
|
||||
* We trigger in sync to avoid dom shaking but this get warning in react 16.13.
|
||||
* So use Promise to keep in micro async to handle this.
|
||||
* https://github.com/ant-design/ant-design/issues/21698#issuecomment-593743485
|
||||
*/
|
||||
Promise.resolve().then(() => {
|
||||
onDomErrorVisibleChange?.(true);
|
||||
});
|
||||
}
|
||||
forceUpdate();
|
||||
},
|
||||
!!help,
|
||||
);
|
||||
|
||||
const memoErrors = useMemo(
|
||||
() => cacheErrors,
|
||||
visible,
|
||||
(_, nextVisible) => nextVisible,
|
||||
);
|
||||
|
||||
// Memo status in same visible
|
||||
const [innerStatus, setInnerStatus] = React.useState(status);
|
||||
React.useEffect(() => {
|
||||
if (visible && status) {
|
||||
setInnerStatus(status);
|
||||
}
|
||||
}, [visible, status]);
|
||||
|
||||
const baseClassName = `${prefixCls}-item-explain`;
|
||||
|
||||
return (
|
||||
<CSSMotion
|
||||
motionDeadline={500}
|
||||
visible={visible}
|
||||
motionName="show-help"
|
||||
onLeaveEnd={() => {
|
||||
onDomErrorVisibleChange?.(false);
|
||||
}}
|
||||
motionAppear
|
||||
removeOnLeave
|
||||
>
|
||||
{({ className: motionClassName }: { className: string }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
baseClassName,
|
||||
{
|
||||
[`${baseClassName}-${innerStatus}`]: innerStatus,
|
||||
},
|
||||
motionClassName,
|
||||
)}
|
||||
key="help"
|
||||
>
|
||||
{memoErrors.map((error, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={index} role="alert">
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</CSSMotion>
|
||||
);
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useContext, useRef } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import classNames from 'classnames';
|
||||
import { Field, FormInstance } from 'rc-field-form';
|
||||
@ -86,15 +87,14 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
hidden,
|
||||
...restProps
|
||||
} = props;
|
||||
const destroyRef = React.useRef(false);
|
||||
const { getPrefixCls } = React.useContext(ConfigContext);
|
||||
const { name: formName, requiredMark } = React.useContext(FormContext);
|
||||
const { updateItemErrors } = React.useContext(FormItemContext);
|
||||
const destroyRef = useRef(false);
|
||||
const { getPrefixCls } = useContext(ConfigContext);
|
||||
const { name: formName, requiredMark } = useContext(FormContext);
|
||||
const { updateItemErrors } = useContext(FormItemContext);
|
||||
const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help);
|
||||
const prevValidateStatusRef = React.useRef<ValidateStatus | undefined>(validateStatus);
|
||||
const [inlineErrors, setInlineErrors] = useFrameState<Record<string, string[]>>({});
|
||||
|
||||
const { validateTrigger: contextValidateTrigger } = React.useContext(FieldContext);
|
||||
const { validateTrigger: contextValidateTrigger } = useContext(FieldContext);
|
||||
const mergedValidateTrigger =
|
||||
validateTrigger !== undefined ? validateTrigger : contextValidateTrigger;
|
||||
|
||||
@ -107,7 +107,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
const hasName = hasValidName(name);
|
||||
|
||||
// Cache Field NamePath
|
||||
const nameRef = React.useRef<(string | number)[]>([]);
|
||||
const nameRef = useRef<(string | number)[]>([]);
|
||||
|
||||
// Should clean up if Field removed
|
||||
React.useEffect(() => {
|
||||
@ -176,10 +176,6 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
mergedValidateStatus = 'success';
|
||||
}
|
||||
|
||||
if (domErrorVisible && help) {
|
||||
prevValidateStatusRef.current = mergedValidateStatus;
|
||||
}
|
||||
|
||||
const itemClassName = {
|
||||
[`${prefixCls}-item`]: true,
|
||||
[`${prefixCls}-item-with-help`]: domErrorVisible || help,
|
||||
@ -190,8 +186,6 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
[`${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 && prevValidateStatusRef.current === 'error',
|
||||
[`${prefixCls}-item-is-validating`]: mergedValidateStatus === 'validating',
|
||||
[`${prefixCls}-item-hidden`]: hidden,
|
||||
};
|
||||
@ -239,6 +233,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
{...meta}
|
||||
errors={mergedErrors}
|
||||
prefixCls={prefixCls}
|
||||
status={mergedValidateStatus}
|
||||
onDomErrorVisibleChange={setDomErrorVisible}
|
||||
validateStatus={mergedValidateStatus}
|
||||
>
|
||||
@ -253,7 +248,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
const isRenderProps = typeof children === 'function';
|
||||
|
||||
// Record for real component render
|
||||
const updateRef = React.useRef(0);
|
||||
const updateRef = useRef(0);
|
||||
updateRef.current += 1;
|
||||
|
||||
if (!hasName && !isRenderProps && !dependencies) {
|
||||
|
@ -4,14 +4,11 @@ import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
|
||||
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
|
||||
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
|
||||
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
|
||||
import useMemo from 'rc-util/lib/hooks/useMemo';
|
||||
import CSSMotion from 'rc-motion';
|
||||
|
||||
import Col, { ColProps } from '../grid/col';
|
||||
import { ValidateStatus } from './FormItem';
|
||||
import { FormContext } from './context';
|
||||
import useCacheErrors from './hooks/useCacheErrors';
|
||||
import useForceUpdate from '../_util/hooks/useForceUpdate';
|
||||
import { FormContext, FormItemPrefixContext } from './context';
|
||||
import ErrorList from './ErrorList';
|
||||
|
||||
interface FormItemInputMiscProps {
|
||||
prefixCls: string;
|
||||
@ -26,6 +23,7 @@ export interface FormItemInputProps {
|
||||
wrapperCol?: ColProps;
|
||||
help?: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
status?: ValidateStatus;
|
||||
}
|
||||
|
||||
const iconMap: { [key: string]: any } = {
|
||||
@ -37,6 +35,7 @@ const iconMap: { [key: string]: any } = {
|
||||
|
||||
const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
|
||||
prefixCls,
|
||||
status,
|
||||
wrapperCol,
|
||||
children,
|
||||
help,
|
||||
@ -46,8 +45,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
|
||||
validateStatus,
|
||||
extra,
|
||||
}) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const baseClassName = `${prefixCls}-item`;
|
||||
|
||||
const formContext = React.useContext(FormContext);
|
||||
@ -56,24 +53,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
|
||||
|
||||
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className);
|
||||
|
||||
const [visible, cacheErrors] = useCacheErrors(
|
||||
errors,
|
||||
changedVisible => {
|
||||
if (changedVisible) {
|
||||
/**
|
||||
* We trigger in sync to avoid dom shaking but this get warning in react 16.13.
|
||||
* So use Promise to keep in micro async to handle this.
|
||||
* https://github.com/ant-design/ant-design/issues/21698#issuecomment-593743485
|
||||
*/
|
||||
Promise.resolve().then(() => {
|
||||
onDomErrorVisibleChange(true);
|
||||
});
|
||||
}
|
||||
forceUpdate();
|
||||
},
|
||||
!!help,
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
onDomErrorVisibleChange(false);
|
||||
@ -81,12 +60,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const memoErrors = useMemo(
|
||||
() => cacheErrors,
|
||||
visible,
|
||||
(_, nextVisible) => nextVisible,
|
||||
);
|
||||
|
||||
// Should provides additional icon if `hasFeedback`
|
||||
const IconNode = validateStatus && iconMap[validateStatus];
|
||||
const icon =
|
||||
@ -108,29 +81,13 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
|
||||
<div className={`${baseClassName}-control-input-content`}>{children}</div>
|
||||
{icon}
|
||||
</div>
|
||||
<CSSMotion
|
||||
motionDeadline={500}
|
||||
visible={visible}
|
||||
motionName="show-help"
|
||||
onLeaveEnd={() => {
|
||||
onDomErrorVisibleChange(false);
|
||||
}}
|
||||
motionAppear
|
||||
removeOnLeave
|
||||
>
|
||||
{({ className: motionClassName }: { className: string }) => {
|
||||
return (
|
||||
<div className={classNames(`${baseClassName}-explain`, motionClassName)} key="help">
|
||||
{memoErrors.map((error, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={index} role="alert">
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</CSSMotion>
|
||||
<FormItemPrefixContext.Provider value={{ prefixCls, status }}>
|
||||
<ErrorList
|
||||
errors={errors}
|
||||
help={help}
|
||||
onDomErrorVisibleChange={onDomErrorVisibleChange}
|
||||
/>
|
||||
</FormItemPrefixContext.Provider>
|
||||
{extra && <div className={`${baseClassName}-extra`}>{extra}</div>}
|
||||
</Col>
|
||||
</FormContext.Provider>
|
||||
|
@ -2,6 +2,8 @@ import * as React from 'react';
|
||||
import { List } from 'rc-field-form';
|
||||
import { StoreValue } from 'rc-field-form/lib/interface';
|
||||
import devWarning from '../_util/devWarning';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import { FormItemPrefixContext } from './context';
|
||||
|
||||
export interface FormListFieldData {
|
||||
name: number;
|
||||
@ -16,19 +18,38 @@ export interface FormListOperation {
|
||||
}
|
||||
|
||||
export interface FormListProps {
|
||||
prefixCls?: string;
|
||||
name: string | number | (string | number)[];
|
||||
children: (fields: FormListFieldData[], operation: FormListOperation) => React.ReactNode;
|
||||
children: (
|
||||
fields: FormListFieldData[],
|
||||
operation: FormListOperation,
|
||||
meta: { errors: React.ReactNode[] },
|
||||
) => React.ReactNode;
|
||||
}
|
||||
|
||||
const FormList: React.FC<FormListProps> = ({ children, ...props }) => {
|
||||
const FormList: React.FC<FormListProps> = ({
|
||||
prefixCls: customizePrefixCls,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
devWarning(!!props.name, 'Form.List', 'Miss `name` prop.');
|
||||
|
||||
const { getPrefixCls } = React.useContext(ConfigContext);
|
||||
const prefixCls = getPrefixCls('form', customizePrefixCls);
|
||||
|
||||
return (
|
||||
<List {...props}>
|
||||
{(fields, operation) => {
|
||||
return children(
|
||||
{(fields, operation, meta) => {
|
||||
return (
|
||||
<FormItemPrefixContext.Provider value={{ prefixCls, status: 'error' }}>
|
||||
{children(
|
||||
fields.map(field => ({ ...field, fieldKey: field.key })),
|
||||
operation,
|
||||
{
|
||||
errors: meta.errors,
|
||||
},
|
||||
)}
|
||||
</FormItemPrefixContext.Provider>
|
||||
);
|
||||
}}
|
||||
</List>
|
||||
|
@ -1078,7 +1078,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -1120,7 +1120,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -1193,7 +1193,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -1235,7 +1235,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -1334,7 +1334,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -1389,7 +1389,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -1480,7 +1480,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -1531,7 +1531,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -6229,7 +6229,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -6356,7 +6356,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-validating"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -6536,7 +6536,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -6924,7 +6924,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-validating"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
@ -7013,7 +7013,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain"
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
|
@ -164,4 +164,76 @@ describe('Form.List', () => {
|
||||
await sleep();
|
||||
expect(onFinish).toHaveBeenLastCalledWith({ list: ['input2', 'input3'] });
|
||||
});
|
||||
|
||||
it('list errors', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
let operation;
|
||||
const wrapper = mount(
|
||||
<Form>
|
||||
<Form.List
|
||||
name="list"
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, value) => {
|
||||
if (value.length < 2) {
|
||||
return Promise.reject(new Error('At least 2'));
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(_, opt, { errors }) => {
|
||||
operation = opt;
|
||||
return <Form.ErrorList errors={errors} />;
|
||||
}}
|
||||
</Form.List>
|
||||
</Form>,
|
||||
);
|
||||
|
||||
async function addItem() {
|
||||
await act(async () => {
|
||||
operation.add();
|
||||
await sleep();
|
||||
jest.runAllTimers();
|
||||
wrapper.update();
|
||||
});
|
||||
}
|
||||
|
||||
await addItem();
|
||||
expect(wrapper.find('.ant-form-item-explain div').text()).toEqual('At least 2');
|
||||
|
||||
await addItem();
|
||||
expect(wrapper.find('.ant-form-item-explain div')).toHaveLength(0);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('ErrorList component', () => {
|
||||
it('should trigger onDomErrorVisibleChange by motion end', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const onDomErrorVisibleChange = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Form.ErrorList
|
||||
errors={['bamboo is light']}
|
||||
onDomErrorVisibleChange={onDomErrorVisibleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await sleep();
|
||||
jest.runAllTimers();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
wrapper.find('CSSMotion').props().onLeaveEnd();
|
||||
});
|
||||
|
||||
expect(onDomErrorVisibleChange).toHaveBeenCalledWith(false);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -5,6 +5,7 @@ import { FormProviderProps as RcFormProviderProps } from 'rc-field-form/lib/Form
|
||||
import { ColProps } from '../grid/col';
|
||||
import { FormLabelAlign } from './interface';
|
||||
import { RequiredMark } from './Form';
|
||||
import { ValidateStatus } from './FormItem';
|
||||
|
||||
/**
|
||||
* Form Context
|
||||
@ -49,3 +50,15 @@ export const FormProvider: React.FC<FormProviderProps> = props => {
|
||||
const providerProps = omit(props, ['prefixCls']);
|
||||
return <RcFormProvider {...providerProps} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used for ErrorList only
|
||||
*/
|
||||
export interface FormItemPrefixContextProps {
|
||||
prefixCls: string;
|
||||
status?: ValidateStatus;
|
||||
}
|
||||
|
||||
export const FormItemPrefixContext = React.createContext<FormItemPrefixContextProps>({
|
||||
prefixCls: '',
|
||||
});
|
||||
|
@ -41,8 +41,19 @@ const DynamicFieldSet = () => {
|
||||
|
||||
return (
|
||||
<Form name="dynamic_form_item" {...formItemLayoutWithOutLabel} onFinish={onFinish}>
|
||||
<Form.List name="names">
|
||||
{(fields, { add, remove }) => {
|
||||
<Form.List
|
||||
name="names"
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, names) => {
|
||||
if (!names || names.length < 2) {
|
||||
return Promise.reject(new Error('At least 2 passengers'));
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(fields, { add, remove }, { errors }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
@ -96,6 +107,8 @@ const DynamicFieldSet = () => {
|
||||
>
|
||||
<PlusOutlined /> Add field at head
|
||||
</Button>
|
||||
|
||||
<Form.ErrorList errors={errors} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
|
@ -165,10 +165,11 @@ You can modify the default verification information of Form.Item through `messag
|
||||
|
||||
Provides array management for fields.
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| name | Field name, support array | [NamePath](#NamePath) | - |
|
||||
| children | Render function | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - |
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| name | Field name, support array | [NamePath](#NamePath) | - | |
|
||||
| children | Render function | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - | |
|
||||
| rules | Validate rules, only support customize validator. Should work with [ErrorList](#Form.ErrorList) | { validator, message }[] | - | 4.7.0 |
|
||||
|
||||
```tsx
|
||||
<Form.List>
|
||||
@ -194,6 +195,14 @@ Some operator functions in render form of Form.List.
|
||||
| remove | remove form item | (index: number \| number[]) => void | number[]: 4.5.0 |
|
||||
| move | move form item | (from: number, to: number) => void | - |
|
||||
|
||||
## Form.ErrorList
|
||||
|
||||
New in 4.7.0. Show error messages, should only work with `rules` of Form.List.
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| -------- | ----------- | ----------- | ------- |
|
||||
| errors | Error list | ReactNode[] | - |
|
||||
|
||||
## Form.Provider
|
||||
|
||||
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).
|
||||
@ -379,6 +388,10 @@ Validating is also part of the value updating. It pass follow steps:
|
||||
|
||||
In each `onFieldsChange`, you will get `false` > `true` > `false` with `isFieldValidating`.
|
||||
|
||||
### Why Form.List do not support `label` and need ErrorList to show errors?
|
||||
|
||||
Form.List use renderProps which mean internal structure is flexible. Thus `label` and `error` can not have best place. If you want to use antd `label`, you can wrap with Form.Item instead.
|
||||
|
||||
<style>
|
||||
.site-form-item-icon {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Rule, RuleObject, RuleRender } from 'rc-field-form/lib/interface';
|
||||
import InternalForm, { useForm, FormInstance, FormProps } from './Form';
|
||||
import Item, { FormItemProps } from './FormItem';
|
||||
import ErrorList, { ErrorListProps } from './ErrorList';
|
||||
import List from './FormList';
|
||||
import { FormProvider } from './context';
|
||||
import devWarning from '../_util/devWarning';
|
||||
@ -10,6 +11,7 @@ interface Form extends InternalForm {
|
||||
useForm: typeof useForm;
|
||||
Item: typeof Item;
|
||||
List: typeof List;
|
||||
ErrorList: typeof ErrorList;
|
||||
Provider: typeof FormProvider;
|
||||
|
||||
/** @deprecated Only for warning usage. Do not use. */
|
||||
@ -20,6 +22,7 @@ const Form: Form = InternalForm as Form;
|
||||
|
||||
Form.Item = Item;
|
||||
Form.List = List;
|
||||
Form.ErrorList = ErrorList;
|
||||
Form.useForm = useForm;
|
||||
Form.Provider = FormProvider;
|
||||
Form.create = () => {
|
||||
@ -30,6 +33,6 @@ Form.create = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export { FormInstance, FormProps, FormItemProps, Rule, RuleObject, RuleRender };
|
||||
export { FormInstance, FormProps, FormItemProps, ErrorListProps, Rule, RuleObject, RuleRender };
|
||||
|
||||
export default Form;
|
||||
|
@ -166,10 +166,11 @@ Form 通过增量更新方式,只更新被修改的字段相关组件以达到
|
||||
|
||||
为字段提供数组化管理。
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| name | 字段名,支持数组 | [NamePath](#NamePath) | - |
|
||||
| children | 渲染函数 | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - |
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| name | 字段名,支持数组 | [NamePath](#NamePath) | - | |
|
||||
| children | 渲染函数 | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - | |
|
||||
| rules | 校验规则,仅支持自定义规则。需要配合 [ErrorList](#Form.ErrorList) 一同使用。 | { validator, message }[] | - | 4.7.0 |
|
||||
|
||||
```tsx
|
||||
<Form.List>
|
||||
@ -196,6 +197,14 @@ Form.List 渲染表单相关操作函数。
|
||||
| remove | 删除表单项 | (index: number \| number[]) => void | number[]: 4.5.0 |
|
||||
| move | 移动表单项 | (from: number, to: number) => void | - |
|
||||
|
||||
## Form.ErrorList
|
||||
|
||||
4.7.0 新增。错误展示组件,仅限配合 Form.List 的 rules 一同使用。
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| ------ | -------- | ----------- | ------ |
|
||||
| errors | 错误列表 | ReactNode[] | - |
|
||||
|
||||
## Form.Provider
|
||||
|
||||
提供表单间联动功能,其下设置 `name` 的 Form 更新时,会自动触发对应事件。查看[示例](#components-form-demo-form-context)。
|
||||
@ -381,6 +390,10 @@ validator(rule, value, callback) => {
|
||||
|
||||
在触发过程中,调用 `isFieldValidating` 会经历 `false` > `true` > `false` 的变化过程。
|
||||
|
||||
### 为什么 Form.List 不支持 `label` 还需要使用 ErrorList 展示错误?
|
||||
|
||||
Form.List 本身是 renderProps,内部样式非常自由。因而默认配置 `label` 和 `error` 节点很难与之配合。如果你需要 antd 样式的 `label`,可以通过外部包裹 Form.Item 来实现。
|
||||
|
||||
<style>
|
||||
.site-form-item-icon {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
|
@ -1,7 +1,6 @@
|
||||
@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;
|
||||
}
|
||||
|
@ -6,6 +6,18 @@
|
||||
// ================================================================
|
||||
/* Some non-status related component style is in `components.less` */
|
||||
|
||||
// ========================= Explain =========================
|
||||
/* To support leave along ErrorList. We add additional className to handle explain style */
|
||||
&-explain {
|
||||
&&-error {
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
&&-warning {
|
||||
color: @warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-has-feedback {
|
||||
// ========================= Input =========================
|
||||
.@{ant-prefix}-input {
|
||||
@ -234,13 +246,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
@ -124,7 +124,7 @@
|
||||
"rc-dialog": "~8.3.0",
|
||||
"rc-drawer": "~4.1.0",
|
||||
"rc-dropdown": "~3.1.2",
|
||||
"rc-field-form": "~1.10.0",
|
||||
"rc-field-form": "~1.11.0",
|
||||
"rc-image": "~3.0.6",
|
||||
"rc-input-number": "~6.0.0",
|
||||
"rc-mentions": "~1.4.0",
|
||||
|
Loading…
Reference in New Issue
Block a user