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>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="config-form-item-explain"
|
class="config-form-item-explain config-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -14227,7 +14227,7 @@ exports[`ConfigProvider components Form configProvider componentSize large 1`] =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="config-form-item-explain"
|
class="config-form-item-explain config-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -14264,7 +14264,7 @@ exports[`ConfigProvider components Form configProvider componentSize middle 1`]
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="config-form-item-explain"
|
class="config-form-item-explain config-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -14301,7 +14301,7 @@ exports[`ConfigProvider components Form configProvider virtual and dropdownMatch
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -14338,7 +14338,7 @@ exports[`ConfigProvider components Form normal 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -14375,7 +14375,7 @@ exports[`ConfigProvider components Form prefixCls 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="prefix-Form-item-explain"
|
class="prefix-Form-item-explain prefix-Form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
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 * as React from 'react';
|
||||||
|
import { useContext, useRef } from 'react';
|
||||||
import isEqual from 'lodash/isEqual';
|
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';
|
||||||
@ -86,15 +87,14 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
|||||||
hidden,
|
hidden,
|
||||||
...restProps
|
...restProps
|
||||||
} = props;
|
} = props;
|
||||||
const destroyRef = React.useRef(false);
|
const destroyRef = useRef(false);
|
||||||
const { getPrefixCls } = React.useContext(ConfigContext);
|
const { getPrefixCls } = useContext(ConfigContext);
|
||||||
const { name: formName, requiredMark } = React.useContext(FormContext);
|
const { name: formName, requiredMark } = useContext(FormContext);
|
||||||
const { updateItemErrors } = React.useContext(FormItemContext);
|
const { updateItemErrors } = useContext(FormItemContext);
|
||||||
const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help);
|
const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help);
|
||||||
const prevValidateStatusRef = React.useRef<ValidateStatus | undefined>(validateStatus);
|
|
||||||
const [inlineErrors, setInlineErrors] = useFrameState<Record<string, string[]>>({});
|
const [inlineErrors, setInlineErrors] = useFrameState<Record<string, string[]>>({});
|
||||||
|
|
||||||
const { validateTrigger: contextValidateTrigger } = React.useContext(FieldContext);
|
const { validateTrigger: contextValidateTrigger } = useContext(FieldContext);
|
||||||
const mergedValidateTrigger =
|
const mergedValidateTrigger =
|
||||||
validateTrigger !== undefined ? validateTrigger : contextValidateTrigger;
|
validateTrigger !== undefined ? validateTrigger : contextValidateTrigger;
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
|||||||
const hasName = hasValidName(name);
|
const hasName = hasValidName(name);
|
||||||
|
|
||||||
// Cache Field NamePath
|
// Cache Field NamePath
|
||||||
const nameRef = React.useRef<(string | number)[]>([]);
|
const nameRef = useRef<(string | number)[]>([]);
|
||||||
|
|
||||||
// Should clean up if Field removed
|
// Should clean up if Field removed
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -176,10 +176,6 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
|||||||
mergedValidateStatus = 'success';
|
mergedValidateStatus = 'success';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domErrorVisible && help) {
|
|
||||||
prevValidateStatusRef.current = mergedValidateStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemClassName = {
|
const itemClassName = {
|
||||||
[`${prefixCls}-item`]: true,
|
[`${prefixCls}-item`]: true,
|
||||||
[`${prefixCls}-item-with-help`]: domErrorVisible || help,
|
[`${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-success`]: mergedValidateStatus === 'success',
|
||||||
[`${prefixCls}-item-has-warning`]: mergedValidateStatus === 'warning',
|
[`${prefixCls}-item-has-warning`]: mergedValidateStatus === 'warning',
|
||||||
[`${prefixCls}-item-has-error`]: mergedValidateStatus === 'error',
|
[`${prefixCls}-item-has-error`]: mergedValidateStatus === 'error',
|
||||||
[`${prefixCls}-item-has-error-leave`]:
|
|
||||||
!help && domErrorVisible && prevValidateStatusRef.current === 'error',
|
|
||||||
[`${prefixCls}-item-is-validating`]: mergedValidateStatus === 'validating',
|
[`${prefixCls}-item-is-validating`]: mergedValidateStatus === 'validating',
|
||||||
[`${prefixCls}-item-hidden`]: hidden,
|
[`${prefixCls}-item-hidden`]: hidden,
|
||||||
};
|
};
|
||||||
@ -239,6 +233,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
|||||||
{...meta}
|
{...meta}
|
||||||
errors={mergedErrors}
|
errors={mergedErrors}
|
||||||
prefixCls={prefixCls}
|
prefixCls={prefixCls}
|
||||||
|
status={mergedValidateStatus}
|
||||||
onDomErrorVisibleChange={setDomErrorVisible}
|
onDomErrorVisibleChange={setDomErrorVisible}
|
||||||
validateStatus={mergedValidateStatus}
|
validateStatus={mergedValidateStatus}
|
||||||
>
|
>
|
||||||
@ -253,7 +248,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
|||||||
const isRenderProps = typeof children === 'function';
|
const isRenderProps = typeof children === 'function';
|
||||||
|
|
||||||
// Record for real component render
|
// Record for real component render
|
||||||
const updateRef = React.useRef(0);
|
const updateRef = useRef(0);
|
||||||
updateRef.current += 1;
|
updateRef.current += 1;
|
||||||
|
|
||||||
if (!hasName && !isRenderProps && !dependencies) {
|
if (!hasName && !isRenderProps && !dependencies) {
|
||||||
|
@ -4,14 +4,11 @@ import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
|
|||||||
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
|
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
|
||||||
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
|
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
|
||||||
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
|
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 Col, { ColProps } from '../grid/col';
|
||||||
import { ValidateStatus } from './FormItem';
|
import { ValidateStatus } from './FormItem';
|
||||||
import { FormContext } from './context';
|
import { FormContext, FormItemPrefixContext } from './context';
|
||||||
import useCacheErrors from './hooks/useCacheErrors';
|
import ErrorList from './ErrorList';
|
||||||
import useForceUpdate from '../_util/hooks/useForceUpdate';
|
|
||||||
|
|
||||||
interface FormItemInputMiscProps {
|
interface FormItemInputMiscProps {
|
||||||
prefixCls: string;
|
prefixCls: string;
|
||||||
@ -26,6 +23,7 @@ export interface FormItemInputProps {
|
|||||||
wrapperCol?: ColProps;
|
wrapperCol?: ColProps;
|
||||||
help?: React.ReactNode;
|
help?: React.ReactNode;
|
||||||
extra?: React.ReactNode;
|
extra?: React.ReactNode;
|
||||||
|
status?: ValidateStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconMap: { [key: string]: any } = {
|
const iconMap: { [key: string]: any } = {
|
||||||
@ -37,6 +35,7 @@ const iconMap: { [key: string]: any } = {
|
|||||||
|
|
||||||
const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
|
const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
|
||||||
prefixCls,
|
prefixCls,
|
||||||
|
status,
|
||||||
wrapperCol,
|
wrapperCol,
|
||||||
children,
|
children,
|
||||||
help,
|
help,
|
||||||
@ -46,8 +45,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
|
|||||||
validateStatus,
|
validateStatus,
|
||||||
extra,
|
extra,
|
||||||
}) => {
|
}) => {
|
||||||
const forceUpdate = useForceUpdate();
|
|
||||||
|
|
||||||
const baseClassName = `${prefixCls}-item`;
|
const baseClassName = `${prefixCls}-item`;
|
||||||
|
|
||||||
const formContext = React.useContext(FormContext);
|
const formContext = React.useContext(FormContext);
|
||||||
@ -56,24 +53,6 @@ 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 => {
|
|
||||||
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(
|
React.useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
onDomErrorVisibleChange(false);
|
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`
|
// Should provides additional icon if `hasFeedback`
|
||||||
const IconNode = validateStatus && iconMap[validateStatus];
|
const IconNode = validateStatus && iconMap[validateStatus];
|
||||||
const icon =
|
const icon =
|
||||||
@ -108,29 +81,13 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = ({
|
|||||||
<div className={`${baseClassName}-control-input-content`}>{children}</div>
|
<div className={`${baseClassName}-control-input-content`}>{children}</div>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<CSSMotion
|
<FormItemPrefixContext.Provider value={{ prefixCls, status }}>
|
||||||
motionDeadline={500}
|
<ErrorList
|
||||||
visible={visible}
|
errors={errors}
|
||||||
motionName="show-help"
|
help={help}
|
||||||
onLeaveEnd={() => {
|
onDomErrorVisibleChange={onDomErrorVisibleChange}
|
||||||
onDomErrorVisibleChange(false);
|
/>
|
||||||
}}
|
</FormItemPrefixContext.Provider>
|
||||||
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>
|
|
||||||
{extra && <div className={`${baseClassName}-extra`}>{extra}</div>}
|
{extra && <div className={`${baseClassName}-extra`}>{extra}</div>}
|
||||||
</Col>
|
</Col>
|
||||||
</FormContext.Provider>
|
</FormContext.Provider>
|
||||||
|
@ -2,6 +2,8 @@ import * as React from 'react';
|
|||||||
import { List } from 'rc-field-form';
|
import { List } from 'rc-field-form';
|
||||||
import { StoreValue } from 'rc-field-form/lib/interface';
|
import { StoreValue } from 'rc-field-form/lib/interface';
|
||||||
import devWarning from '../_util/devWarning';
|
import devWarning from '../_util/devWarning';
|
||||||
|
import { ConfigContext } from '../config-provider';
|
||||||
|
import { FormItemPrefixContext } from './context';
|
||||||
|
|
||||||
export interface FormListFieldData {
|
export interface FormListFieldData {
|
||||||
name: number;
|
name: number;
|
||||||
@ -16,19 +18,38 @@ export interface FormListOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FormListProps {
|
export interface FormListProps {
|
||||||
|
prefixCls?: string;
|
||||||
name: string | number | (string | number)[];
|
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.');
|
devWarning(!!props.name, 'Form.List', 'Miss `name` prop.');
|
||||||
|
|
||||||
|
const { getPrefixCls } = React.useContext(ConfigContext);
|
||||||
|
const prefixCls = getPrefixCls('form', customizePrefixCls);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List {...props}>
|
<List {...props}>
|
||||||
{(fields, operation) => {
|
{(fields, operation, meta) => {
|
||||||
return children(
|
return (
|
||||||
fields.map(field => ({ ...field, fieldKey: field.key })),
|
<FormItemPrefixContext.Provider value={{ prefixCls, status: 'error' }}>
|
||||||
operation,
|
{children(
|
||||||
|
fields.map(field => ({ ...field, fieldKey: field.key })),
|
||||||
|
operation,
|
||||||
|
{
|
||||||
|
errors: meta.errors,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</FormItemPrefixContext.Provider>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</List>
|
</List>
|
||||||
|
@ -1078,7 +1078,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -1120,7 +1120,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -1193,7 +1193,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -1235,7 +1235,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -1334,7 +1334,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -1389,7 +1389,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -1480,7 +1480,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -1531,7 +1531,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -6229,7 +6229,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -6356,7 +6356,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-validating"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -6536,7 +6536,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -6924,7 +6924,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-validating"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@ -7013,7 +7013,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ant-form-item-explain"
|
class="ant-form-item-explain ant-form-item-explain-error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
|
@ -164,4 +164,76 @@ describe('Form.List', () => {
|
|||||||
await sleep();
|
await sleep();
|
||||||
expect(onFinish).toHaveBeenLastCalledWith({ list: ['input2', 'input3'] });
|
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 { ColProps } from '../grid/col';
|
||||||
import { FormLabelAlign } from './interface';
|
import { FormLabelAlign } from './interface';
|
||||||
import { RequiredMark } from './Form';
|
import { RequiredMark } from './Form';
|
||||||
|
import { ValidateStatus } from './FormItem';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form Context
|
* Form Context
|
||||||
@ -49,3 +50,15 @@ export const FormProvider: React.FC<FormProviderProps> = props => {
|
|||||||
const providerProps = omit(props, ['prefixCls']);
|
const providerProps = omit(props, ['prefixCls']);
|
||||||
return <RcFormProvider {...providerProps} />;
|
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 (
|
return (
|
||||||
<Form name="dynamic_form_item" {...formItemLayoutWithOutLabel} onFinish={onFinish}>
|
<Form name="dynamic_form_item" {...formItemLayoutWithOutLabel} onFinish={onFinish}>
|
||||||
<Form.List name="names">
|
<Form.List
|
||||||
{(fields, { add, remove }) => {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
@ -96,6 +107,8 @@ const DynamicFieldSet = () => {
|
|||||||
>
|
>
|
||||||
<PlusOutlined /> Add field at head
|
<PlusOutlined /> Add field at head
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Form.ErrorList errors={errors} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -165,10 +165,11 @@ You can modify the default verification information of Form.Item through `messag
|
|||||||
|
|
||||||
Provides array management for fields.
|
Provides array management for fields.
|
||||||
|
|
||||||
| Property | Description | Type | Default |
|
| Property | Description | Type | Default | Version |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| name | Field name, support array | [NamePath](#NamePath) | - |
|
| name | Field name, support array | [NamePath](#NamePath) | - | |
|
||||||
| children | Render function | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - |
|
| 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
|
```tsx
|
||||||
<Form.List>
|
<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 |
|
| remove | remove form item | (index: number \| number[]) => void | number[]: 4.5.0 |
|
||||||
| move | move form item | (from: number, to: number) => void | - |
|
| 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
|
## 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).
|
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`.
|
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>
|
<style>
|
||||||
.site-form-item-icon {
|
.site-form-item-icon {
|
||||||
color: rgba(0, 0, 0, 0.25);
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Rule, RuleObject, RuleRender } from 'rc-field-form/lib/interface';
|
import { Rule, RuleObject, RuleRender } from 'rc-field-form/lib/interface';
|
||||||
import InternalForm, { useForm, FormInstance, FormProps } from './Form';
|
import InternalForm, { useForm, FormInstance, FormProps } from './Form';
|
||||||
import Item, { FormItemProps } from './FormItem';
|
import Item, { FormItemProps } from './FormItem';
|
||||||
|
import ErrorList, { ErrorListProps } from './ErrorList';
|
||||||
import List from './FormList';
|
import List from './FormList';
|
||||||
import { FormProvider } from './context';
|
import { FormProvider } from './context';
|
||||||
import devWarning from '../_util/devWarning';
|
import devWarning from '../_util/devWarning';
|
||||||
@ -10,6 +11,7 @@ interface Form extends InternalForm {
|
|||||||
useForm: typeof useForm;
|
useForm: typeof useForm;
|
||||||
Item: typeof Item;
|
Item: typeof Item;
|
||||||
List: typeof List;
|
List: typeof List;
|
||||||
|
ErrorList: typeof ErrorList;
|
||||||
Provider: typeof FormProvider;
|
Provider: typeof FormProvider;
|
||||||
|
|
||||||
/** @deprecated Only for warning usage. Do not use. */
|
/** @deprecated Only for warning usage. Do not use. */
|
||||||
@ -20,6 +22,7 @@ const Form: Form = InternalForm as Form;
|
|||||||
|
|
||||||
Form.Item = Item;
|
Form.Item = Item;
|
||||||
Form.List = List;
|
Form.List = List;
|
||||||
|
Form.ErrorList = ErrorList;
|
||||||
Form.useForm = useForm;
|
Form.useForm = useForm;
|
||||||
Form.Provider = FormProvider;
|
Form.Provider = FormProvider;
|
||||||
Form.create = () => {
|
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;
|
export default Form;
|
||||||
|
@ -166,10 +166,11 @@ Form 通过增量更新方式,只更新被修改的字段相关组件以达到
|
|||||||
|
|
||||||
为字段提供数组化管理。
|
为字段提供数组化管理。
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 默认值 |
|
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| name | 字段名,支持数组 | [NamePath](#NamePath) | - |
|
| name | 字段名,支持数组 | [NamePath](#NamePath) | - | |
|
||||||
| children | 渲染函数 | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - |
|
| children | 渲染函数 | (fields: Field[], operation: { add, remove, move }) => React.ReactNode | - | |
|
||||||
|
| rules | 校验规则,仅支持自定义规则。需要配合 [ErrorList](#Form.ErrorList) 一同使用。 | { validator, message }[] | - | 4.7.0 |
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<Form.List>
|
<Form.List>
|
||||||
@ -196,6 +197,14 @@ Form.List 渲染表单相关操作函数。
|
|||||||
| remove | 删除表单项 | (index: number \| number[]) => void | number[]: 4.5.0 |
|
| remove | 删除表单项 | (index: number \| number[]) => void | number[]: 4.5.0 |
|
||||||
| move | 移动表单项 | (from: number, to: number) => void | - |
|
| move | 移动表单项 | (from: number, to: number) => void | - |
|
||||||
|
|
||||||
|
## Form.ErrorList
|
||||||
|
|
||||||
|
4.7.0 新增。错误展示组件,仅限配合 Form.List 的 rules 一同使用。
|
||||||
|
|
||||||
|
| 参数 | 说明 | 类型 | 默认值 |
|
||||||
|
| ------ | -------- | ----------- | ------ |
|
||||||
|
| errors | 错误列表 | ReactNode[] | - |
|
||||||
|
|
||||||
## Form.Provider
|
## Form.Provider
|
||||||
|
|
||||||
提供表单间联动功能,其下设置 `name` 的 Form 更新时,会自动触发对应事件。查看[示例](#components-form-demo-form-context)。
|
提供表单间联动功能,其下设置 `name` 的 Form 更新时,会自动触发对应事件。查看[示例](#components-form-demo-form-context)。
|
||||||
@ -381,6 +390,10 @@ validator(rule, value, callback) => {
|
|||||||
|
|
||||||
在触发过程中,调用 `isFieldValidating` 会经历 `false` > `true` > `false` 的变化过程。
|
在触发过程中,调用 `isFieldValidating` 会经历 `false` > `true` > `false` 的变化过程。
|
||||||
|
|
||||||
|
### 为什么 Form.List 不支持 `label` 还需要使用 ErrorList 展示错误?
|
||||||
|
|
||||||
|
Form.List 本身是 renderProps,内部样式非常自由。因而默认配置 `label` 和 `error` 节点很难与之配合。如果你需要 antd 样式的 `label`,可以通过外部包裹 Form.Item 来实现。
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.site-form-item-icon {
|
.site-form-item-icon {
|
||||||
color: rgba(0, 0, 0, 0.25);
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
@import '../../input/style/mixin';
|
@import '../../input/style/mixin';
|
||||||
|
|
||||||
.form-control-validation(@text-color: @input-color; @border-color: @input-border-color; @background-color: @input-bg) {
|
.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 {
|
.@{ant-prefix}-form-item-split {
|
||||||
color: @text-color;
|
color: @text-color;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,18 @@
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
/* Some non-status related component style is in `components.less` */
|
/* 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 {
|
&-has-feedback {
|
||||||
// ========================= Input =========================
|
// ========================= Input =========================
|
||||||
.@{ant-prefix}-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 =======================
|
// ====================== Validating =======================
|
||||||
&-is-validating {
|
&-is-validating {
|
||||||
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
|
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
|
||||||
|
@ -124,7 +124,7 @@
|
|||||||
"rc-dialog": "~8.3.0",
|
"rc-dialog": "~8.3.0",
|
||||||
"rc-drawer": "~4.1.0",
|
"rc-drawer": "~4.1.0",
|
||||||
"rc-dropdown": "~3.1.2",
|
"rc-dropdown": "~3.1.2",
|
||||||
"rc-field-form": "~1.10.0",
|
"rc-field-form": "~1.11.0",
|
||||||
"rc-image": "~3.0.6",
|
"rc-image": "~3.0.6",
|
||||||
"rc-input-number": "~6.0.0",
|
"rc-input-number": "~6.0.0",
|
||||||
"rc-mentions": "~1.4.0",
|
"rc-mentions": "~1.4.0",
|
||||||
|
Loading…
Reference in New Issue
Block a user