ant-design/components/form/FormItem.tsx
陈帅 523b74e3b6
merge master into Feature (#25262)
* feat: add successColor for Progress (#24655)

* feat: add successColor for Progress

* feat: update

* fix: update test

* remove snap

* feat: add test case

* refactor success

* feat: adjust styyle

* feat: add DevWarning

* feat: Support rowSelection.dirty (#24718)

* feat: Support rowSelection.dirty

* rename to reserveKeys

* preserveKeys will keep record also

* to preserveSelectedRowKeys

* feat: add ghost prop for collapse (#24734)

* feat: add ghost prop for collapse

* doc: version of collapse's ghost prop

* refactor: make ghost collapse's less code to a nested style

* chore: remove redundant codes in ghost collapse's less & doc

* doc: add a background wrapper for ghost collapse demo

* doc: dark-theme wrapper bg-color for ghost collapse demo

* test: update snapshot of ghost collapse

* doc: use softer bg-color on ghost collapse demo

* doc: remove disabled panel in ghost collapse demo

* feat: form instance support getFieldInstance (#24711)

* support getFieldInstance

* update doc

* fix lint

* move func

* move into hooks

* update ref logic

* fix lint

* rm only

* fix docs

* feat: dropdown support arrow (#23869)

* feat: dropdown support arrow prop

close #22758

* test: update snapshot

* fix: fix dropdown cls names

* test: update snapshot

* test: update snapshot

* doc: update demo

* test: update demo snapshot

* demo

* fix: snapshot

* chore: change the style of ghost collapse & demo modified (#24762)

* refactor: reduce content padding in ghost collapse

* doc: remove the wrapper outside ghost collapse

Designer want the demo differs from other demos

* refactor: remove redundant .less code in collapse

* feat: cascader dropdown-render prop (#24812)

* feat: cascader dropdown-render prop

* fix: update Cascader dropdownRender type annotation

* fix: set rc-cascader semver from ^ to ~

* docs: fix coding style in cascader/custom-dropdown

* feat: 🆕 support Drawer closeIcon (#24842)

* feat: 🆕 support Drawer closeIcon

close #19283
close #19153

* add test case

* update docs

* feat: 🆕 Cascader expandIcon (#24865)

* feat: cascader expandIcon

* fix: snap

* refactor: reduce CSS size (#24846)

* refactor: reduce button css size

* refactor: remove redundant button .less code

* feat: add Table onChange an action param (#24697)

* Working on tests

* created TableAction type

* changed TableActions to tuple

* removed chinese documentation line

* refactor TableActions

* fix documentation

* Moved action into extra param

* minor doc change

* feat: add closeIcon customize tag close (#24885)

* feat: add closeIcon customize tag close

* docs fix

* update snap

* fix: css name

* update snapshot

* snapshot

* feat: add radio `optionType` api to set radio option type (#24809)

* feat: radio component

* docs: update md

* fix: snap

* test components

* fix: use optionType

* fix name

* add warning

* fix

* feat: expand rate character (#24903)

* feat: expand rate character

* fix: demo

* fix: snap

* Update components/rate/index.zh-CN.md

Co-authored-by: 偏右 <afc163@gmail.com>

* fix

Co-authored-by: 偏右 <afc163@gmail.com>

* Refactor demo code box actions (#24887)

* refactor: refine the styling of actions part of demo code-box

* fix: lint style

* refactor: move Result children to end (#24945)

* feat: remove content max-width on dot-step (#24907)

* feat: add Skeleton-Image (#24805)

* feat: add Skeleton-Image

* feat: add docs

* fix: adjust skeleton

* feat: adjust Image Component

* feat: rebase

* feat: adjust style

* fix: lint

* feat: remove size

* feat: delete md

* feat: fix style

*  feat: Mentions support autoSize (#24961)

close #17746

* chore: replace textarea with rc-textarea (#24966)

* feat: update pagination@2.3.0 support onChange called when pageSize change (#24964)

* feat: update pagination@2.5.0 and add test case to relative component

* fix: lint

* delete

* feat: add test case for pagination

* adjust test case

* feat: Implement centered prop in Tabs (#24958)

* Implement centered in Tabs along with its tests and docs

* Fix build error

* Add Chinese translations and remove test case

Co-authored-by: Ashkan Pourghasem <ashkan.pourghasem@gmail.com>

* feat: Add modal style parameter (#24773)

* add some paramters in default.less

* Update components/style/themes/default.less

Co-authored-by: Amumu <yoyo837@hotmail.com>

* change parameter in compact.less

Co-authored-by: Crystal Gao <jinggao@ebay.com>
Co-authored-by: Amumu <yoyo837@hotmail.com>

* feat: export Tabs addIcon (#25006)

* feat: export Tabs addIcon

* update snapshot

* feat: showNow on timepicker and datetimepicker (#25032)

* feat: update rc-picker@1.7.1 and fix icons of month and quarter picker in DatePicker Component (#25035)

* feat: update rc-picker@1.7.1

* delete

* add

* feat: expand rate support props (#24993)

* docs: 📝 Add Form.Item hidden in doc (#25108)

close #25101

* fix: ⌨️ Improve Pagination accessibility issue (#25119)

* ⌨️ Improve Pagination a11y by fixing a W3C error

https://github.com/react-component/pagination/issues/280

* update snapshot

* 🆙 rc-pagination to 2.4.1

* feat: support triggerSubMenuAction for <Menu /> (#25127)

* feat(menu): add triggerSubMenuAction for Menu

* feat(menu): test cases

* chore: Adjust picker logic (#25135)

* chore: update rc-picker 1.10.0 (#25174)

* feat: table row check strictly (#24931)

* feat: add checkStrictly on Table.rowSelection

* fix: LGTM warnings

* test: table rowSelection.checkStrictly

* test: add cov [wip]

* refactor: tree.rowSelection.checkStrictly [wip]

* test: table.rowSelection.checkStrictly basic case

* feat: support rowKey on checkStrictly table

* feat: Table checkStrictly support getCheckboxProps

* docs: Table checkStrictly

* chore: typo

* chore: remove useless comment

* chore: update snapshot

* chore: update snapshot

* fix: fire selectAll on selection dropdown menu & changeRows incorrect in selectAll callback

* docs: typo

* chore

* chore

* fix: expand buttons of leaf rows in tree data are not hidden

* feat: Table warning about rowKey index parameter

* perf: only generate keyEntities when not checkStrictly

* refactor: remove useless parseCheckedKeys

* refactor: get derived selected & half selected keys from selectedRowKeys

* chore: remove env condition stmt

* chore: revert index usage & code formatting

* chore: rerun ci

* docs: table tree-data checkstrictly

* test: update snapshots

* refactor: use useMergedState hook

* chore: rerun ci

* chore: rerun ci 2

* chore: revert selection select all behavior

* refactor: refactor code based on feature

* chore: revert table code format

* chore: revert table code format

* fix: useMemo deps

* fix: useMemo deps

* fix: useMemo deps

* feat: support preserve (#25186)

* docs: add responsibly order for Col (#25139)

* feat: add type

* feat: add responsibly order cols

* feat: add docs

* feat: add test case

* fix test

Co-authored-by: 二货机器人 <smith3816@gmail.com>
Co-authored-by: 偏右 <afc163@gmail.com>
Co-authored-by: zoomdong <1344492820@qq.com>
Co-authored-by: 07akioni <07akioni2@gmail.com>
Co-authored-by: wendellhu <wendellhu95@gmail.com>
Co-authored-by: xrkffgg <xrkffgg@gmail.com>
Co-authored-by: Neto Braghetto <netow93@gmail.com>
Co-authored-by: Kermit Xuan <kermitlx@outlook.com>
Co-authored-by: Ashkan Pourghasem <64011067+ashkan-pm@users.noreply.github.com>
Co-authored-by: Ashkan Pourghasem <ashkan.pourghasem@gmail.com>
Co-authored-by: hicrystal <295247343@qq.com>
Co-authored-by: Crystal Gao <jinggao@ebay.com>
Co-authored-by: Amumu <yoyo837@hotmail.com>
Co-authored-by: Li Ming <armyiljfe@gmail.com>
2020-06-28 22:41:59 +08:00

377 lines
12 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 } from 'rc-field-form/lib/Field';
import FieldContext from 'rc-field-form/lib/FieldContext';
import { Meta, NamePath } from 'rc-field-form/lib/interface';
import { supportRef } from 'rc-util/lib/ref';
import omit from 'omit.js';
import Row from '../grid/row';
import { ConfigContext } from '../config-provider';
import { tuple } from '../_util/type';
import devWarning from '../_util/devWarning';
import FormItemLabel, { FormItemLabelProps } from './FormItemLabel';
import FormItemInput, { FormItemInputProps } from './FormItemInput';
import { FormContext, FormItemContext } from './context';
import { toArray, getFieldId } from './util';
import { cloneElement, isValidElement } from '../_util/reactNode';
import useFrameState from './hooks/useFrameState';
import useItemRef from './hooks/useItemRef';
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
export type ValidateStatus = typeof ValidateStatuses[number];
type RenderChildren = (form: FormInstance) => React.ReactNode;
type RcFieldProps = Omit<FieldProps, 'children'>;
type ChildrenType = RenderChildren | React.ReactNode;
interface MemoInputProps {
value: any;
update: number;
children: React.ReactNode;
}
const MemoInput = React.memo(
({ children }: MemoInputProps) => children as JSX.Element,
(prev, next) => {
return prev.value === next.value && prev.update === next.update;
},
);
export interface FormItemProps extends FormItemLabelProps, FormItemInputProps, RcFieldProps {
prefixCls?: string;
noStyle?: boolean;
style?: React.CSSProperties;
className?: string;
children?: ChildrenType;
id?: string;
hasFeedback?: boolean;
validateStatus?: ValidateStatus;
required?: boolean;
hidden?: boolean;
/** Auto passed by List render props. User should not use this. */
fieldKey?: React.Key | React.Key[];
}
function hasValidName(name?: NamePath): Boolean {
if (name === null) {
devWarning(false, 'Form.Item', '`null` is passed as `name` property');
}
return !(name === undefined || name === null);
}
function FormItem(props: FormItemProps): React.ReactElement {
const {
name,
fieldKey,
noStyle,
dependencies,
prefixCls: customizePrefixCls,
style,
className,
shouldUpdate,
hasFeedback,
help,
rules,
validateStatus,
children,
required,
label,
trigger = 'onChange',
validateTrigger,
hidden,
...restProps
} = props;
const destroyRef = React.useRef(false);
const { getPrefixCls } = React.useContext(ConfigContext);
const { name: formName } = React.useContext(FormContext);
const { updateItemErrors } = React.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 mergedValidateTrigger =
validateTrigger !== undefined ? validateTrigger : contextValidateTrigger;
function setDomErrorVisible(visible: boolean) {
if (!destroyRef.current) {
innerSetDomErrorVisible(visible);
}
}
const hasName = hasValidName(name);
// Cache Field NamePath
const nameRef = React.useRef<(string | number)[]>([]);
// Should clean up if Field removed
React.useEffect(() => {
return () => {
destroyRef.current = true;
updateItemErrors(nameRef.current.join('__SPLIT__'), []);
};
}, []);
const prefixCls = getPrefixCls('form', customizePrefixCls);
// ======================== Errors ========================
// Collect noStyle Field error to the top FormItem
const updateChildItemErrors = noStyle
? updateItemErrors
: (subName: string, subErrors: string[]) => {
if (!isEqual(inlineErrors[subName], subErrors)) {
setInlineErrors(prevInlineErrors => ({
...prevInlineErrors,
[subName]: subErrors,
}));
}
};
// ===================== Children Ref =====================
const getItemRef = useItemRef();
function renderLayout(
baseChildren: React.ReactNode,
fieldId?: string,
meta?: Meta,
isRequired?: boolean,
): React.ReactNode {
if (noStyle) {
return baseChildren;
}
// ======================== Errors ========================
let mergedErrors: React.ReactNode[];
if (help !== undefined && help !== null) {
mergedErrors = toArray(help);
} else {
mergedErrors = meta ? meta.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 && meta.validating) {
mergedValidateStatus = 'validating';
} else if (!help && mergedErrors.length) {
mergedValidateStatus = 'error';
} else if (meta && meta.touched) {
mergedValidateStatus = 'success';
}
if (domErrorVisible && help) {
prevValidateStatusRef.current = mergedValidateStatus;
}
const itemClassName = {
[`${prefixCls}-item`]: true,
[`${prefixCls}-item-with-help`]: domErrorVisible || help,
[`${className}`]: !!className,
// Status
[`${prefixCls}-item-has-feedback`]: mergedValidateStatus && hasFeedback,
[`${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}-hidden`]: hidden,
};
// ======================= Children =======================
return (
<Row
className={classNames(itemClassName)}
style={style}
key="row"
{...omit(restProps, [
'colon',
'extra',
'getValueFromEvent',
'getValueProps',
'hasFeedback',
'help',
'htmlFor',
'id', // It is deprecated because `htmlFor` is its replacement.
'initialValue',
'isListField',
'label',
'labelAlign',
'labelCol',
'normalize',
'required',
'validateFirst',
'validateStatus',
'valuePropName',
'wrapperCol',
])}
>
{/* Label */}
<FormItemLabel htmlFor={fieldId} required={isRequired} {...props} prefixCls={prefixCls} />
{/* Input Group */}
<FormItemInput
{...props}
{...meta}
errors={mergedErrors}
prefixCls={prefixCls}
onDomErrorVisibleChange={setDomErrorVisible}
validateStatus={mergedValidateStatus}
>
<FormItemContext.Provider value={{ updateItemErrors: updateChildItemErrors }}>
{baseChildren}
</FormItemContext.Provider>
</FormItemInput>
</Row>
);
}
const isRenderProps = typeof children === 'function';
// Record for real component render
const updateRef = React.useRef(0);
updateRef.current += 1;
if (!hasName && !isRenderProps && !dependencies) {
return renderLayout(children) as JSX.Element;
}
const variables: Record<string, string> = {};
if (typeof label === 'string') {
variables.label = label;
}
return (
<Field
{...props}
messageVariables={variables}
trigger={trigger}
validateTrigger={mergedValidateTrigger}
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) {
const fieldKeys = Array.isArray(fieldKey) ? fieldKey : [fieldKey];
nameRef.current = [...mergedName.slice(0, -1), ...fieldKeys];
}
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,
};
let childNode: React.ReactNode = null;
if (Array.isArray(children) && hasName) {
devWarning(false, 'Form.Item', '`children` is array of render props cannot have `name`.');
childNode = children;
} else if (isRenderProps && (!shouldUpdate || hasName)) {
devWarning(
!!shouldUpdate,
'Form.Item',
'`children` of render props only work with `shouldUpdate`.',
);
devWarning(
!hasName,
'Form.Item',
"Do not use `name` with `children` of render props since it's not a field.",
);
} else if (dependencies && !isRenderProps && !hasName) {
devWarning(
false,
'Form.Item',
'Must set `name` or use render props when `dependencies` is set.',
);
} else if (isValidElement(children)) {
devWarning(
children.props.defaultValue === undefined,
'Form.Item',
'`defaultValue` will not work on controlled Field. You should use `initialValues` of Form instead.',
);
const childProps = { ...children.props, ...mergedControl };
if (!childProps.id) {
childProps.id = fieldId;
}
if (supportRef(children)) {
childProps.ref = getItemRef(mergedName, children);
}
// We should keep user origin event handler
const triggers = new Set<string>([
...toArray(trigger),
...toArray(mergedValidateTrigger),
]);
triggers.forEach(eventName => {
childProps[eventName] = (...args: any[]) => {
mergedControl[eventName]?.(...args);
children.props[eventName]?.(...args);
};
});
childNode = (
<MemoInput
value={mergedControl[props.valuePropName || 'value']}
update={updateRef.current}
>
{cloneElement(children, childProps)}
</MemoInput>
);
} else if (isRenderProps && shouldUpdate && !hasName) {
childNode = (children as RenderChildren)(context);
} else {
devWarning(
!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;
}
return renderLayout(childNode, fieldId, meta, isRequired);
}}
</Field>
);
}
export default FormItem;