ant-design/components/form/FormItem.tsx
zombieJ 4cdf37bedb
feat: New Form (#17327)
* init form

* first demo

* add normal login

* add style

* webit

* support nest errors

* beauti form errors

* use onReset

* modal demo

* add list demo

* match key of errors logic

* date demo

* customize component

* moving style

* add status style

* without form create

* add demos

* add inline style

* clean up legacy

* fix drawer demo

* mention

* fix edit-row

* editable table cell

* update mentions demo

* fix some test case

* fix upload test

* fix lint

* part of doc

* fix ts

* doc update

* rm react 15

* rm config

* enhance test coverage

* clean up

* fix FormItem context pass logic

* add more demo

* en to build

* update demo

* update demo & snapshot

* more doc

* update list doc

* update doc

* update demo to display condition render

* update snapshot

* add provider doc

* support configProvider

* more doc about validateMessages

* more description

* more and more doc

* fix typo

* en doc

* Form.List doc

* m v3 -> v4

* add skip
2019-07-03 20:14:39 +08:00

220 lines
7.5 KiB
TypeScript

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