2022-05-07 14:31:54 +08:00
import type { FormInstance } from 'rc-field-form' ;
import { Field , FieldContext , ListContext } from 'rc-field-form' ;
import type { FieldProps } from 'rc-field-form/lib/Field' ;
import type { Meta , NamePath } from 'rc-field-form/lib/interface' ;
2022-03-10 15:21:10 +08:00
import useState from 'rc-util/lib/hooks/useState' ;
2022-06-06 23:39:00 +08:00
import { supportRef } from 'rc-util/lib/ref' ;
import * as React from 'react' ;
2022-07-19 16:01:31 +08:00
import { useContext } from 'react' ;
import useFormItemStatus from '../hooks/useFormItemStatus' ;
import { ConfigContext } from '../../config-provider' ;
import { cloneElement , isValidElement } from '../../_util/reactNode' ;
import { tuple } from '../../_util/type' ;
import warning from '../../_util/warning' ;
import { FormContext , NoStyleItemContext } from '../context' ;
import type { FormItemInputProps } from '../FormItemInput' ;
import type { FormItemLabelProps , LabelTooltipType } from '../FormItemLabel' ;
import useFrameState from '../hooks/useFrameState' ;
import useItemRef from '../hooks/useItemRef' ;
import { getFieldId , toArray } from '../util' ;
import ItemHolder from './ItemHolder' ;
2019-07-03 20:14:39 +08:00
2021-01-19 11:53:44 +08:00
const NAME_SPLIT = '__SPLIT__' ;
2021-06-04 14:44:41 +08:00
interface FieldError {
errors : string [ ] ;
warnings : string [ ] ;
}
2019-07-03 20:14:39 +08:00
const ValidateStatuses = tuple ( 'success' , 'warning' , 'error' , 'validating' , '' ) ;
2019-11-13 11:48:20 +08:00
export type ValidateStatus = typeof ValidateStatuses [ number ] ;
2019-07-03 20:14:39 +08:00
2020-09-17 16:51:19 +08:00
type RenderChildren < Values = any > = ( form : FormInstance < Values > ) = > React . ReactNode ;
2021-02-27 23:51:35 +08:00
type RcFieldProps < Values = any > = Omit < FieldProps < Values > , 'children' > ;
2020-09-17 16:51:19 +08:00
type ChildrenType < Values = any > = RenderChildren < Values > | React . ReactNode ;
2019-07-03 20:14:39 +08:00
2020-03-08 16:28:33 +08:00
interface MemoInputProps {
value : any ;
2021-06-04 14:44:41 +08:00
update : any ;
2020-04-13 21:30:55 +08:00
children : React.ReactNode ;
2022-08-24 17:07:50 +08:00
childProps : any [ ] ;
2020-03-08 16:28:33 +08:00
}
2020-04-13 21:30:55 +08:00
const MemoInput = React . memo (
( { children } : MemoInputProps ) = > children as JSX . Element ,
2022-08-24 17:07:50 +08:00
( prev , next ) = >
prev . value === next . value &&
prev . update === next . update &&
prev . childProps . length === next . childProps . length &&
prev . childProps . every ( ( value , index ) = > value === next . childProps [ index ] ) ,
2020-03-08 16:28:33 +08:00
) ;
2020-09-17 16:51:19 +08:00
export interface FormItemProps < Values = any >
extends FormItemLabelProps ,
FormItemInputProps ,
2021-02-27 23:51:35 +08:00
RcFieldProps < Values > {
2019-07-03 20:14:39 +08:00
prefixCls? : string ;
2019-07-19 14:07:39 +08:00
noStyle? : boolean ;
2019-07-03 20:14:39 +08:00
style? : React.CSSProperties ;
className? : string ;
2020-09-17 16:51:19 +08:00
children? : ChildrenType < Values > ;
2019-07-03 20:14:39 +08:00
id? : string ;
hasFeedback? : boolean ;
validateStatus? : ValidateStatus ;
required? : boolean ;
2020-06-28 22:41:59 +08:00
hidden? : boolean ;
2020-08-20 14:42:09 +08:00
initialValue? : any ;
2020-09-10 21:00:21 +08:00
messageVariables? : Record < string , string > ;
2020-09-17 17:11:45 +08:00
tooltip? : LabelTooltipType ;
2021-10-29 18:24:50 +08:00
/** @deprecated No need anymore */
2020-05-10 10:39:30 +08:00
fieldKey? : React.Key | React . Key [ ] ;
2019-07-03 20:14:39 +08:00
}
2020-02-03 17:54:33 +08:00
function hasValidName ( name? : NamePath ) : Boolean {
if ( name === null ) {
2022-05-10 15:43:29 +08:00
warning ( false , 'Form.Item' , '`null` is passed as `name` property' ) ;
2020-02-03 17:54:33 +08:00
}
return ! ( name === undefined || name === null ) ;
}
2021-06-04 14:44:41 +08:00
function genEmptyMeta ( ) : Meta {
return {
errors : [ ] ,
warnings : [ ] ,
touched : false ,
validating : false ,
name : [ ] ,
} ;
}
2022-07-14 11:01:39 +08:00
function InternalFormItem < Values = any > ( props : FormItemProps < Values > ) : React . ReactElement {
2019-07-03 20:14:39 +08:00
const {
name ,
2019-07-19 14:07:39 +08:00
noStyle ,
2019-07-03 20:14:39 +08:00
dependencies ,
prefixCls : customizePrefixCls ,
shouldUpdate ,
rules ,
children ,
required ,
2020-03-03 21:57:06 +08:00
label ,
2020-09-10 21:00:21 +08:00
messageVariables ,
2019-07-03 20:14:39 +08:00
trigger = 'onChange' ,
2020-06-14 14:15:17 +08:00
validateTrigger ,
2020-06-28 22:41:59 +08:00
hidden ,
2019-07-03 20:14:39 +08:00
} = props ;
2020-09-11 21:27:51 +08:00
const { getPrefixCls } = useContext ( ConfigContext ) ;
2022-07-19 16:01:31 +08:00
const { name : formName } = useContext ( FormContext ) ;
2021-06-04 14:44:41 +08:00
const isRenderProps = typeof children === 'function' ;
const notifyParentMetaChange = useContext ( NoStyleItemContext ) ;
2020-03-05 16:22:02 +08:00
2020-09-11 21:27:51 +08:00
const { validateTrigger : contextValidateTrigger } = useContext ( FieldContext ) ;
2020-06-14 14:15:17 +08:00
const mergedValidateTrigger =
validateTrigger !== undefined ? validateTrigger : contextValidateTrigger ;
2021-06-04 14:44:41 +08:00
const hasName = hasValidName ( name ) ;
const prefixCls = getPrefixCls ( 'form' , customizePrefixCls ) ;
2021-10-29 18:24:50 +08:00
// ========================= MISC =========================
// Get `noStyle` required info
const listContext = React . useContext ( ListContext ) ;
const fieldKeyPathRef = React . useRef < React.Key [ ] > ( ) ;
2021-06-04 14:44:41 +08:00
// ======================== Errors ========================
// >>>>> Collect sub field errors
const [ subFieldErrors , setSubFieldErrors ] = useFrameState < Record < string , FieldError > > ( { } ) ;
// >>>>> Current field errors
2022-03-10 15:21:10 +08:00
const [ meta , setMeta ] = useState < Meta > ( ( ) = > genEmptyMeta ( ) ) ;
2021-06-04 14:44:41 +08:00
const onMetaChange = ( nextMeta : Meta & { destroy? : boolean } ) = > {
2021-10-29 18:24:50 +08:00
// This keyInfo is not correct when field is removed
// Since origin keyManager no longer keep the origin key anymore
// Which means we need cache origin one and reuse when removed
const keyInfo = listContext ? . getKey ( nextMeta . name ) ;
2021-06-04 14:44:41 +08:00
// Destroy will reset all the meta
2022-03-10 15:21:10 +08:00
setMeta ( nextMeta . destroy ? genEmptyMeta ( ) : nextMeta , true ) ;
2021-06-04 14:44:41 +08:00
// Bump to parent since noStyle
if ( noStyle && notifyParentMetaChange ) {
let namePath = nextMeta . name ;
2021-10-29 18:24:50 +08:00
if ( ! nextMeta . destroy ) {
if ( keyInfo !== undefined ) {
const [ fieldKey , restPath ] = keyInfo ;
namePath = [ fieldKey , . . . restPath ] ;
fieldKeyPathRef . current = namePath ;
}
} else {
// Use origin cache data
namePath = fieldKeyPathRef . current || namePath ;
2021-06-04 14:44:41 +08:00
}
notifyParentMetaChange ( nextMeta , namePath ) ;
2020-03-05 16:22:02 +08:00
}
2021-06-04 14:44:41 +08:00
} ;
// >>>>> Collect noStyle Field error to the top FormItem
const onSubItemMetaChange = ( subMeta : Meta & { destroy : boolean } , uniqueKeys : React.Key [ ] ) = > {
// Only `noStyle` sub item will trigger
setSubFieldErrors ( prevSubFieldErrors = > {
const clone = {
. . . prevSubFieldErrors ,
} ;
2019-07-03 20:14:39 +08:00
2021-06-04 14:44:41 +08:00
// name: ['user', 1] + key: [4] = ['user', 4]
const mergedNamePath = [ . . . subMeta . name . slice ( 0 , - 1 ) , . . . uniqueKeys ] ;
const mergedNameKey = mergedNamePath . join ( NAME_SPLIT ) ;
2019-07-03 20:14:39 +08:00
2021-06-04 14:44:41 +08:00
if ( subMeta . destroy ) {
// Remove
delete clone [ mergedNameKey ] ;
} else {
// Update
clone [ mergedNameKey ] = subMeta ;
}
2019-07-03 20:14:39 +08:00
2021-06-04 14:44:41 +08:00
return clone ;
} ) ;
} ;
2019-07-03 20:14:39 +08:00
2021-06-04 14:44:41 +08:00
// >>>>> Get merged errors
const [ mergedErrors , mergedWarnings ] = React . useMemo ( ( ) = > {
const errorList : string [ ] = [ . . . meta . errors ] ;
const warningList : string [ ] = [ . . . meta . warnings ] ;
2019-07-03 20:14:39 +08:00
2021-06-04 14:44:41 +08:00
Object . values ( subFieldErrors ) . forEach ( subFieldError = > {
errorList . push ( . . . ( subFieldError . errors || [ ] ) ) ;
warningList . push ( . . . ( subFieldError . warnings || [ ] ) ) ;
} ) ;
2021-01-19 11:53:44 +08:00
2021-06-04 14:44:41 +08:00
return [ errorList , warningList ] ;
} , [ subFieldErrors , meta . errors , meta . warnings ] ) ;
2020-06-28 22:41:59 +08:00
// ===================== Children Ref =====================
const getItemRef = useItemRef ( ) ;
2021-06-04 14:44:41 +08:00
// ======================== Render ========================
2020-01-17 11:50:06 +08:00
function renderLayout (
2020-03-05 22:05:22 +08:00
baseChildren : React.ReactNode ,
2020-01-17 11:50:06 +08:00
fieldId? : string ,
isRequired? : boolean ,
2020-04-13 21:30:55 +08:00
) : React . ReactNode {
2020-08-05 10:08:57 +08:00
if ( noStyle && ! hidden ) {
2022-06-15 00:09:31 +08:00
return baseChildren ;
2020-01-17 11:50:06 +08:00
}
return (
2022-07-19 16:01:31 +08:00
< ItemHolder
2020-01-17 11:50:06 +08:00
key = "row"
2022-07-19 16:01:31 +08:00
{ . . . props }
prefixCls = { prefixCls }
fieldId = { fieldId }
isRequired = { isRequired }
errors = { mergedErrors }
warnings = { mergedWarnings }
meta = { meta }
onSubItemMetaChange = { onSubItemMetaChange }
2020-01-17 11:50:06 +08:00
>
2022-07-19 16:01:31 +08:00
{ baseChildren }
< / ItemHolder >
2020-01-17 11:50:06 +08:00
) ;
}
2020-03-10 13:19:35 +08:00
if ( ! hasName && ! isRenderProps && ! dependencies ) {
2020-04-13 21:30:55 +08:00
return renderLayout ( children ) as JSX . Element ;
2020-03-10 13:19:35 +08:00
}
2020-09-10 21:00:21 +08:00
let variables : Record < string , string > = { } ;
2020-03-03 21:57:06 +08:00
if ( typeof label === 'string' ) {
variables . label = label ;
2021-06-30 11:32:34 +08:00
} else if ( name ) {
variables . label = String ( name ) ;
2020-03-03 21:57:06 +08:00
}
2020-09-10 21:00:21 +08:00
if ( messageVariables ) {
variables = { . . . variables , . . . messageVariables } ;
}
2020-03-03 21:57:06 +08:00
2021-06-04 14:44:41 +08:00
// >>>>> With Field
2019-07-03 20:14:39 +08:00
return (
< Field
{ . . . props }
2020-03-03 21:57:06 +08:00
messageVariables = { variables }
2019-07-03 20:14:39 +08:00
trigger = { trigger }
2020-06-14 14:15:17 +08:00
validateTrigger = { mergedValidateTrigger }
2021-06-04 14:44:41 +08:00
onMetaChange = { onMetaChange }
2019-07-03 20:14:39 +08:00
>
2021-06-04 14:44:41 +08:00
{ ( control , renderMeta , context ) = > {
const mergedName = toArray ( name ) . length && renderMeta ? renderMeta . name : [ ] ;
2020-01-17 11:50:06 +08:00
const fieldId = getFieldId ( mergedName , formName ) ;
2019-07-03 20:14:39 +08:00
const isRequired =
required !== undefined
? required
2019-12-12 12:05:59 +08:00
: ! ! (
rules &&
rules . some ( rule = > {
2021-06-04 14:44:41 +08:00
if ( rule && typeof rule === 'object' && rule . required && ! rule . warningOnly ) {
2019-12-12 12:05:59 +08:00
return true ;
}
if ( typeof rule === 'function' ) {
const ruleEntity = rule ( context ) ;
2021-06-04 14:44:41 +08:00
return ruleEntity && ruleEntity . required && ! ruleEntity . warningOnly ;
2019-12-12 12:05:59 +08:00
}
return false ;
} )
) ;
2019-07-03 20:14:39 +08:00
// ======================= Children =======================
const mergedControl : typeof control = {
. . . control ,
} ;
2020-03-05 22:05:22 +08:00
let childNode : React.ReactNode = null ;
2020-07-03 16:25:29 +08:00
2022-05-10 15:43:29 +08:00
warning (
2020-07-03 16:25:29 +08:00
! ( shouldUpdate && dependencies ) ,
'Form.Item' ,
2022-11-14 16:32:34 +08:00
"`shouldUpdate` and `dependencies` shouldn't be used together. See https://u.ant.design/form-deps." ,
2020-07-03 16:25:29 +08:00
) ;
2020-02-03 17:54:33 +08:00
if ( Array . isArray ( children ) && hasName ) {
2022-11-07 17:32:58 +08:00
warning (
false ,
'Form.Item' ,
2022-11-14 16:32:34 +08:00
'A `Form.Item` with a `name` prop must have a single child element. For information on how to render more complex form items, see https://u.ant.design/complex-form-item.' ,
2022-11-07 17:32:58 +08:00
) ;
2020-01-01 20:07:10 +08:00
childNode = children ;
2020-07-03 16:25:29 +08:00
} else if ( isRenderProps && ( ! ( shouldUpdate || dependencies ) || hasName ) ) {
2022-05-10 15:43:29 +08:00
warning (
2020-07-03 16:25:29 +08:00
! ! ( shouldUpdate || dependencies ) ,
2020-01-07 19:17:51 +08:00
'Form.Item' ,
2022-11-07 17:32:58 +08:00
'A `Form.Item` with a render function must have either `shouldUpdate` or `dependencies`.' ,
2020-01-07 19:17:51 +08:00
) ;
2022-05-10 15:43:29 +08:00
warning (
2020-02-03 17:54:33 +08:00
! hasName ,
2020-01-07 19:17:51 +08:00
'Form.Item' ,
2022-11-07 17:32:58 +08:00
"A `Form.Item` with a render function cannot be a field, and thus cannot have a `name` prop." ,
2020-01-07 19:17:51 +08:00
) ;
2020-02-03 17:54:33 +08:00
} else if ( dependencies && ! isRenderProps && ! hasName ) {
2022-05-10 15:43:29 +08:00
warning (
2020-01-17 11:50:06 +08:00
false ,
'Form.Item' ,
2022-11-07 17:32:58 +08:00
'Must set `name` or use a render function when `dependencies` is set.' ,
2020-01-17 11:50:06 +08:00
) ;
2020-05-14 20:54:49 +08:00
} else if ( isValidElement ( children ) ) {
2022-05-10 15:43:29 +08:00
warning (
2020-04-13 21:30:55 +08:00
children . props . defaultValue === undefined ,
2020-03-24 22:23:40 +08:00
'Form.Item' ,
'`defaultValue` will not work on controlled Field. You should use `initialValues` of Form instead.' ,
) ;
2019-07-03 20:14:39 +08:00
const childProps = { . . . children . props , . . . mergedControl } ;
2020-06-11 22:25:58 +08:00
if ( ! childProps . id ) {
childProps . id = fieldId ;
}
2019-07-03 20:14:39 +08:00
2022-08-24 17:07:50 +08:00
if ( props . help || mergedErrors . length > 0 || mergedWarnings . length > 0 || props . extra ) {
const describedbyArr = [ ] ;
if ( props . help || mergedErrors . length > 0 ) {
describedbyArr . push ( ` ${ fieldId } _help ` ) ;
}
if ( props . extra ) {
describedbyArr . push ( ` ${ fieldId } _extra ` ) ;
}
childProps [ 'aria-describedby' ] = describedbyArr . join ( ' ' ) ;
}
if ( mergedErrors . length > 0 ) {
childProps [ 'aria-invalid' ] = 'true' ;
}
if ( isRequired ) {
childProps [ 'aria-required' ] = 'true' ;
}
2020-06-28 22:41:59 +08:00
if ( supportRef ( children ) ) {
childProps . ref = getItemRef ( mergedName , children ) ;
}
2019-07-03 20:14:39 +08:00
// We should keep user origin event handler
2020-06-14 14:15:17 +08:00
const triggers = new Set < string > ( [
. . . toArray ( trigger ) ,
. . . toArray ( mergedValidateTrigger ) ,
] ) ;
2019-07-03 20:14:39 +08:00
triggers . forEach ( eventName = > {
2020-02-18 15:38:00 +08:00
childProps [ eventName ] = ( . . . args : any [ ] ) = > {
mergedControl [ eventName ] ? . ( . . . args ) ;
children . props [ eventName ] ? . ( . . . args ) ;
} ;
2019-07-03 20:14:39 +08:00
} ) ;
2022-08-24 17:07:50 +08:00
// List of props that need to be watched for changes -> if changes are detected in MemoInput -> rerender
const watchingChildProps = [
childProps [ 'aria-required' ] ,
childProps [ 'aria-invalid' ] ,
childProps [ 'aria-describedby' ] ,
] ;
2020-03-08 16:28:33 +08:00
childNode = (
2022-08-24 17:07:50 +08:00
< MemoInput
value = { mergedControl [ props . valuePropName || 'value' ] }
update = { children }
childProps = { watchingChildProps }
>
2020-05-14 20:54:49 +08:00
{ cloneElement ( children , childProps ) }
2020-03-08 16:28:33 +08:00
< / MemoInput >
) ;
2020-07-03 16:25:29 +08:00
} else if ( isRenderProps && ( shouldUpdate || dependencies ) && ! hasName ) {
2020-01-17 11:50:06 +08:00
childNode = ( children as RenderChildren ) ( context ) ;
2019-12-11 16:08:59 +08:00
} else {
2022-05-10 15:43:29 +08:00
warning (
2019-12-11 16:08:59 +08:00
! 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.' ,
) ;
2022-04-08 22:55:42 +08:00
childNode = children as React . ReactNode ;
2019-07-03 20:14:39 +08:00
}
2021-06-04 14:44:41 +08:00
return renderLayout ( childNode , fieldId , isRequired ) ;
2019-07-03 20:14:39 +08:00
} }
< / Field >
) ;
2020-01-17 11:50:06 +08:00
}
2019-07-03 20:14:39 +08:00
2022-07-14 11:01:39 +08:00
type InternalFormItemType = typeof InternalFormItem ;
interface FormItemInterface extends InternalFormItemType {
useStatus : typeof useFormItemStatus ;
}
const FormItem = InternalFormItem as FormItemInterface ;
FormItem . useStatus = useFormItemStatus ;
2019-07-03 20:14:39 +08:00
export default FormItem ;