/* eslint-disable no-param-reassign */ /* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable @typescript-eslint/no-use-before-define */ import React, { ReactNode, useRef, useImperativeHandle, forwardRef, useMemo, memo, isValidElement, Fragment, useEffect, useState, useCallback, } from 'react' import { Form, FormProps, RowProps, FormItemProps, Descriptions, Space, DescriptionsProps } from 'antd' import { DescriptionsItemProps } from 'antd/es/descriptions/Item' import { QuestionCircleOutlined } from '@ant-design/icons' import { useDebounceFn, useMemoizedFn, useUnmount, useUpdate, useUpdateEffect } from 'ahooks' import { run, flatten, isString, isArray, isFunction, isObject, classnames, pick, isUndefined, isExist, debounce, } from '@fexd/tools' import { Hook, ErrorBoundary, Grid, GridProps, GridConfig, Tooltip, filterObjectEmptyValue } from '@fexd/pro-utils' import { useContextSize, ConfigProvider, useProContext } from '@fexd/pro-provider' import hoistStatic from 'hoist-non-react-statics' import { InternalFormInstance } from 'rc-field-form/es/interface' import { HOOK_MARK } from 'rc-field-form/es/FieldContext' import useLocales from '../locales' import ProField from './ProField' import FormItem from './FormItem' import createForm from '../createForm' import useForm from '../useForm' import useValues from '../useValues' import { formSharedContext } from './formSharedContext' import { ProFieldValueTypes, ProFieldValueFieldType, NamePath, RenderFieldsConfig, // ProFormRenderDescriptionParams, ProFormInternalParams, // ProFormRenderParams, ProFormProps, // ProFormRef, } from '../types' import types from '../valueTypes' import defineCoverableProps from '../defineCoverableProps' import useCoverableProps from '../useCoverableProps' import coloringOptions from '../coloringOptions' const CoreProForm = memo( forwardRef(function ProForm( { gridDynamicRender: formPropGridDynamicRender, mode, form: propForm, layout, fields: propFields = [], gridColumns, size: propSize, gridGutter: propRridGutter, filterEmptyParam, normalizeFieldValue, children, render: propRender, // customizedRenderFields, sharedFieldProps = {}, formRef, localeKey, ...restProps }: ProFormProps, ref: any, ) { const contextSize = useContextSize() const size = propSize ?? contextSize const gridGutter = propRridGutter ?? (size === 'small' ? 12 : 16) const [form] = useForm(propForm) const fields = flatten(propFields) const groupRegisterMap = useRef({}) useMemo(() => { fields?.map((field) => { const groupRegisterInfo = field?.group if (isExist(groupRegisterInfo)) { try { const groupList = (isArray(groupRegisterInfo) ? groupRegisterInfo : [groupRegisterInfo]).filter((group) => isString(group), ) as string[] groupList.map((group) => { const currentGroupInfo = groupRegisterMap?.current?.[group] ?? {} const name = (isArray(field?.name) ? field?.name : [field?.name]).join('.') currentGroupInfo[name] = isArray(field?.name) ? [...field?.name] : field?.name groupRegisterMap.current[group] = currentGroupInfo }) } catch {} } }) // @ts-ignore form.groupRegisterMap = groupRegisterMap.current }, [fields]) const fieldsMap = useMemo>( () => fields.reduce((map: any, field: ProFieldValueFieldType) => { const name = String(field?.name) return { ...map, [name]: { mode, ...(map?.[name] ?? {}), ...field }, } }, {}) as any, [fields], ) const fieldsMapRef = useRef({}) const renderField = ( fieldConfig: ProFieldValueFieldType | NamePath, overrideConfig?: Omit, ) => { const fieldName = String((fieldConfig as ProFieldValueFieldType)?.name ?? fieldConfig) if ((isString(fieldConfig) || isArray(fieldConfig)) && !fieldsMap?.[fieldName]) { return null } if (isObject(fieldConfig)) { fieldConfig = { ...(fieldConfig as any) } delete (fieldConfig as any).name delete (fieldConfig as any).type } const field = { ...(fieldsMap?.[fieldName] ?? {}), ...(isObject(fieldConfig) ? { ...(fieldConfig as any) } : {}), ...(overrideConfig ?? {}), } if (!field.name) { return null } return ( { let dynamicField = run(field?.hook, undefined, { form }) if (dynamicField === undefined || dynamicField === true) { dynamicField = {} } return dynamicField }, } : field)} ref={(ref: any) => { fieldsMapRef.current[fieldName] = ref }} /> ) } // 此函数用来应对 hook 动态过滤结果与预留位布局的冲突问题 const renderDynamicFieldHook = useMemoizedFn( ({ disabled = false, // FIXME: Object.values 会对纯数字 key 做默认排序,影响最终渲染,此处待修复 fields = Object.values(fieldsMap), renderContent = () => null, updateDelay = 96, }: any = {}) => ( {() => { const setFieldHooksStateRef = useRef() const fieldHooksRef = useRef() useMemo(() => (fieldHooksRef.current = new Map()), []) const fieldHookNodes = disabled ? null : fields .filter((field: any) => isFunction(field?.hook)) .map((field: any, idx: number) => ( {() => { const dynamicField: any = run(field?.hook, undefined, { form }) useMemo(() => { fieldHooksRef.current.set(field, dynamicField) run(setFieldHooksStateRef, 'current', fieldHooksRef.current) }, [dynamicField, field]) useEffect(() => { return () => { fieldHooksRef.current.delete(field) } }, [field]) return null }} )) const contentNode = ( {() => { const [fieldHooksState, setFieldHooksState] = useState() const update = useUpdate() const [ready, setReady] = useState(disabled ? true : false) useMemo(() => { setFieldHooksStateRef.current = debounce((nextState: any) => { setFieldHooksState(nextState) update() setReady(true) }, updateDelay) }, []) const getFieldHook = useMemoizedFn((field) => run(fieldHooksState, 'get', field)) return ready ? run(renderContent, undefined, { getFieldHook }) : null }} ) useEffect(() => { run(setFieldHooksStateRef, 'current', fieldHooksRef.current) }, []) return ( <> {fieldHookNodes} {contentNode} ) }} ), ) const renderFields = ( configs: | ((ProFieldValueFieldType | NamePath) | ReactNode)[] | ((ProFieldValueFieldType | NamePath) | ReactNode)[][] = Object.keys(fieldsMap), { gridDynamicRender = formPropGridDynamicRender, freeLayout = isArray(configs?.[0]), useBuiltInGrid = true, ...config }: RenderFieldsConfig = {}, ) => { const getFieldConfig = (config: (ProFieldValueFieldType | NamePath) | ReactNode) => { if (isObject(config) && isValidElement(config)) { return null } if (isObject(config)) { return config as ProFieldValueFieldType } const fieldName = String((config as ProFieldValueFieldType)?.name ?? config) return fieldsMap?.[fieldName] } const fields = run(() => { if (freeLayout) { return flatten(configs.map((configs: any) => configs.map(getFieldConfig).filter(Boolean))) } return configs.map(getFieldConfig).filter(Boolean) }) return renderDynamicFieldHook({ disabled: !gridDynamicRender, renderContent: ({ getFieldHook }: any) => { const gridLayout = run(() => { const filterConfig = (config: any) => { const fieldConfig = getFieldConfig(config) if (isFunction(fieldConfig?.hook)) { const dynamicField = run(getFieldHook, undefined, fieldConfig) return dynamicField !== false } return true } const adaptGridConfig = (config: any): GridConfig | undefined => { if ( isValidElement(config) && config?.type === FormItem && isUndefined((config?.props as any)?.label) && layout === 'vertical' ) { config = React.cloneElement(config, { ...(config?.props as any), label: ' ', }) } const content = config?.content ?? (isValidElement(config) ? config : run(() => { const fieldConfig = getFieldConfig(config) if (gridDynamicRender && isFunction(fieldConfig?.hook)) { return renderField({ ...fieldConfig, hook: () => run(getFieldHook, undefined, fieldConfig), }) } return renderField(config) })) const key = run(() => { if (isString(config)) { return config } if (isExist(config?.name)) { return String(config?.name) } return undefined }) return { ...(isObject(config) && !isValidElement(config) ? pick(config, [ 'flex', 'offset', 'order', 'pull', 'push', 'span', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl', ]) : {}), ...(key ? { key } : {}), span: config?.colSpan ?? fieldsMap?.[config?.name ?? config]?.colSpan, content: isString(content) || isValidElement(content) ? content : null, } } if (freeLayout) { return configs.map((configs: any) => configs.filter(filterConfig).map(adaptGridConfig)) } return configs.filter(filterConfig).map(adaptGridConfig) }) return useBuiltInGrid ? ( ) : ( <> {flatten(gridLayout).map((item) => ( {item?.content} ))} ) }, fields, updateDelay: 0, }) } const renderDescriptions: ProFormInternalParams['renderDescriptions'] = ({ gridDynamicRender = formPropGridDynamicRender, group, configs = group ? (Object.values(groupRegisterMap?.current?.[group] ?? {}) ?? [])?.map((names) => String(names)) : Object.keys(fieldsMap), filter = () => true, sort = () => undefined, // 火狐浏览器中 sort: () => 1 与 chrome 表现不一致 https://forum.freecodecamp.org/t/the-sort-method-behaves-different-on-different-browsers/237221/4 descriptionsProps = {}, descriptionsItemProps = {}, } = {}) => { const getFieldConfig = (config: (ProFieldValueFieldType | NamePath) | ReactNode) => { if (isObject(config) && isValidElement(config)) { return null } if (isObject(config)) { return config as ProFieldValueFieldType } const fieldName = String((config as ProFieldValueFieldType)?.name ?? config) return fieldsMap?.[fieldName] } const fieldConfigs = run(() => { return configs .map(getFieldConfig) .filter(filter as any) .filter(Boolean) }) return renderDynamicFieldHook({ disabled: !gridDynamicRender, fields: fieldConfigs, renderContent: ({ getFieldHook }: any) => ( {fieldConfigs .filter((field: any) => { if (isFunction(field?.hook)) { const dynamicField = run(getFieldHook, undefined, field) return dynamicField !== false } return true }) .sort(sort as any) .map((field: any, idx: number) => ( { const dynamicField = run(getFieldHook, undefined, field) const label = dynamicField?.label ?? field?.label const tooltip = dynamicField?.tooltip ?? field?.tooltip return ( isExist(label) && ( {label} : null} /> ) ) })} span={run(descriptionsItemProps, undefined, field)?.span ?? 1} {...run(descriptionsItemProps, undefined, field)} > { const dynamicField = gridDynamicRender ? run(getFieldHook, undefined, field) : run(field?.hook, undefined, ...args) if (!isValidElement(dynamicField) && isObject(dynamicField)) { return { ...dynamicField, label: null, } } return dynamicField }, } : {}), }} /> ))} ), }) } const renderGroupFields = (group: string, options: RenderFieldsConfig = {}) => { const fieldNames = (Object.values(groupRegisterMap?.current?.[group] ?? {}) ?? [])?.map((names) => ({ name: names, })) return renderFields(fieldNames, options) } const defaultRenderFields = () => { if (isArray(propFields?.[0])) { return renderFields(propFields as any) } return renderFields() // const renderer = mode === 'view' ? renderDescriptions : renderFields // return renderer() } const antdFormRef = useRef() const normalizeValues = useMemoizedFn((values: any) => { const params = Object.entries(values).reduce( (prev: Record, [key, value]: [string, any]) => ({ ...prev, [key]: run(types?.[fieldsMapRef.current[key]?.current?.type as ProFieldValueTypes], 'normalizeValue', value) ?? value, }), {}, ) return filterEmptyParam ? filterObjectEmptyValue(params) : params }) const getFieldsValue = useMemoizedFn(() => { const values = form.getFieldsValue() const params = normalizeFieldValue ? normalizeValues(values) : values return filterEmptyParam ? filterObjectEmptyValue(params) : params }) const getValues = useMemoizedFn(async (...args: any[]) => { const values = await form.validateFields(...args) const params = normalizeFieldValue ? normalizeValues(values) : values return filterEmptyParam ? filterObjectEmptyValue(params) : params }) const getProFormInternalParams = useMemoizedFn( () => ({ fieldsMap, antdFormRef, fieldsMapRef, form, getValues, getFieldsValue, normalizeValues, renderField, renderFields, renderDescriptions, renderGroupFields, }) as ProFormInternalParams, ) useImperativeHandle(ref, () => getProFormInternalParams()) useImperativeHandle(formRef, () => getProFormInternalParams()) const preserve = restProps?.preserve ?? false const content = (
{isValidElement(children) || isArray(children) ? ( React.Children.map(children, (child, idx) => child) ) : ( {() => { const customizedRenderFields = isFunction(children) ? children : propRender const renderResult = run( isArray(customizedRenderFields) ? () => renderFields(customizedRenderFields as any[]) : (customizedRenderFields ?? defaultRenderFields), undefined, getProFormInternalParams(), ) if (isArray(renderResult)) { return renderFields(renderResult) } return renderResult }} )}
) return ( {content} ) }), ) export function useProFormRef() { const ref = React.useRef(null) return ref! } export function createProFormRef() { const ref = React.createRef() return ref! } type CoreProFormType = typeof CoreProForm type FormType = typeof Form interface ProFormType extends CoreProFormType { useRef: typeof useProFormRef createRef: typeof createProFormRef createForm: typeof createForm defaultProps: ProFormProps defineCoverableProps: typeof defineCoverableProps useCoverableProps: typeof useCoverableProps coloringOptions: typeof coloringOptions useValues: typeof useValues } interface ProFormType extends Omit { ref?: ProFormProps['formRef'] useForm: typeof useForm } const ProForm: ProFormType = hoistStatic(CoreProForm, Form) as any as ProFormType Object.assign(ProForm, { Item: FormItem, useRef: useProFormRef, createRef: createProFormRef, createForm, useForm, useValues, defineCoverableProps, useCoverableProps, coloringOptions, }) ProForm.defaultProps = { mode: 'edit', layout: 'vertical', fields: [], gridColumns: 3, filterEmptyParam: false, normalizeFieldValue: true, gridDynamicRender: false, } export default ProForm