ant-design/components/form/FormItem.tsx

415 lines
12 KiB
TypeScript
Raw Normal View History

import * as React from 'react';
import * as ReactDOM from 'react-dom';
2018-08-07 21:07:52 +08:00
import * as PropTypes from 'prop-types';
import classNames from 'classnames';
2017-09-23 15:21:11 +08:00
import Animate from 'rc-animate';
import Row from '../grid/row';
import Col, { ColProps } from '../grid/col';
2018-07-25 11:42:26 +08:00
import Icon from '../icon';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import warning from '../_util/warning';
import { tuple } from '../_util/type';
import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants';
import { FormContext, FormContextProps } from './context';
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
2015-10-09 13:53:04 +08:00
export interface FormItemProps {
prefixCls?: string;
className?: string;
2016-08-22 17:26:14 +08:00
id?: string;
2016-10-24 12:04:26 +08:00
label?: React.ReactNode;
labelCol?: ColProps;
wrapperCol?: ColProps;
help?: React.ReactNode;
2017-02-27 10:20:46 +08:00
extra?: React.ReactNode;
validateStatus?: (typeof ValidateStatuses)[number];
hasFeedback?: boolean;
required?: boolean;
style?: React.CSSProperties;
2016-08-22 17:26:14 +08:00
colon?: boolean;
}
function intersperseSpace<T>(list: Array<T>): Array<T | string> {
return list.reduce((current, item) => [...current, ' ', item], []).slice(1);
}
export default class FormItem extends React.Component<FormItemProps, any> {
static defaultProps = {
hasFeedback: false,
2016-07-13 11:14:24 +08:00
};
static propTypes = {
prefixCls: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
labelCol: PropTypes.object,
help: PropTypes.oneOfType([PropTypes.node, PropTypes.bool]),
validateStatus: PropTypes.oneOf(ValidateStatuses),
hasFeedback: PropTypes.bool,
wrapperCol: PropTypes.object,
className: PropTypes.string,
id: PropTypes.string,
children: PropTypes.node,
colon: PropTypes.bool,
2016-07-13 11:14:24 +08:00
};
helpShow = false;
componentDidMount() {
const { children, help, validateStatus } = this.props;
warning(
this.getControls(children, true).length <= 1 ||
(help !== undefined || validateStatus !== undefined),
'Form.Item',
'Cannot generate `validateStatus` and `help` automatically, ' +
2018-12-07 20:02:01 +08:00
'while there are more than one `getFieldDecorator` in it.',
);
}
getHelpMessage() {
const { help } = this.props;
if (help === undefined && this.getOnlyControl()) {
2019-03-08 11:25:37 +08:00
const { errors } = this.getField();
if (errors) {
return intersperseSpace(
errors.map((e: any, index: number) => {
let node: React.ReactElement<any> | null = null;
if (React.isValidElement(e)) {
node = e;
} else if (React.isValidElement(e.message)) {
node = e.message;
}
return node ? React.cloneElement(node, { key: index }) : e.message;
}),
2018-12-07 20:02:01 +08:00
);
}
return '';
2016-01-21 16:23:35 +08:00
}
return help;
2016-01-21 16:23:35 +08:00
}
2017-11-21 20:45:24 +08:00
getControls(children: React.ReactNode, recursively: boolean) {
2016-10-24 12:04:26 +08:00
let controls: React.ReactElement<any>[] = [];
const childrenArray = React.Children.toArray(children);
for (let i = 0; i < childrenArray.length; i++) {
if (!recursively && controls.length > 0) {
break;
}
const child = childrenArray[i] as React.ReactElement<any>;
2018-12-07 20:02:01 +08:00
if (
child.type &&
((child.type as any) === FormItem || (child.type as any).displayName === 'FormItem')
) {
continue;
}
if (!child.props) {
continue;
}
2018-12-07 20:02:01 +08:00
if (FIELD_META_PROP in child.props) {
// And means FIELD_DATA_PROP in child.props, too.
controls.push(child);
} else if (child.props.children) {
controls = controls.concat(this.getControls(child.props.children, recursively));
}
}
return controls;
}
getOnlyControl() {
const child = this.getControls(this.props.children, false)[0];
return child !== undefined ? child : null;
}
2017-11-21 20:45:24 +08:00
getChildProp(prop: string) {
2016-08-24 16:09:55 +08:00
const child = this.getOnlyControl() as React.ReactElement<any>;
return child && child.props && child.props[prop];
}
2016-01-28 21:43:45 +08:00
getId() {
return this.getChildProp('id');
2016-01-28 21:43:45 +08:00
}
2016-02-01 10:23:06 +08:00
getMeta() {
2016-07-07 16:59:47 +08:00
return this.getChildProp(FIELD_META_PROP);
2016-02-01 10:23:06 +08:00
}
getField() {
return this.getChildProp(FIELD_DATA_PROP);
}
onHelpAnimEnd = (_key: string, helpShow: boolean) => {
this.helpShow = helpShow;
if (!helpShow) {
this.setState({});
}
2018-12-07 20:02:01 +08:00
};
renderHelp(prefixCls: string) {
const help = this.getHelpMessage();
2017-09-22 21:50:14 +08:00
const children = help ? (
<div className={`${prefixCls}-explain`} key="help">
2017-09-22 21:47:28 +08:00
{help}
</div>
2017-09-22 21:47:28 +08:00
) : null;
if (children) {
this.helpShow = !!children;
}
2017-09-22 21:50:14 +08:00
return (
<Animate
transitionName="show-help"
component=""
transitionAppear
key="help"
onEnd={this.onHelpAnimEnd}
>
{children}
</Animate>
2017-09-22 21:50:14 +08:00
);
2015-10-09 13:53:04 +08:00
}
renderExtra(prefixCls: string) {
const { extra } = this.props;
2018-12-07 20:02:01 +08:00
return extra ? <div className={`${prefixCls}-extra`}>{extra}</div> : null;
2016-04-25 16:25:57 +08:00
}
2016-01-21 16:23:35 +08:00
getValidateStatus() {
const onlyControl = this.getOnlyControl();
if (!onlyControl) {
return '';
}
const field = this.getField();
if (field.validating) {
2016-01-21 16:23:35 +08:00
return 'validating';
}
if (field.errors) {
2016-01-21 16:23:35 +08:00
return 'error';
}
const fieldValue = 'value' in field ? field.value : this.getMeta().initialValue;
if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
2016-01-21 16:23:35 +08:00
return 'success';
}
return '';
2016-01-21 16:23:35 +08:00
}
2018-12-07 20:02:01 +08:00
renderValidateWrapper(
prefixCls: string,
c1: React.ReactNode,
c2: React.ReactNode,
c3: React.ReactNode,
) {
2019-03-08 11:25:37 +08:00
const { props } = this;
const onlyControl = this.getOnlyControl;
2018-12-07 20:02:01 +08:00
const validateStatus =
props.validateStatus === undefined && onlyControl
? this.getValidateStatus()
: props.validateStatus;
2016-01-21 16:23:35 +08:00
let classes = `${prefixCls}-item-control`;
2016-01-21 16:23:35 +08:00
if (validateStatus) {
classes = classNames(`${prefixCls}-item-control`, {
'has-feedback': props.hasFeedback || validateStatus === 'validating',
'has-success': validateStatus === 'success',
'has-warning': validateStatus === 'warning',
'has-error': validateStatus === 'error',
'is-validating': validateStatus === 'validating',
});
2015-10-09 13:53:04 +08:00
}
2018-07-25 11:42:26 +08:00
let iconType = '';
switch (validateStatus) {
case 'success':
2018-08-24 17:36:22 +08:00
iconType = 'check-circle';
2018-07-25 11:42:26 +08:00
break;
case 'warning':
2018-08-24 17:36:22 +08:00
iconType = 'exclamation-circle';
2018-07-25 11:42:26 +08:00
break;
case 'error':
2018-08-24 17:36:22 +08:00
iconType = 'close-circle';
2018-07-25 11:42:26 +08:00
break;
case 'validating':
2018-08-24 17:36:22 +08:00
iconType = 'loading';
2018-07-25 11:42:26 +08:00
break;
default:
iconType = '';
break;
}
2018-12-07 20:02:01 +08:00
const icon =
props.hasFeedback && iconType ? (
<span className={`${prefixCls}-item-children-icon`}>
<Icon type={iconType} theme={iconType === 'loading' ? 'outlined' : 'filled'} />
</span>
) : null;
2018-07-25 11:42:26 +08:00
return (
2018-01-12 16:33:04 +08:00
<div className={classes}>
<span className={`${prefixCls}-item-children`}>
2018-12-07 20:02:01 +08:00
{c1}
{icon}
2018-07-25 11:42:26 +08:00
</span>
2018-12-07 20:02:01 +08:00
{c2}
{c3}
</div>
);
2015-10-09 13:53:04 +08:00
}
renderWrapper(prefixCls: string, children: React.ReactNode) {
return (
<FormContext.Consumer key="wrapper">
{({ wrapperCol: contextWrapperCol, vertical }: FormContextProps) => {
const { wrapperCol } = this.props;
const mergedWrapperCol: ColProps =
('wrapperCol' in this.props ? wrapperCol : contextWrapperCol) || {};
const className = classNames(
`${prefixCls}-item-control-wrapper`,
mergedWrapperCol.className,
);
// No pass FormContext since it's useless
return (
<FormContext.Provider value={{ vertical }}>
<Col {...mergedWrapperCol} className={className}>
{children}
</Col>
</FormContext.Provider>
);
}}
</FormContext.Consumer>
);
2015-10-09 13:53:04 +08:00
}
2016-01-21 16:23:35 +08:00
isRequired() {
2017-01-06 01:33:09 +08:00
const { required } = this.props;
if (required !== undefined) {
return required;
}
if (this.getOnlyControl()) {
2016-02-01 10:23:06 +08:00
const meta = this.getMeta() || {};
const validate = meta.validate || [];
2018-12-07 20:02:01 +08:00
return validate
.filter((item: any) => !!item.rules)
.some((item: any) => {
return item.rules.some((rule: any) => rule.required);
});
2016-01-28 21:43:45 +08:00
}
return false;
2016-01-21 16:23:35 +08:00
}
// Resolve duplicated ids bug between different forms
// https://github.com/ant-design/ant-design/issues/7351
2018-01-24 21:49:14 +08:00
onLabelClick = (e: any) => {
const { label } = this.props;
const id = this.props.id || this.getId();
if (!id) {
return;
}
2019-02-04 22:02:46 +08:00
const formItemNode = ReactDOM.findDOMNode(this) as Element;
const control = formItemNode.querySelector(`[id="${id}"]`) as HTMLElement;
if (control) {
2018-01-24 21:49:14 +08:00
// Only prevent in default situation
// Avoid preventing event in `label={<a href="xx">link</a>}``
if (typeof label === 'string') {
e.preventDefault();
}
2019-02-04 22:02:46 +08:00
if (control.focus) {
control.focus();
}
}
2018-12-07 20:02:01 +08:00
};
renderLabel(prefixCls: string) {
return (
<FormContext.Consumer key="label">
2019-03-08 11:25:37 +08:00
{({ vertical, labelCol: contextLabelCol, colon: contextColon }: FormContextProps) => {
const { label, labelCol, colon, id } = this.props;
const required = this.isRequired();
const mergedLabelCol: ColProps =
('labelCol' in this.props ? labelCol : contextLabelCol) || {};
const labelColClassName = classNames(`${prefixCls}-item-label`, mergedLabelCol.className);
const labelClassName = classNames({
[`${prefixCls}-item-required`]: required,
});
let labelChildren = label;
// Keep label is original where there should have no colon
2019-03-08 11:25:37 +08:00
const computedColon = colon === true || (contextColon !== false && colon !== false);
const haveColon = computedColon && !vertical;
// Remove duplicated user input colon
if (haveColon && typeof label === 'string' && (label as string).trim() !== '') {
labelChildren = (label as string).replace(/[|:]\s*$/, '');
}
return label ? (
<Col {...mergedLabelCol} className={labelColClassName}>
<label
htmlFor={id || this.getId()}
className={labelClassName}
title={typeof label === 'string' ? label : ''}
onClick={this.onLabelClick}
>
{labelChildren}
</label>
</Col>
) : null;
}}
</FormContext.Consumer>
);
2015-10-09 13:53:04 +08:00
}
renderChildren(prefixCls: string) {
const { children } = this.props;
2015-10-09 13:53:04 +08:00
return [
this.renderLabel(prefixCls),
2015-10-09 13:53:04 +08:00
this.renderWrapper(
prefixCls,
2015-10-09 13:53:04 +08:00
this.renderValidateWrapper(
prefixCls,
2016-01-21 16:23:35 +08:00
children,
this.renderHelp(prefixCls),
this.renderExtra(prefixCls),
),
2015-10-09 13:53:04 +08:00
),
];
}
renderFormItem = ({ getPrefixCls }: ConfigConsumerProps) => {
2015-10-09 13:53:04 +08:00
return (
2019-03-08 11:25:37 +08:00
<FormContext.Consumer key="row">
{({ colon: contextColon }: FormContextProps) => {
const { prefixCls: customizePrefixCls, style, colon, className } = this.props;
const computedColon = colon === true || (contextColon !== false && colon !== false);
const prefixCls = getPrefixCls('form', customizePrefixCls);
const children = this.renderChildren(prefixCls);
const itemClassName = {
[`${prefixCls}-item`]: true,
[`${prefixCls}-item-with-help`]: this.helpShow,
[`${prefixCls}-item-no-colon`]: !computedColon,
[`${className}`]: !!className,
};
return (
<Row className={classNames(itemClassName)} style={style}>
{children}
</Row>
);
}}
</FormContext.Consumer>
2015-10-09 13:53:04 +08:00
);
2018-12-07 20:02:01 +08:00
};
2015-10-09 13:53:04 +08:00
render() {
2018-12-07 20:02:01 +08:00
return <ConfigConsumer>{this.renderFormItem}</ConfigConsumer>;
2015-10-09 13:53:04 +08:00
}
}