import React from 'react'; import { createFieldValidator } from './FormikFormField'; import type { IControlledInputProps, IFormInputProps, IFormInputValidation } from '../inputs'; import type { ICommonFormFieldProps } from './interface'; import type { ILayoutProps } from '../layouts'; import { LayoutContext } from '../layouts'; import { renderContent } from './renderContent'; import { firstDefined, noop } from '../../../utils'; import type { IValidator } from '../validation'; import { useValidationData } from '../validation'; import '../forms.less'; export type IFormFieldProps = ICommonFormFieldProps & Partial; const { useState, useCallback, useContext, useMemo } = React; export function FormField(props: IFormFieldProps): JSX.Element { const { input, layout, label, help, required, actions } = props; const { value } = props; // Internal validators are defined by an Input component const [internalValidators, setInternalValidators] = useState([]); const [revalidateRequestId, setRevalidateRequestId] = useState(0); const addValidator = useCallback((v: IValidator) => setInternalValidators((list) => list.concat(v)), []); const removeValidator = useCallback( (v: IValidator) => setInternalValidators((list) => list.filter((x) => x !== v)), [], ); // Capture props.validate when the component initializes (to allow for inline arrow functions) const validate = useMemo(() => props.validate, []); const fieldValidator = useMemo( () => createFieldValidator(label, required, [].concat(validate || noop).concat(internalValidators)), [label, required, validate, internalValidators.length], ); const FieldLayoutFromContext = useContext(LayoutContext); const inputRenderPropOrNode = firstDefined(input, noop); const layoutFromContext = (fieldLayoutProps: ILayoutProps) => ; const layoutRenderPropOrNode = firstDefined(layout, layoutFromContext); const [hasBlurred, setHasBlurred] = useState(false); const touched = firstDefined(props.touched, hasBlurred); // When called, this bumps a revalidateRequestId which in causes validatorResult to be updated const revalidate = () => setRevalidateRequestId((prev) => prev + 1); const validatorResult = useMemo(() => fieldValidator(value), [fieldValidator, value, revalidateRequestId]); const validationMessage = firstDefined(props.validationMessage, validatorResult ? validatorResult : undefined); const validationData = useValidationData(validationMessage, touched); const validation: IFormInputValidation = { touched, addValidator, removeValidator, revalidate, ...validationData, }; const controlledInputProps: IControlledInputProps = { value: props.value, name: props.name || '', onChange: props.onChange || noop, onBlur: (e: React.FocusEvent) => { setHasBlurred(true); props.onBlur && props.onBlur(e); }, }; // Render the input const inputProps: IFormInputProps = { ...controlledInputProps, validation }; const inputElement = renderContent(inputRenderPropOrNode, inputProps); // Render the layout passing the rendered input in const layoutProps: ILayoutProps = { label, help, required, actions, validation, input: inputElement }; return <>{renderContent(layoutRenderPropOrNode, layoutProps)}; }